WebRTC シグナリング over MQTT (over WebSocket)

## シグナリングはお好みで
WebRTCのPeer-to-Peerをつなぐためのシグナリングは、決まったやり方はありません。手動のコピー＆ペーストでも、ファイル経由でも、とにかくお互いの情報を交換できれば良いです。
WebSocketを使うのが定番だとは思いますが、今回はMQTTを使ってみました。実際にはWebSocket経由で利用しているので、MQTTならではという活用はできていません。が、「そこにメッセージングの仕組みがある」ならば無理やり試してみましょう。

## MQTTとの遭遇
MQTTは、IoTやM2Mの流れで再評価されているプロトコルです。こちらが分かりやすいと思います。

* MQTTについてのまとめ http://tdoc.info/blog/2014/01/27/mqtt.html
* MQTTについて詳しく知る https://sango.shiguredo.jp/mqtt

MQTTを利用するにはブローカー（サーバー）が必要です。自分で動かすのは大変なので、今回はこちらのサービスを使わせていただきました。

* Sango: MQTT as a Service   https://sango.shiguredo.jp/

今回、JavaScriptからMQTTを利用するので、MQTT over WebSocket で接続します。使い方はこちらを参考にしました。

* sangoの使い方 - JavaScript WebSocket 編  http://tdoc.info/blog/2014/09/25/mqtt_javascript.html

