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

前回の続き

こちらは前回の記事の続きになります。
また前回の記事にコメントを頂いたのですが、それが非常にためになる内容でしたので目を通しておくととっても幸せになれると思います。

前回各々の関数について文字ベースで簡単に説明を行なったので、今回はコードベースで書いて行こうと思います。
ただし、P2P通信について理解が浅い為によく理解してない点が多いのでしっかりとした説明は出来ないです。

関数群

webkitRTCPeerConnection

RTCPeerConnectionの生成関数です。
火狐ではmozRTCPeerConnectionとなっております。
今後RTCPeerConnectionに変わっていくと予想されます。

以下コードです。

var pcConfig = { "iceServers": [{"url": "stun:stun.l.google.com:19302"}]};  
var pcConstraints = { "optional": [{"DtlsSrtpKeyAgreement": true}]};  
var peerConnection = new webkitRTCPeerConnection(pcConfig, pcConstraints);  

上記コードについてですが
1行目: googleさんが公開してくれているSTUNサーバを指定。
2行目: 鍵暗号化通信を行なうかどうかを指定(な気がする)
3行目: コンストラクタよりRTCPeerオブジェクトを生成

WebRTCはP2P通信を行なってストリームの送受信行なうわけですが、P2P通信というのは
ユーザ同士がサーバを介さずに直接通信を行なうものなんだそうです。
またグローバル(外)から見た自分のパソコンのIPアドレスは接続されているルーターグローバルIPになるためにIPアドレスだけではマシンが特定出来なかったりするようです。
なので、それを可能にする為にNAT越えということを行なわないと行けないのですがSTUNサーバを使用して情報を取得してRTCPeerConnectionに情報をセットすることでその部分を勝手に行なってくれているのがWebRTCの良いところ。

僕もいまいち理解していないのですがSkypeなどが通話可能なのもNAT越えをアプリケーションレベルで行なう仕組みがあるからなのでしょう。

また2行目は正直全く理解していないのですが、これを見るところ、通信に対するセキュリティ制御を行なうかどうかの設定のように見えます。
これをonにしていて今のところ困っていないので、あえてfalseにする必要もないかと思います。
ただ処理や通信量に変化があると思われるので、本当に最大限データを送受信したいのであればfalseにすることもあるのかもしれません。

そして、それぞれのオプションをセットしてオブジェクトの生成です。
基本的にここで生成したオブジェクトが起点になりますので、保持しておきましょう。

onicecandidate

STUNサーバに問い合わせた結果、返事が返って来たときに呼び出されるイベントです。
ここに対して設定した関数がそのタイミングで実行されます。

peerConnection.onicecandidate = function(response){  
   webSocket.emit("candidate", response.candidate));  
}  

例えば上記のようにして使用します。
socket.ioを使用してサーバ側の"candidate"という名前のイベントに対してデータを送信しています。
こうすることで、STUNサーバからデータを取得したタイミングそのまま通信相手に対してcandidateを送ることが出来ます。

onaddstream

これは通信相手がstreamを追加したことをキャッチしたタイミングで呼び出されるイベントです。

前回私はストリームをコネクションに追加してからoffer等を投げて繋がないと通信が上手くいかないと言っていましたが、これは誤りかもしれません。

前回の記事のコメントで、onaddstream, onremovestreamはoffer/answerを受け取ってdescriptionの状態を判断して実行されるということをで教えて頂きました。
僕が「動かない!」っと思ったときの処理の流れが
offer -> answer -> (candidate) -> addstream
であったことを考えると、実はこのあとに
offer -> answer -> (candidate) -> addstream -> offer
としていたら映像が映し出されていたのかもしれません。

というのも、onaddstreamは以下のように使用する(ことが多いであろう)からです。

peerConnection.onaddstream = function onRemoteStreamAdded(e) {  
  
    // 現在のブラウザではwebkitの方式でストリームの処理が共通となったようなのでifは不要  
    if (navigator.mozGetUserMedia) {   
        partnerVideo.mozSrcObject = e.stream;  
        partnerVideo.play();  
    } else {  
        var url = webkitURL.createObjectURL(e.stream);  
        partnerVideo.src = url;  
        partnerVideo.play();  
    }  
}   

関数の第一引数として、通信相手のストリーム情報が渡ってくるので
このイベントを取得出来ないとそもそも相手の映像を取得出来ないのです。

ただ、どちらにしても何か明確に理由がない限りは先にstreamをセットする用が楽で良いとは思います。

onremovestream

これは通信相手がストリームを削除したときに呼ばれるイベントです。
このイベントの注意点は単純に通信先でストリームが消されただけではイベントが発生しないことです。

前回でも記載しましたが、ストリームを削除後にoffer/answerを送ることでイベントが発生することが確認出来ています。
これは何かというと、PeerConnectionで通信を行なうときにsetLocalDescriptionなどdescriptionというものをセットするのですが、そのオブジェクトが何かしらの状態を持っているらしく、このデータを見ることでストリームが追加/削除されたことを判定するからのようです。
前回の記事で「gtk2k」様からご指摘いただきました。

では以下コードとなります

peerConnection.onremovestream = function(){  
    if (navigator.mozGetUserMedia) {  
        partnerVideo.mozSrcObject = null;  
        partnerVideo.pause();  
    } else {  
        partnerVideo.src = null;  
        partnerVideo.pause();  
    }  
}  

上記では、ストリームが止まったらビデオを丁寧に止めている処理になります。
特にこのようなことをしなくても送られてくるストリームが止まるので映像等は勝手に停止します。

onnegotiationneeded

