がねこまさし

WebRTCに触ってみたいエンジニア必見!手動でWebRTC通信をつなげてみよう

こんにちは! がねこまさしです。前回はWebRTCでカメラを使いましたが、今回は通信をしてみましょう。

※こちらの記事は2014年に書かれました。2016年6月のアップデート記事がありますので、そちらもご参照ください。

WebRTCの通信はどうなっているの?

WebRTCでは、映像や音声などリアルタイムに取得されたデータ(バイトストリーム)を、ブラウザ間で送受信することができます。それを司るのが RTCPeerConnection です。 RTCPeerConnectionには2つの特徴があります。

  • Peer-to-Peer(P2P)の通信 → ブラウザとブラウザの間で直接通信する
  • UDP/IPを使用 → TCP/IPのようにパケットの到着は保障しないが、オーバーヘッドが少ない

多少の情報の欠落があっても許容する替わりに、通信のリアルタイム性を重視しています。UDPのポート番号は動的に割り振られ、49152 ~ 65535の範囲が使われるようです。 rtcpeerconnection

P2P通信が確立するまで

ブラウザ間でP2P通信を行うには、相手のIPアドレスを知らなくてはなりません。また、動的に割り振られるUDPのポート番号も知る必要があります。そのためP2P通信が確立するまでに、WebRTCではいくつかの情報をやり取りしています。

Session Description Protocol (SDP)

各ブラウザの情報を示し、文字列で表現されます。例えば次のような情報を含んでいます。

  • セッションが含むメディアの種類(音声、映像)、メディアの形式(コーデック)
  • IPアドレス、ポート番号
  • P2Pのデータ転送プロトコル → WebRTCでは Secure RTP
  • 通信で使用する帯域
  • セッションの属性(名前、識別子、アクティブな時間、など) → WebRTCでは使っていなさそう

rtcpeer_ip_port

Interactive Connectivity Establishment (ICE)

可能性のある通信経路に関する情報を示し、文字列で表現されます。次のような複数の経路を候補としてリストアップします。

  • P2Pによる直接通信
  • STUNによる、NAT通過のためのポートマッピング → 最終的にはP2Pになる
  • TURNによる、リレーサーバーを介した中継通信

候補が出そろったら、ネットワーク的に近い経路(オーバーヘッドの少ない経路)が選択されます。リストの上から順に優先です。※STUNやTURNについては、別の回で触れたいと思います。

手動シグナリングを実験してみる

このように、P2Pを始めるまでの情報のやり取りを「シグナリング」と言います。実は、WebRTCではシグナリングのプロトコルは規定されていません(自由に選べます)。シグナリングを実現するには複数の方法がありますが、今回は最も原始的な方法を試してみましょう。(理論的にできることは皆知っていますが、実際に試した人は少ないと思います。本邦初公開?)
ちょっと長いですが、こちらのHTMLをお好きなWebサーバーに配置してください。

Chromeを起動し、2つのウィンドウでWebサーバ上のHTMLにアクセスしてください。同一ウィンドウで複数タブを開くよりも、別のウィンドウで横に並べたほうが作業しやすいです。 以降、間違えやすいので慎重に操作してくださいね。

※ 2014/04/03 訂正 掲載したソースの45行目に誤りがありました。大変申し訳ありません。

誤 : var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':false, 'OfferToReceiveVideo':true }};
正 : var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':true, 'OfferToReceiveVideo':true }};

SDPのやり取り

(1) 両方のウィンドウで[Start video]ボタンをクリックします。カメラのアクセスを許可すると、それぞれリアルタイムの映像が表示されます。
handshake_sdp1

(2) 左のウィンドウで[Connect]ボタンをクリックしてください。すると[SDP to send:]のテキストエリアに、ドバドバっとSDPの文字列が表示されます。
handshake_sdp2

(3) これを全選択してコピーし、(4)右のウィンドウの[SDP to receive:]のテキストエリアにペーストします。
handshake_sdp3

(5) 右のウィンドウの[Receive SDP]ボタンをクリックすると、今度は右のウィンドウの[SDP to send:]のテキストエリアに、ドバドバっとSDPの文字列が表示されます。
handshake_sdp4

(6) 右のウィンドウの[SDP to send:]のテキストエリアを全選択してコピー、(7)左に戻って[SDP to receive:]のテキストエリアにペーストします。
handshake_sdp5

(8) 左のウィンドウの[Receive SDP]ボタンをクリックします。ここまででSDPのやり取りが終わりました。
handshake_sdp6

ICE Candidateのやり取り

通信経路を示すICE Candidateは、本来は1つずつやり取りされます。手動でやりとりするのは大変なので、今回は複数経路分まとめて渡してしまいましょう。

