8
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

milkcocoaAdvent Calendar 2014

Day 22

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

Posted at

はじめに

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

8
8
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?