シグナリングはお好みで
WebRTCのPeer-to-Peerをつなぐためのシグナリングは、決まったやり方はありません。手動のコピー&ペーストでも、ファイル経由でも、とにかくお互いの情報を交換できれば良いです。
WebSocketを使うのが定番だとは思いますが、今回はMQTTを使ってみました。実際にはWebSocket経由で利用しているので、MQTTならではという活用はできていません。が、「そこにメッセージングの仕組みがある」ならば無理やり試してみましょう。
MQTTとの遭遇
MQTTは、IoTやM2Mの流れで再評価されているプロトコルです。こちらが分かりやすいと思います。
- MQTTについてのまとめ http://tdoc.info/blog/2014/01/27/mqtt.html
- MQTTについて詳しく知る https://sango.shiguredo.jp/mqtt
MQTTを利用するにはブローカー(サーバー)が必要です。自分で動かすのは大変なので、今回はこちらのサービスを使わせていただきました。
- Sango: MQTT as a Service https://sango.shiguredo.jp/
今回、JavaScriptからMQTTを利用するので、MQTT over WebSocket で接続します。使い方はこちらを参考にしました。
- sangoの使い方 - JavaScript WebSocket 編 http://tdoc.info/blog/2014/09/25/mqtt_javascript.html
シグナリング over MQTT
今回のシグナリングは、SDPとICEを一緒に送る簡易シグナリングにします。また、複数の会議室や複数人での通信を考慮しない、シンプルな1対1のみを作ってみます。
今回のポイント
- シグナリングでは双方向に情報をやり取りしなければなりません。MQTTのクライアントは発信(Publish)と受信(Subscribe)の二役を担えるので、それを使って実現しています
- トピック使い分けることで、情報の種類(offer / answer)を識別しています
MQTTブローカーへの接続
var clientId = "web_client_01"; // 適当なクライアントID。今回は一意にしません
var user_name = "you@github"; // githubアカウント
var pass = "yourpassword"; // sango用パスワード
var wsurl = "ws://lite.mqtt.shiguredo.jp:8080/mqtt";
// WebSocketURLとClientIDからMQTT Clientを作成します
var client = new Paho.MQTT.Client(wsurl, clientId);
// connectします
client.connect({userName: user_name, password: pass, onSuccess:onConnect, onFailure: failConnect});
// 接続に成功したら呼び出されます
function onConnect() {
console.log("onConnect");
subscribe("offer"); // offerを待ち受ける
}
ブローカーに接続したら offer SDPを待ち受けるために、/signaling/offer トピックをsubscribeしておきます。その中身はこちら。
function subscribe(waitType) {
// コールバック関数を登録します
client.onMessageArrived = onMessageArrived;
var topic = buildTopic(waitType);
// Subscribeします
client.subscribe(topic);
}
function buildTopic(signalingType) {
var topic = user_name + '/signaling/' + signalingType;
return topic;
}
シグナリングの開始
1対1の通信ですが、2人とも待ち受けているだけでは始まりません。片方が通信を開始しなければなりません。
Offer SDP を作って準備をします。
function makeOffer() {
unsubscribe("offer"); // offerの待ち受けを解除
subscribe("answer"); // answerを待ち受ける
peerConnection = prepareNewConnection();
peerConnection.createOffer(function (sessionDescription) { // in case of success
peerConnection.setLocalDescription(sessionDescription);
}, function () { // in case of error
console.log("Create Offer failed");
}, mediaConstraints);
}
/signaling/offer の待ち受けを解除し、代わりに /signaling/answer を待ち受けます。
SDPを生成すると非同期でICE Candidateが生成され、onicecandidate()イベントハンドラが呼び出されます。すべてのICE Candidateが出そろったら、MQTTで offer SDP+ICE を /signaling/offer に送ります。
function prepareNewConnection() {
var pc_config = {"iceServers":[]};
var peer = null;
try {
peer = new webkitRTCPeerConnection(pc_config);
} catch (e) {
console.log("Failed to create peerConnection, exception: " + e.message);
}
// send any ice candidates to the other peer
peer.onicecandidate = function (evt) {
if (evt.candidate) {
console.log(evt.candidate);
} else {
// すべての candidate が出そろったので、相手に送る
sendSDPTextMQTT(peer.localDescription.type, peer.localDescription.sdp);
}
};
peer.addStream(localStream);
peer.addEventListener("addstream", onRemoteStreamAdded, false);
peer.addEventListener("removestream", onRemoteStreamRemoved, false)
function onRemoteStreamAdded(event) {
remoteVideo.src = window.webkitURL.createObjectURL(event.stream);
}
function onRemoteStreamRemoved(event) {
remoteVideo.src = "";
}
return peer;
}
// MQTTでSDPを送る
function sendSDPTextMQTT(type, text){
var topic = buildTopic(type);
message = new Paho.MQTT.Message(text);
message.destinationName = topic;
client.send(message);
}
※MQTTのクライアントでは、待ち受け(subscribe)と送信(publish)の両方を行えることを利用しています。
シグナリング情報を受け取ったら
MQTTの待ち受けでSDP+ICEを受け取ると、こちらのコールバックが呼ばれます。destinationName (トピック)を見て、offerが届いたのか、answerが届いたのかを区別しています。
// メッセージが到着したら呼び出されるコールバック関数
function onMessageArrived(message) {
if (message.destinationName === buildTopic('answer')) {
onAnswerText(message.payloadString)
}
else if (message.destinationName === buildTopic('offer')) {
onOfferText(message.payloadString)
}
else {
console.warn('Bad SDP topic');
}
}
SDPの種別に応じて、処理を分岐しています。Offerを受け取った場合はPeerConnectionを用意し、Answer SDPを生成します。
function onOfferText(text) {
setOfferText(text);
makeAnswer();
}
function setOfferText(text) {
peerConnection = prepareNewConnection();
var offer = new RTCSessionDescription({
type : 'offer',
sdp : text,
});
peerConnection.setRemoteDescription(offer);
}
function makeAnswer(evt) {
console.log('sending Answer. Creating remote session description...' );
if (! peerConnection) {
console.error('peerConnection NOT exist!');
return;
}
peerConnection.createAnswer(function (sessionDescription) { // in case of success
peerConnection.setLocalDescription(sessionDescription);
}, function () { // in case of error
console.log("Create Answer failed");
}, mediaConstraints);
}
Peer-to-Peerの開始
ICE candiateが生成され、出そろったら先ほどと同じように Answerとして送り返します。
呼び出し側がAnswerを受け取れば、Peer-to-Peer通信が始まります。
function onAnswerText(text) {
console.log("Received answer text...")
setAnswerText(text);
}
function setAnswerText(text) {
if (! peerConnection) {
console.error('peerConnection NOT exist!');
return;
}
var answer = new RTCSessionDescription({
type : 'answer',
sdp : text,
});
peerConnection.setRemoteDescription(answer);
}
複数会議室、複数人に対応するには
今回は実現していませんが、複数会議室に対応するには会議室ごとにトピックを用意すればできそうです。
- /signaling/room1/offer , /signaling/room1/answer
- /signaling/room2/offer , /signaling/room2/answer
同様に複数人での通信を確立するためには、各ペアごとにSDP+ICEを交換しなければなりません。MQTTで実現するには、相手ごとにトピックを細分してあげる必要がありそうです。
- /signaling/room1/offer/toB
- /signaling/room1/offer/fromA/toB
などなど。
まとめ
情報を双方向に伝える手段があれば、WebRTCのシグナリングに使うことができます。すでに利用中の仕組みがあったら、ぜひ試してみてください。Peer-to-Peerを開始するまでの処理が良く理解できると思います。
- 完全なソースコードはこちら: https://gist.github.com/mganeko/160a298bcc9f5c237dd4