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