OpenSource – Flutter WebRTC Camara

최근 WebRTC 관련 작업을 많이 하고 있습니다. 주로 Flutter를 사용해서 앱을 만들고 있는데 아래 플러터 패키지가 꾸준히 업그레이드 되고 있어서 도움을 많이 받고 있습니다.

플러터용 WebRTC 패키지
https://pub.dev/packages/flutter_webrtc
WebRTC를 이용한 반려동물 CCTV
http://practical.kr/?p=580

시그널링(Signaling)

WebRTC는 offer – answer – candidate 등의 데이터를 주고받는 과정을 거쳐야만 화상통신을 연결 할 수 있는데 이 과정을 시그널링이라고 하고 일반적으로 시그널 서버를 만들어서 카메라와 뷰어를 연결합니다. 규정된 방법은 없고 개발자가 원하는 방법으로 데이터를 전달 하기만 하면 됩니다. 주로 소켓을 많이 이용합니다.

저는 이런 방법을 사용해 보기도 했습니다.

MQTT 기반으로 WebRTC 연결
http://practical.kr/?p=521

이것을 위해 별도의 서버를 운영을 해야 합니다. 언젠가 이런 생각을 해 봤습니다. 로그인 하지 않고 서버 없이 연결 할 수 없을까? 원격은 좀 어렵겠지만 로컬 Wifi에서는 가능하지 않을까? 로컬 Wifi 에 연결된 모든 앱이 소켓을 열고 서로 데이터를 주고 받으면 WebRTC 서버 없이 연결할 수 있겠는데? … 그런데 일단 앱의 IP를 알아야만 되더군요.

IP 찾기( Discover)

Bonsoir 패키지
https://pub.dev/packages/bonsoir

