WebRTC + Node によるルーム機能付きビデオチャット 1

現在googleで調べても日本語では微妙にかゆいところに手が届かないWebRTCと
Socket.io率いるNodeを使ってルーム機能付きビデオチャットを作ってみました。
細かい制御等は無視していますが、備忘録。
対応ブラウザはchromeのみ!
※知識量も深さもないので、全体的にイメージで記してます。

機能

・ルーム制御: Socket.io
・入室処理: Socket.io
・文字チャット: Socket.io
ビデオチャット: WebRTC

機能的には上記がメインになるかと思います。
通常の文字チャットであれば Socket.io のデフォルトの機能だけで全て網羅出来るので非常に簡単に作ることが出来ます。問題はWebRTCです。

以下、簡易なモジュール説明

メディアアクセス

ビデオチャットを作成する為に、まずはカメラとマイクにアクセスしなければなりません。
HTML5では便利なことにデバイスへアクセスする機能が用意されています。
Navigator.getUserMediaというものがそれにあたります。

バイスへアクセスするには以下のようにします。

if (!navigator.mozGetUserMedia) {  
    navigator.webkitGetUserMedia(defaultOption, bootMediaSuccess, bootMediaError);  
} else {  
    navigator.mozGetUserMedia(defaultOption, bootMediaSuccess, bootMediaError);  
}  

defaultOptionには以下のようなjsonを代入しています。

var defaultOption = { video : true,  
                      audio : true }  

trueになっているものが有効となるので、例えば音声が必要ない場合はaudioをfalseにすることで制御可能です。

bootMediaSuccessはメディアへのアクセスが成功した場合のコールバック関数
bootMediaErrorはメディアへのアクセスが失敗した場合のコールバック関数が指定されています。

メディアへのアクセスが成功すると、関数の第一引数に取得した映像/音声が返ってきます。
私は以下のようにコールバック関数を記載しています。

function bootMediaSuccess(stream){  
    localStream = stream;  
}  

あとで使い回せるようにグローバルな変数として確保した感じです。
初期宣言はしてあるので、あくまでこのモジュール化された無名関数内でのグローバルとして(のつもり)ですが。

メディアへアクセスして映像/音声を取得することは以上で完了です。
余談ですが、getUserMedia系のメソッドは複数呼べば複数ストリームが取得可能ですが、毎回確認文言が表示されます。(当たり前ですね)

次にストリームを画面に表示するための処理です。
そのイメージが以下になります。

var videoTag = $('#video')[0];  
  
if (navigator.mozGetUserMedia) {  
    videoTag.mozSrcObject = stream;  
    videoTag.play();  
} else {  
    var url = webkitURL.createObjectURL(stream);  
    videoTag.src = url;  
    videoTag.play();  
}   

videoタグはjQueryオブジェクトのまま使っても使い辛いので、[0]で普通のオブジェクトとして取得しています。
また火狐とchromeではストリームの扱い方が違うため分岐処理が入っています。(火狐で動くかは知らない)
videoにストリームをセットしているということなのでしょうね。

これで取得したストリームを再生することが出来ます。

では、起動しているデバイスの停止方法です。
何気にここを説明してくれているサイトが少なくて面倒でした。

localStream.stop();  

これで、カメラやマイクが停止します。
非常に簡単ですね。
stream.stop()っていうのがなんか気持ち悪いです。
僕が勝手にストリームだと思って(命名して)いるオブジェクトは実はストリームではないのでしょうか?

以上でビデオチャット作成に必要なデバイス制御を満たせるかと思います。

RTCPeerConnection

WebRTCのP2P通信を行なうコネクションオブジェクトです。
何かと面倒くさいヤツです。最近は慣れてきましたが、それでも面倒くさい罠にハマったりします。
※今現在の私の使用方法も正しくない可能性がかなり高いです。

関数群

まず、今回私が使用した関数を一覧します。
※コメントは僕が勝手にそうやって思っているだけのものです。

・webkitRTCPeerConnection
RTCPeerConnectionを生成する。

・onicecandidate
STUNサーバから応答が返って来たときに呼ばれるイベント