addStreamやremoveStreamを行うと発生するイベントです。

私はこのイベントを使用していないのですが非常に便利なイベントなので紹介したいと思います。
ちなみにこれもコメントで頂いた内容になります。

ここまでの説明でonaddstream, onremovestreamはイベントを発生させるのにはofferなどを送る必要があるといいました。
ですので、このイベント処理の中でofferを送る処理を記述することで、addStream, removeStreamなどでストリームの追加/削除を行なったときにofferの再送について考える必要がなくなります。
便利ですね。

以下コードになりますが、イメージですので限りなく意味はありません。

peerConnection.onnegotiationneeded = function(){  
    // この処理はイメージです  
    // webSocket.emit("offer", offer);  
}  

createOffer

オファーを作成する関数です。

まずcreateOffer関数は

createOffer(callback, errorCallback, constraints);  

このような形からなっています。

そして実際使用するときは以下のようになります。

var sdpConstraints = { "mandatory": { "OfferToReceiveAudio": true,   
                                      "OfferToReceiveVideo": true }  
  
connection_.createOffer(function(description){  
    peerConnection.setLocalDescription(description);  
    socket_.emit("offer", description)  
}, null, sdpConstraints);  

callback関数に引数として値が渡ってきます。
descriptionがそれにあたりますね。
そしてこれを自らが生成したdescriptionですので、setLocalDescription関数でRTCPeerConnectionにセットします。
次にこのdescriptionをofferとしてnodeのサーバへ投げています。
通信を確立するためには、offerを通信相手へ届ける必要があるのです。
私の場合はそこでnodeのwebsocket.ioモジュールを使用しています。一般的にこれが使用されているようです。まぁ便利ですからね。

そして上記の例では面倒なので一旦エラーのコールバックは省いていますが、引数としてエラーが渡ってくると予想されます。

第3引数については設定を記載したjsonを指定しています。
それぞれ渡すデータにAudio, Videoがあるかを通知するような役割なのだと理解しています。

setLocalDescription

自身が生成したdescriptionをセットする関数です。

名前から単純なsetterかと思ってしまいますが、この関数にはもう一つ大切な役割があります。
それはSTUNサーバと通信を開始することです。
この関数が正常に処理されてからonicecandidateが着火するようになります。
というよりは、通信が開始されてSTUNサーバから応答が返ってきます、のほうが適切でしょうか。

createOfferの項目で使用例は上げてしまいました一応

peerConnection.createOffer(function(description){  
    peerConnection.setLocalDescription(description);  
})  

使い方については単純なsetterと同じですので特に迷うところはないかと思います。

createAnswer

offerを送ってきた相手に対して返す応答を生成する関数です。

通信相手からのofferを受け取ったら、今度はこちらから「受け取ったよ。通信しましょう」という返事をしなければなりません。
応答を生成するものがこちらです。
また、相手からofferを受け取る流れの中で使用するので、setRemoteDescription関数と一緒に使われることが多いでしょう。

以下コードです。

peerConnection.createAnswer(function (answer){  
    peerConnection.setLocalDescription(answer);  
    socket_.emit('answer', answer);  
}, onError);  

このように使用します。
createAnswer関数にコールバック関数を指定し、コールバック関数の引数としてanswerオブジェクトが渡されます。
またanswerもこの場合自分が生成したdescriptionになるので、setLocalDescription関数に渡してあげます。
answerの通信相手への譲渡はsocket.ioを使用してemitしております。
※emitとはsocket.ioの関数で、サーバ側へデータを送るときに使用します。

setRemoteDescription

通信相手から受け取ったdescriptionをセットする関数です。

自分がofferを投げた側であればその後受け取ったanswerを
自分がanswerを返した側であれば受け取ったofferをセットします。

以下がコードとなります。

peerConnection.setRemoteDescription(  
    new RTCSessionDescription(description)  
);  

peerConnectionに対してdescriptionをセットしています。
また RTCSessionDescriptionコンストラクタの引数として受け取ったdescriptionを指定しています。
これは通信を行なう中で単純なobjectにcastされているdescriptionをRTCPeerConnection用のobjectに再変換していると僕はイメージしています。

addIceCandidate

通信相手から受け取ったicecandidateを追加します。

setLocalDescriptionやsetRemoteDescriptionと同じようなものだと思います。
詳しくは調べていないと知識不足のため良そうでしかありませんが、descriptionは通信の状態。
icecandidateは通信するための情報のように見えます。

以下がコードとなります

var iceCandidate = new RTCIceCandidate({"candidate" : candidate});  
connection_.addIceCandidate(iceCandidate);  

再度オブジェクト化をし、追加という流れでしょうか。

addStream

RTCPeerConnectionにストリームを追加します。

基本的にgetUserMedia関数で取得したstreamを追加することになるかと思います。

peerConnection.addStream(stream);  

この関数が呼ばれる前に通信を確立しても相手に映像が届かないが、その後offerを投げることで相手に映像が届くことから、この前後で生成されるdescriptionに違いが発生するようです。
ですので、descriptionとは通信もしくはconnectionの状態を現すオブジェクトなのかな? と思っています。

removeStream

RTCPeerConnectionからストリームを削除します。

引数に指定したstreamをコネクションから削除します。

peerConnection.removeStream(stream);  

基本的にaddStream関数と同様です。

close

コネクションと閉じます。

peerConnection.close()  

単純にコネクションを閉じているだけですので、オブジェクトが破棄されているわけではありません。
この関数が呼ばれると通信が止まります。

以上で簡単な関数の説明としたいと思います。
次回はnodeのsocket.ioモジュールの簡単な説明です。