## シグナリング over MQTT
今回のシグナリングは、SDPとICEを一緒に送る[簡易シグナリング](http://qiita.com/massie_g/items/f5baf316652bbc6fcef1)にします。また、複数の会議室や複数人での通信を考慮しない、シンプルな1対1のみを作ってみます。

### 今回のポイント
* シグナリングでは双方向に情報をやり取りしなければなりません。MQTTのクライアントは発信(Publish)と受信(Subscribe)の二役を担えるので、それを使って実現しています
* トピック使い分けることで、情報の種類(offer / answer)を識別しています

### MQTTブローカーへの接続
```js:
  var clientId = "web_client_01"; // 適当なクライアントID。今回は一意にしません
  var user_name = "you@github"; // githubアカウント
  var pass = "yourpassword"; // sango用パスワード
  var wsurl = "ws://lite.mqtt.shiguredo.jp:8080/mqtt";

  // WebSocketURLとClientIDからMQTT Clientを作成します
  var client = new Paho.MQTT.Client(wsurl, clientId);

  // connectします
  client.connect({userName: user_name, password: pass, onSuccess:onConnect, onFailure: failConnect});

  // 接続に成功したら呼び出されます
  function onConnect() {
    console.log("onConnect");
    subscribe("offer");  // offerを待ち受ける
  }
```
ブローカーに接続したら offer SDPを待ち受けるために、/signaling/offer トピックをsubscribeしておきます。その中身はこちら。

```js:
  function subscribe(waitType) {
    // コールバック関数を登録します
    client.onMessageArrived = onMessageArrived;

    var topic = buildTopic(waitType);
    // Subscribeします
    client.subscribe(topic);
  }

  function buildTopic(signalingType) {
    var topic = user_name + '/signaling/' + signalingType;
    return topic;
  }
```

### シグナリングの開始
1対1の通信ですが、2人とも待ち受けているだけでは始まりません。片方が通信を開始しなければなりません。
Offer SDP を作って準備をします。

```js:
 function makeOffer() {
    unsubscribe("offer");  // offerの待ち受けを解除
    subscribe("answer");  // answerを待ち受ける
    peerConnection = prepareNewConnection();
    peerConnection.createOffer(function (sessionDescription) { // in case of success
      peerConnection.setLocalDescription(sessionDescription);
    }, function () { // in case of error
      console.log("Create Offer failed");
    }, mediaConstraints);
  }
```

/signaling/offer の待ち受けを解除し、代わりに /signaling/answer を待ち受けます。
SDPを生成すると非同期でICE Candidateが生成され、onicecandidate()イベントハンドラが呼び出されます。すべてのICE Candidateが出そろったら、MQTTで offer SDP+ICE を /signaling/offer に送ります。

```js:
  function prepareNewConnection() {
    var pc_config = {"iceServers":[]};
    var peer = null;
    try {
      peer = new webkitRTCPeerConnection(pc_config);
    } catch (e) {
      console.log("Failed to create peerConnection, exception: " + e.message);
    }

    // send any ice candidates to the other peer
    peer.onicecandidate = function (evt) {
      if (evt.candidate) {
        console.log(evt.candidate);
      } else {
         // すべての candidate が出そろったので、相手に送る
        sendSDPTextMQTT(peer.localDescription.type, peer.localDescription.sdp);
      }
    };

    peer.addStream(localStream);
    peer.addEventListener("addstream", onRemoteStreamAdded, false);
    peer.addEventListener("removestream", onRemoteStreamRemoved, false)
    function onRemoteStreamAdded(event) {
      remoteVideo.src = window.webkitURL.createObjectURL(event.stream);
    }
    function onRemoteStreamRemoved(event) {
      remoteVideo.src = "";
    }

    return peer;
  }

  // MQTTでSDPを送る
  function sendSDPTextMQTT(type, text){
    var topic = buildTopic(type);
    message = new Paho.MQTT.Message(text);
    message.destinationName = topic;
    client.send(message);
  }
```

※MQTTのクライアントでは、待ち受け(subscribe)と送信(publish)の両方を行えることを利用しています。

### シグナリング情報を受け取ったら
MQTTの待ち受けでSDP+ICEを受け取ると、こちらのコールバックが呼ばれます。destinationName (トピック)を見て、offerが届いたのか、answerが届いたのかを区別しています。

```js:
  // メッセージが到着したら呼び出されるコールバック関数
  function onMessageArrived(message) {
     if (message.destinationName === buildTopic('answer')) {
       onAnswerText(message.payloadString)
     }
     else if (message.destinationName === buildTopic('offer')) {
       onOfferText(message.payloadString)
     }
     else {
       console.warn('Bad SDP topic');
     }
  }
```

SDPの種別に応じて、処理を分岐しています。Offerを受け取った場合はPeerConnectionを用意し、Answer SDPを生成します。

```js:
  function onOfferText(text) {
    setOfferText(text);
    makeAnswer();
  }

  function setOfferText(text) {
    peerConnection = prepareNewConnection();
    var offer = new RTCSessionDescription({
      type : 'offer',
      sdp : text,
    });
    peerConnection.setRemoteDescription(offer);
  }

  function makeAnswer(evt) {
    console.log('sending Answer. Creating remote session description...' );
    if (! peerConnection) {
      console.error('peerConnection NOT exist!');
      return;
    }

    peerConnection.createAnswer(function (sessionDescription) { // in case of success
      peerConnection.setLocalDescription(sessionDescription);
    }, function () { // in case of error
      console.log("Create Answer failed");
    }, mediaConstraints);
  }
```

### Peer-to-Peerの開始
ICE candiateが生成され、出そろったら先ほどと同じように Answerとして送り返します。
呼び出し側がAnswerを受け取れば、Peer-to-Peer通信が始まります。

```js:
  function onAnswerText(text) {
    console.log("Received answer text...")
    setAnswerText(text);
  }

  function setAnswerText(text) {
    if (! peerConnection) {
      console.error('peerConnection NOT exist!');
      return;
    }
    var answer = new RTCSessionDescription({
      type : 'answer',
      sdp : text,
    });
    peerConnection.setRemoteDescription(answer);
  }
```
### 複数会議室、複数人に対応するには
今回は実現していませんが、複数会議室に対応するには会議室ごとにトピックを用意すればできそうです。

* /signaling/room1/offer , /signaling/room1/answer
* /signaling/room2/offer , /signaling/room2/answer

同様に複数人での通信を確立するためには、各ペアごとにSDP+ICEを交換しなければなりません。MQTTで実現するには、相手ごとにトピックを細分してあげる必要がありそうです。

* /signaling/room1/offer/toB
* /signaling/room1/offer/fromA/toB

などなど。

## まとめ
情報を双方向に伝える手段があれば、WebRTCのシグナリングに使うことができます。すでに利用中の仕組みがあったら、ぜひ試してみてください。Peer-to-Peerを開始するまでの処理が良く理解できると思います。

* 完全なソースコードはこちら: https://gist.github.com/mganeko/160a298bcc9f5c237dd4