Bonsoir는 Zeroconf(https://ko.wikipedia.org/wiki/Zeroconf) 기반의 Discover 패키지입니다. 아래의 코드처럼 이벤트를 리스닝하고 있으면 브로드개스트를 하는 모든 기기를 찾을 수 있습니다. 애플은 봉주르라는 이름으로 기기들을 연결하고 있습니다.

  Future<void> _discoverService() async {
    _discovery = BonsoirDiscovery(type: _type);
    if (_discovery != null) {
      await _discovery!.ready;

      _discovery!.eventStream!.listen((event) {
        if (event.service != null) {
          ResolvedBonsoirService service = event.service as ResolvedBonsoirService;

          if (event.type == BonsoirDiscoveryEventType.discoveryServiceResolved) {
            final index = _resolvedServices.indexWhere((ResolvedBonsoirService item) => item.ip == service.ip);
            if (index == -1 && service.ip.toString() != "null") {
              _resolvedServices.add(service);
              setState(() {});
            }
          } else if (event.type == BonsoirDiscoveryEventType.discoveryServiceLost) {
            _resolvedServices.remove(service);
            setState(() {});
          }
        }
      });
      await _discovery!.start();
    }
  }

오픈소스 WebRTC 카메라

위의 몇가지 기술을 조립해서 앱을 만들어 봤습니다. 플러터의 장점인 멀티 플랫폼 지원 기능으로 아이폰 / 안드로이드 / 맥 에서 실행 가능 하도록 만들었습니다. 아래 링크에서 소스를 다운 받을 수 있습니다.

우선 맥앱을 빌드해서 맥에 실행하고 스마트폰에 앱을 빌드해서 실행하면 위의 그림과 같이 와이파이 무선 카메라를 실행 할 수 있습니다. 연결에 제한이 없으므로 많은 카메라를 한번에 연결 할 수도 있습니다.

코드를 수정하면 두대의 스마트폰으로 한대는 서버로 다른 스마트폰을 카메라로 사용할 수도 있습니다.

소스코드 링크
https://github.com/bipark/flutter_webrtc_wifi_camera

rtlink.park@gmail.com

브릿지(BritGi – Smart CCTV) 릴리즈

사용하지 않는 스마트폰을 CCTV로 활용할 수 있는 앱

브릿지(BritGi) V1.0

브릿지(BritGi)는 오래되서 사용하지 않는 스마트폰을 CCTV로 활용할 수 있는 앱입니다. 집을 비울때 반려견, 반려묘가 무었을 하고 있는지 내 스마트폰으로 실시간 조회 할 수 있습니다.

다운로드

아래 링크에서 무료로 다운로드 할 수 있습니다. 인앱결재 없이 계속 무료로 사용할 수 있습니다. 아이패드, 안드로이드 패드 지원합니다.

사용방법

브릿지(BritGi)는 오래된 스마트폰을 카메라로 사용하고 현재 쓰고 있는 스마트폰을 조회용으로 사용합니다.

  1. 카메라로 사용할 오래된 스마트폰과 지금쓰고 있는 스마트폰에 앱을 다운로드 하여 설치 합니다.
  2. 두개의 스마트폰에서 동일한 아이디(구글로그인 또는 애플로그인)로 로그인 합니다. (주의) 반드시 동일한 아이디로 로그인 해야 합니다. 카메라폰과 뷰어폰은 다른 종류라도 됩니다. – 예를들면 카메라는 안드로이드 뷰어는 아이폰
  3. 카메라 폰에서 카메라버튼을 눌러 카메라를 실행하고 뷰어폰에서 새로고침을 실행하면 카메라 목록이 화면에 나타나고 목록을 누르면 원격 영상을 볼수 있습니다.
  4. 카메라에 상시전원을 연결하고 고정하여 원격에서 전면, 후면 카메라를 선택하며 360도 거의 모든 방향을 볼 수 있습니다.

WebRTC(Peer-to-Peer)

브릿지(BritGi)는 카메라와 뷰어를 직접 연결하는 방식(WebRTC – Peer to Peer)으로 서버를 거치지 않고 영상정보를 전송합니다. 그래서 서버에 영상이 저장되지 않고 실시간으로만 조회가 가능합니다.

HD / Wifi, LTE, 5G

  • Wifi 연결에서는 HD 화질을 30 frame / sec 로 연결됩니다.
  • 5G/LTE 환경에서는 SD화질로 연결됩니다.
  • 가급적 Wifi 환경에서 사용하세요.

WebRTC, MQTT, Flutter

WebRTC를 이용한 화상/데이터 통신은 기본적으로 시그널 서버가 필요하다. 일반적으로 Websocket을 많이 사용하는데 서버는 클라이언트에서 전송되는 SDP, Candidate를 릴레이 하여 클라이언트가 연결할 상대방의 IP와 Port 정보를 주고 받은 다음 받은 정보를 이용하여 P2p 접속을 시도한다.

MQTT

MQTT는 발행/구독 기반으로 일대일, 일대다 데이터 통신에 적합하고 구독 채널을 트리구조로 구성 할 수 있기 때문에 Websocket에서 채팅방-서브 채팅방을 구현하는 기능을 아주 간단하게 구현 할 수 있다.

아이디어 – Mosquitto

WebRTC 화상통신앱을 만들며 처음부터 이 생각을 했다. WebSocket으로 채팅방을 만들지 않고 MQTT를 이용하면 안될까? Mosquitto 서버라면 보안성 문제도 쉽게 해결 할 수 있고 성능도 우수한 시그널 서버로 활용할 수 있지 않을까? 그래서 한번 해봤다.

Sample Code – Flutter

이 이미지는 대체 속성이 비어있습니다. 그 파일 이름은 1638688086230-1.jpg입니다

아래 링크의 소스 코드는 현재 만들고 있는 CCTV관련앱에서 기본적인 기능만 추출해서 Sample Project를 만들었다. Flutter로 만들어진 오픈소스이며 오픈된 Mosquitto 서버에 접속하여 동일한 토픽을 발행/구독하여 시그널을 주고 받은후 P2p를 연결하여 화상통신을 실행한다.

결론적으로 이 프로젝트에서 WebRTC 통신을 하기 위한 시그널 서버는 만들지않았다.

소스다운로드 링크

https://github.com/bipark/webrtc_mqtt_flutter_phone

P2p연결을 위해 Google Stun 서버를 사용하며 3G, LTE에서 연결이 되지 않을수도 있다. Stun 서버를 통한 P2p 연결이 불가능한 경우는 Turn 서버를 개별적으로 설치하고 운영해야 하는데 오픈소스인 Coturn을 활용하여 운용이 가능하다.

class MQTTManager {
  MqttServerClient? _client;

  MQTTManager(String clientId) {
    _client = MqttServerClient("test.mosquitto.org", clientId);
    _client!.autoReconnect = true;
    _client!.logging(on: false);
    _client!.keepAlivePeriod = 20;
  }

  Future&lt;void&gt; connect(String topic) async {
    try {
      await _client!.connect();
    } on Exception catch (e) {
      print('EXAMPLE::client exception - $e');
    }
    _client!.subscribe(topic, MqttQos.atMostOnce);
  }

  void publishMessage(String topic ,String message) {
    try {
      final builder1 = MqttClientPayloadBuilder();
      builder1.addString(message);
      _client!.publishMessage(topic, MqttQos.atLeastOnce, builder1.payload!);
    } on Exception catch (e) {
      print('EXAMPLE::client exception - $e');
    }
  }

  void subscribeTopic(String topic) {
    _client!.subscribe(topic, MqttQos.atMostOnce);
  }

  void unSubscribeTopic(String topic) {
    _client!.unsubscribe(topic);
  }

  void disconnect() {
    _client!.disconnect();
  }

  get client =&gt; _client;
}

WebRTC와 관련한 코드는 시그널 기능에 따른 많은 코드들이 요구되지만 셈플에서는 가장 기본적인 통신 기능만을 구현 했다.

  _mqtt = MQTTManager(_clientId);
  await _mqtt!.connect(_topic);
  
  ...
  var desc = await _localPc!.createOffer(offerSdpConstraints);
  await _localPc!.setLocalDescription(desc);
  _sendData("offer", desc.sdp);
  
  ...
  void _sendData(event, data) {
    var request = Map();
    request["command"] = "signal";
    request["clientid"] = _clientId;
    request["type"] = event;
    request["data"] = data;
    _mqtt!.publishMessage(_topic, jsonEncode(request).toString());
  }

박병일, 2021.12.5, rtlink.park@gmail.com