milkcocoaでWebRTCのシグナリングをやってみた

## はじめに
この記事は [milkcocoa Advent Calender 2014](http://qiita.com/advent-calendar/2014/mlkcca) の記事です。同時に [WebRTC Advent Calendar 2014](http://qiita.com/advent-calendar/2014/webrtc) の記事でもあります。milkcocoaから来た人には「WebRTC とはなんぞや？」という感じだと思います。そんな場合は、WebRTCぼ1日目の記事を先ずご覧ください。

* WebRTCコトハジメ http://qiita.com/Tukimikage/items/286f569d110daede721e

## シグナリング、シグナリング、シグナリング
しつこく、WebRTCのシグナリングねたを続けます。
「そこにメッセージングの仕組みがある」ならば、シグナリングをやりたくなるのが人情です。

## milkcocoa 登場

milkcocoa は、リアルタイムメッセージングのためのBaaSです。Socket.IOを使っていて、docker上に構築されているそうです。 

* JavaScript１行でバックエンドを提供します https://mlkcca.com/
* チュートリアル https://mlkcca.com/tutorial/page1.html
* API リファレンス https://mlkcca.com/document/api-js.html
* Node学園での発表資料 https://slidebean.com/p/TO1HWyiK6r/milkcocoa

リアルタイムメッセージングと来れば、シグナリングサーバーにうってつけですね。

## 複数会議室、複数人でのビデオチャット

MQTTの利用例では1対1のみ実装していたので、今回は複数人での利用を実現してみます。実際の内容は、こちらの記事と同じです。

* シグナリングサーバーを応用！ 「WebRTCを使って複数人で話してみよう」 http://html5experts.jp/mganeko/5438/

この記事では node.js + socket.io でシグナリングサーバーを自分で立てていましたが、それをそっくりmilkcocoaに置き換えます。

### 複数会議室の実現

socket.io の場合は、ルーム機能(join/leave)を使って、複数会議室を実現していました。
今回はmilkcocoaのデータストアを使うことで実現しています。

```js:
  var selfID = '';
  var room = getRoomName();
  var milkcocoa = new MilkCocoa("https://io-appid.mlkcca.com/"); // 自分のAppIDに置き換えてください
  var ds = milkcocoa.dataStore("signaling/" + room);

  function getRoomName() { // たとえば、 URLに  ?roomname  とする
    var url = document.location.href;
    var args = url.split('?');
    if (args.length > 1) {
      var room = args[1];
      if (room != "") {
        return room;
      }
    }
    return "_defaultroom";
  }
```

会議室ごとに、異なるdataStoreを利用しています。

### クライアントの識別
複数人のシグナリングでは、会議室内の全員宛てにメッセージを送る場合と、特定の相手だけに送りたい場合があります。そのベースとなるIDは自分で管理ても良いのですが、今回は anonymousログインを利用して、milkcocoa側で振り出してもらいます。

```js:
  var signalingReady = false;
  milkcocoa.anonymous(function(err, user) {
    if (user) {
      selfID = user.id;
      console.log('anonymous() id=' + selfID);
      signalingReady = true;
    }
    else {
      console.error('milk anonymous() error');
    }
  });
```

### 全員宛てのメッセージ、特定の相手へのメッセージ
実はmilkcocoaを使って特定の相手だけにメッセージを送る方法は見つけられませんでした。（※もしご存知でしたら教えてください）
milkcocoaの動作イメージ（メッセージは、自分含む全員に届く）
![milkcocoa_image.png](https://qiita-image-store.s3.amazonaws.com/0/19403/08fbdecf-22a5-f9d7-16e3-b1806114835b.png)


そこで、受け手側が自分宛てのメッセージかどうか判断するようにしました。

```js:
  //リアルタイムに変更を監視
  ds.on("push", function(data) {
    onMessage(data.value);
  });

 
  // socket: accept connection request
  function onMessage(evt) {
    var id = evt.from;
    var target = evt.sendto;
    var conn = getConnection(id);
    
    if (id === selfID) {
      // skip message from myself （自分発信のメッセージはスキップ）
      console.log('.... skip self message ...' + id);
      return;
    }
    if( (target) && (target != '') && (target != selfID) ) {
      // skip message to someone else （自分以外へ宛てたメッセージはスキップ）
      console.log('.... skip others message ...' + target);
      return;
    }
    // 全員宛て、あるいは自分宛てのメッセージを内容に応じて処理する 

    if (evt.type === 'call') {
      // ... 省略
    }
    else if (evt.type === 'response') {
      // ... 省略
    }
    else if ( (evt.type === 'bye') && isPeerStarted() ) {
      // ... 省略
    } 
  } 
```

## シグナリングの流れ
すっかりおなじみですが、WebRTCのシグナリングでは次の2種類の情報を交換します。

* offer/answer SDP の交換
* ICE candidate の交換

### SDP のやりとり
3人(browserA, browserB, browserC)のケースを考えてみます。milkcocoaのサーバー側は省略し、ブラウザ同士の通信のみを取り出すと次の流れになります。
自分発信のメッセージや、自分以外の特定の相手宛てのメッセージは無視(ignore)します。
![milkcocoa_sdp.png](https://qiita-image-store.s3.amazonaws.com/0/19403/6cd71476-3d1c-40df-7d90-62e2b5f6464c.png)

callを送る部分のソース

```js:
function call() {
  if (! isLocalStreamStarted()) {
    alert("Local stream not running yet. Please [Start Video] or [Start Screen].");
    return;
  }

  // call others, in same room
  ds.push({type: "call", from: selfID});
} 
```

callを受け取り、responseを送る部分のソース

```js:
function onMessage(evt) {
  var id = evt.from;
  var target = evt.sendto;
  var conn = getConnection(id);

  // ... 省略 ...

  if (evt.type === 'call') {
    console.log('--- got call ---');
    if (! isLocalStreamStarted()) {
      return;
    }
    if (conn) {
      return; // already connected
    }
    if (isConnectPossible()) {
      ds.push({type: "response", sendto: id, from: selfID});
    }
    else {
      console.warn('max connections. so ignore call');
    }
    return;
  }

  // ... 省略 ...

} 
```

どっちも ds.push()を使ってメッセージを送信しています。
同様に、offer / answer SDP の送受信もこちら

```js:
function onMessage(evt) {
  var id = evt.from;
  var target = evt.sendto;
  var conn = getConnection(id);

  // ... 省略 ...
  else if (evt.type === 'response') {
    sendOffer(id);
    return;
  } else if (evt.type === 'offer') {
    onOffer(evt);
  } else if (evt.type === 'answer' && isPeerStarted()) { // **
    onAnswer(evt);
  }

  // ... 省略 ...

}


function sendOffer(id) {
  var conn = getConnection(id);
  if (!conn) {
    conn = prepareNewConnection(id);
  }
  conn.peerconnection.createOffer(function (sessionDescription) { // in case of success
    conn.peerconnection.setLocalDescription(sessionDescription);
    sessionDescription.sendto = id;
    sessionDescription.from = selfID;
    sendSDP(sessionDescription);
  }, function () { // in case of error
    console.log("Create Offer failed");
  }, mediaConstraints);
}

function sendSDP(sdp) {
  ds.push(sdp);
}


function onOffer(evt) {
  setOffer(evt);
  sendAnswer(evt);
}

function sendAnswer(evt) {
  var id = evt.from;
  var conn = getConnection(id);
  if (! conn) {
    console.error('peerConnection not exist!');
    return
  }

  conn.peerconnection.createAnswer(function (sessionDescription) {
    // in case of success
    conn.peerconnection.setLocalDescription(sessionDescription);
    sessionDescription.sendto = id;
    sessionDescription.from = selfID;
    sendSDP(sessionDescription);
  }, function () { // in case of error
    console.log("Create Answer failed");
  }, mediaConstraints);
}
```



### ICE candidate のやりとり
無視されるメッセージは省略して、有効なやり取りのみを図にしてみます。
![milkcocoa_ice.png](https://qiita-image-store.s3.amazonaws.com/0/19403/40956145-e808-1600-13f0-cc1541f62f5c.png)

ICE candidateも、同じくds.push()を使って送り、onMessage()で受け取ります。

```js:
function sendCandidate(candidate) {
  ds.push(candidate);
}


function onMessage(evt) {
  var id = evt.from;
  var target = evt.sendto;
  var conn = getConnection(id);

  // ... 省略 ...
  } else if (evt.type === 'candidate' && isPeerStarted()) {
    onCandidate(evt);
  }

  // ... 省略 ...
}

function onCandidate(evt) {
  var id = evt.from;
  var conn = getConnection(id);
  if (! conn) {
    console.error('peerConnection not exist!');
  return;
  }

  var candidate = new RTCIceCandidate({sdpMLineIndex:evt.sdpMLineIndex,
     sdpMid:evt.sdpMid, candidate:evt.candidate});
  conn.peerconnection.addIceCandidate(candidate);
}
```

### 全体のソース
完全なソースは、こちらに置いておきます。 https://gist.github.com/mganeko/0662e3c5f01247c81b88

## まとめ
WebRTCでシグナリングサーバーを用意するのが、ハードルの一つになっています。milkcocoaの様なリアルタイムメッセージングサービスを使えば、それを乗り越えるのが簡単になりますね！