・onaddstream
P2Pが確立し通信相手がストリームをコネクションにセットした時に呼ばれるイベント

・onremovestream
通信相手がストリームを削除したときに呼ばれるイベント(一癖あり

・createOffer
通信したいです、っていうオファーを作成する(だいたいついでにサーバに投げる

・setLocalDescription
通信相手に送ったofferなどをセットする関数

・setRemoteDescription
相手から受け取ったanswerなどをセットする関数

・createAnswer
オファーを受け取ったと相手に伝える返答オブジェクトを作る関数

・addIceCandidate
受け取ったIcdCandidateを追加する関数

・addStream
コネクションにストリームを追加する関数

・removeStream
引数に指定されたストリームを削除する関数

・close
コネクションを閉じる

処理の流れ

簡単に僕のイメージしている処理の流れを説明します。

  1. RTCPeerConnectionを生成する。
  2. on系イベントを設定する
  3. コネクションにストリームを追加する
  4. オファーを作成する
  5. setLocalDescriptionで作成したオファーをセットする
  6. オファーを通信相手へ投げる
  7. STUNサーバから取得したcandidate情報を通信相手へ投げる
  8. 通信相手からアンサーを取得する
  9. setRemoteDescriptionで相手から受け取ったアンサーをセットする。
  10. 通信相手からcandidateを受け取りセットする。
  11. 映像や音声が相手に届く
  12. ストリームを削除する or デバイスを止める
  13. 何故か着火しないonremovestream
  14. オファーを通信相手に再度送信する
  15. 何故か着火するonremovestream

ここで大切なのが ストリームを追加するタイミングです。
私は当初
「普通に考えてP2P通信が始まってから、そのコネクションに対してストリームを送り込むだろ」
と考えて addStreamをアンサーを受け取ってからなどにしていました。
しかし、それでは通信が確立しても映像/音声が届くことはありませんでした。
どうやらofferを投げる前にはストリームを追加しておいた方が良いようです。
具体的にはここを見ましょう。
WebRTCをやるのなら日本語は諦めましょう。

またSTUNサーバとやりとりが行なわれると呼び出される
onicecandidateですが、そもそもSTUNサーバとやり取りを開始するのはいつだよ、という疑問がありました。
いろいろ任意のタイミングでSTUNサーバにリクエストを投げる関数などを探していたのですが、見当たらず。
調べた結果、どうもsetLocalDescriptionがトリガーとなっているようでした。

なので、candidateを取得したいときはsetLocalDescriptionに対してオファーなりをセットしてあげましょう。
オファーをセットするタイミングを任意にすることで制御できそうです。
※それが正攻法なのかはまた別

そしてもう一つ大切なのが、相手のcandidateを受け取ることです。
僕がonicecandidateを着火するタイミングを制御したかったのもこの為です。
つまり、こちらのオファーが相手に届いた時点で相手にSTUNサーバに問い合わせてもらう必要があったのです。

ここで僕が実施した処理が以下
※僕はMyRTCPeerConnectionとかいうクラスを模倣したオブジェクトを使用しています。

webSocket.on('offer', function(offer) {  
    connection.sendAnswer(JSON.parse(offer.data), offer.socketId, offer.connectionId);  
    $("#info").append("<span>get offer</span><br/>");  
    connection.setLocalDescription();  
});  

offerを受け取ったら、answerを返してからsetLocalDescriptionを呼び出すということです。
ここでセットするオファーは別にこの時投げるわけではないのですが、candidateが欲しいが為に呼んでいます。
さらに、この後こちらからもオファーを送りたくなった場合には新たに作り直したオファーをsetLocalDescriptionにセットしてから新しいオファーを投げるという処理をしています。

上記処理については何か違和感がハンパではないために正しい使い方ではない可能性がとっても高いですが、一応動くことは間違いないです。

ここまでで話した関数の雰囲気と処理の流れを理解? してもらえれば、実際に実装を行なうときにかなり楽になると思います。

また実装を行なうときはRTCPeerConnectionをラップしたMyPeerConnectionみたいなのを作ると楽でした。

疲れたので今日はここまで、続きは明日にでも。