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

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