(9) 左のウィンドウの[ICE Candidate to send:]のテキストエリアを全選択してコピー、(10)右のウィンドウの[ICE Candidates to receive:]のテキストエリアにペーストします。
handshake_ice1

(11) 右のウィンドウの[Receive ICE Candidate]ボタンをクリックします。
handshake_ice2

(12) 右のウィンドウの[ICE Candidate to send:]のテキストエリアを全選択してコピー、(13)左に戻って[ICE Candidates to receive:]のテキストエリアにペーストします。
handshake_ice3

(14) 左のウィンドウの[Receive ICE Candidate]ボタンをクリックします。
handshake_ice4

これで上手くいけば、P2P通信が始まり両方のウィンドウに2つ目の動画が表示されるはずです。上手くいかない場合は、手順が抜けているか間違っている可能性があります。深呼吸してから両方のブラウザをリロードし、もう一度試してみてください。(私のソースのバグや、説明の間違いの可能性もあります。気がつかれたらご指摘ください)

SDPのやり取りを追ってみる

[Connect]ボタンを押してから、SDPのやり取りをソースで追ってみましょう。 ※ソースは抜粋しています。

発信側で[Connect]ボタンを押すと connect() → sendOffer() という順に呼び出されます。中ではRTCPeerConnectionのインスタンスを生成し、そのインスタンスにSDPの作成を依頼します。SDPには通信開始を依頼するOfferと、応答するAnswerの2種類があり、ここではOfferを使います。

SDPはコールバック関数に渡されるので、何らかの手段を使って相手に渡します。今回のsendSDP()ではテキストエリアにSDPを表示するところまでになります。

応答側にSDPを貼り付け、[Receive SDP]ボタンをクリックすると、onSDP() → onOffer() → setOffer() と呼び出されます。setOffer()の中ではRTCPeerConnectionのインスタンスを生成し、受け取ったOffer SDPを覚えさせます。

次にsendAnswer()で今度は応答用のAnswer SDPの生成を依頼し、コールバックからsendSDP()で発信側に送り返します。ここでもテキストエリアにSDPを表示するところまでになります。

発信側にSDPを貼り付け、[Receive SDP]ボタンをクリックすると、onSDP() → onAnswer() → setAnswer() と呼び出されます。setAnswer()の中では生成済のRTCPeerConnectionのインスタンスに受け取ったAnswer SDPを覚えさせます。

ICEのやり取りも追ってみる

同様に、ICE Candidateのやり取りも見てみましょう。まず、prepareNewConnection()の中身から。

ICE Candidateの生成は、非同期に発生します。そのためRTCPeerConnection.onicecandidateに、コールバック関数を指定します。今回は sendCandidate()を呼び出していますが、その中では例によってテキストエリアに追記する処理を行います。※ICE Candidateは複数個生成され、コールバックもその度に呼び出されます。

ICE Candidateをペーストして[Receive ICE Candidates]ボタンをクリックすると、onICE()でテキストエリアの内容を分割し、1つ1つのICE Candidateを取り出します。それをonCandidate()に渡すと、RTCPeerConnectionのインスタンスにセットします。すべての経路の候補(ICE Candidate)の交換が終わると、P2P通信が開始されます。

手動シグナリングの改良版ソース(2014年4月21日追加)

手動シグナリングは操作が面倒なので、ちょっとでも楽になるように必要なテキストを自動で選択するようにしました。あとはコピー&ペーストでできます。

※手動シグナリングはなぜか動作が不安定で、端末によっては通信が確立しないケースがあります(今のところ成功は4台/7台)。原因がさっぱりわからないのですが、もし心当たりがあったら教えていただけると助かります。情報求む!

次回は

以上、今回は原始的なビデオチャットを動かしてみました。同一PC上ではなく異なるPC間でも(FirewallやNATを挟まない場合は)、SDP/ICEの文字列をチャットなどで渡せば「原理上は」P2P通信は成立するはずです。(私はやってません…)

実際には手動でシグナリングなんかやってられません。次回はシグナリングサーバーを Node.js + socket.io で動かしてみたいと思います。

バックナンバーを読む

Powered byNTT Communications

tag list

アクセシビリティ イベント エンタープライズ デザイン ハイブリッド パフォーマンス ブラウザ プログラミング マークアップ モバイル 海外 高速化 Angular2 AngularJS Canvas Chrome Cordova CSS de:code ECMAScript Edge Firefox Google Google I/O 2014 HTML5 Conference 2013 html5j IoT JavaScript Microsoft Node.js PhoneGap Polymer React Safari SkyWay TypeScript UI UX W3C W3C仕様 Webアプリ Web Components WebGL WebRTC WebSocket