WebRTC
milkcocoa
milkcocoaDay 22

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

More than 3 years have passed since last update.

はじめに

この記事は milkcocoa Advent Calender 2014 の記事です。同時に WebRTC Advent Calendar 2014 の記事でもあります。milkcocoaから来た人には「WebRTC とはなんぞや?」という感じだと思います。そんな場合は、WebRTCぼ1日目の記事を先ずご覧ください。

シグナリング、シグナリング、シグナリング

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

milkcocoa 登場

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

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

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

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

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

複数会議室の実現

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

  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側で振り出してもらいます。

  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

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

  //リアルタイムに変更を監視
  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

callを送る部分のソース

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を送る部分のソース

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 の送受信もこちら

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

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

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の様なリアルタイムメッセージングサービスを使えば、それを乗り越えるのが簡単になりますね!