はじめに
この記事は WebRTC Advent Calendar 2014 の記事です。無謀なことに同時に milkcocoa Advent Calender 2014 にもクロスポストしてみました。milkcocoaから来たかたには「WebRTC とはなんぞや?」という感じだと思います。そんな場合は、1日目の記事をまずはご覧ください。
シグナリング、シグナリング、シグナリング
しつこく、WebRTCのシグナリングねたを続けます。
「そこにメッセージングの仕組みがある」ならば、シグナリングをやりたくなるのが人情です。
milkcocoa 登場
milkcocoa は、リアルタイムメッセージングのためのBaaSです。Socket.IOを使っていて、docker上に構築されているそうです。
- JavaScript1行でバックエンドを提供します 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のデータストアを使うことで実現しています。
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の動作イメージ(メッセージは、自分含む全員に届く)
そこで、受け手側が自分宛てのメッセージかどうか判断するようにしました。
//リアルタイムに変更を監視
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)します。
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 のやりとり
無視されるメッセージは省略して、有効なやり取りのみを図にしてみます。
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の様なリアルタイムメッセージングサービスを使えば、それを乗り越えるのが簡単になりますね!