がねこまさし

手動でWebRTCの通信をつなげよう ーWebRTC入門2016

連載: WebRTC入門2016 (2)

こんにちは! がねこまさしです。2014年に連載した「WebRTCを使ってみよう!」シリーズを、2016年6月の最新情報に基づき、内容をアップデートして改めてお届けしています。1回目はカメラにアクセスしてみました。2回目となる今回は、WebRTCの通信の仕組みを実感するために、「手動」でP2P通信をつなげてみましょう。

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

WebRTCでは、映像/音声/アプリケーションデータなどをリアルタイムにブラウザ間で送受信することができます。それをつかさどるのが「RTCPeerConnection」です。 RTCPeerConnectionには3つの特徴があります。

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

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

P2P通信を確立するまで

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

Session Description Protocol (SDP)

各ブラウザが通信した内容を示し、テキストで表現されます。例えば次のような情報を含んでいます。

  • 通信するメディアの種類(音声、映像)、メディアの形式(コーデック)、アプリケーションデータ
  • IPアドレス、ポート番号
  • 暗号化の鍵
  • セッションの属性(名前、識別子、アクティブな時間など)→ WebRTCでは使っていないようです

rtcpeer_ip_port

ICE Candidate

P2P通信を行う際にどのような通信経路が使えるかは、お互いのネットワーク環境に依存します。通信経路を定めるための仕組みが「Interactive Connectivity Establishment (ICE)」で、その通信経路の候補が「ICE Candidate」になります。WebRTCの通信を始める前に、可能性のある候補がリストアップされます。

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

候補が見つかったら順次通信を試み、最初につながった経路が採用されます。

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

このように、P2Pを始めるまでの情報のやり取りを「シグナリング」と言います。実は、WebRTCではシグナリングのプロトコルは規定されていません(自由に選べます)。シグナリングを実現するにはWebSocketを使うなど複数の方法がありますが、今回は最も原始的な方法であるコピー&ペーストを試してみましょう。

ちょっと長いですが、こちらのHTMLをお好きなWebサーバーに配置してください。

GitHub Pagesでも公開していますので、すぐに試すことができます。

次に、PCにWebカメラを接続してからFirefox 47 またはChrome 51でアクセスしてみてください。(※Chromeの場合はカメラ映像取得の制限があるので、https://~か http://localhost/~のWebサーバーが必要になります)残念ながら今回はEdgeでは利用できません。

通信するために2つページを開く必要があります。同一ウィンドウで複数タブを開くよりも、別のウィンドウで横に並べたほうが作業しやすいです。

接続手順

接続手順は2014年のものよりも簡略化しました。それでも間違えやすいので慎重に操作してくださいね。

(1) 映像の取得

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

(2) 通信の開始

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

(3)(4) SDPの送信(左→右)

(3)左の[SDP to send:]の内容をコピーし、(4)右の[SDP to receive:]の下のテキストエリアにペーストします。
hand2016_3_4

(5) SDPの受信(右)

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

(6)(7) SDPの返信(左←右)

さっきと反対に(6)右の[SDP to send:]の内容をコピーし、(7)左の[SDP to receive:]の下のテキストエリアにペーストします。
hand2016_6_7

(8) SDPの受信(左)

左の[Receive remote SDP]ボタンをクリックします。しばらくすると(~数秒)P2P通信が始まり両方のウィンドウに2つ目の動画が表示されるはずです。
hand2016_8

上手くいかなかった場合は、コピー範囲が欠けているか、手順が抜けている可能性があります。深呼吸してから両方のブラウザをリロードし、もう一度試してみてください。

それでも通信できない場合は、実はネットワーク環境の問題の可能性があります。この記事の「トラブルシューティング」の章をご覧ください。(あるいはソースのバグや、説明の不備の可能性もあります。何かお気づきの際にはご指摘ください)

トラブルシューティング

これまで何度も手動シグナリングを試して/試していただいて、通信ができないケースがありました。当初は原因が分からなかったのですが、その後に判明したケースを説明します。

外部ネットワークにつながっていない場合

PCが全くネットワークに接続されていない状態では、カメラ映像の取得に成功しても通信ができません。これはネットワークに接続されていない状態では、通信経路の情報であるICE Candidateが収集できないためです。
例え同一PC内で通信を行う場合にも、外部に接続できる状態で利用する必要があります。

ハンズオン等で手動シグナリングを試してもらうことがあるのですが、長いことこの制約に気が付かず通信できないで悩んでいました。

Chrome – Firefox 間での通信

WebRTCではChrome – Chrome間や、Firefox – Firefox 間のように同一種類のブラウザ同士だけでなく、Chrome – Firefox間でも通信することができます。もちろん手動シグナリングでも同様です。
ところが実際に1台のPCで Firefox – Chrome 間で手動シグナリングを行おうとすると、カメラ映像の取得で衝突してしまうケースがあります。この場合は次のどちらかをお試しください

  • (a) 2台のカメラをご用意して、ブラウザごとに違うカメラの映像を取得する
  • (b) 映像は片方のブラウザのみで取得し、そのブラウザから[Connect]で通信を始める
    • → ※この場合は片方向の映像通信となります

裏側で起こっていること

それでは映像通信に成功したところで、その裏側で起きていることを見てみましょう。

Vanilla ICE と Trickle ICE

WebRTCのP2P通信を確立するためのシグナリングでは、次の2種類の情報を交換する必要があると説明しました。

  • SDP
  • ICE candidate

ところが今回の手動シグナリングではSDPしか交換していません。いったいぜんたい、ICE candidateの情報はどうなっているのでしょうか?

実はICE Candidateの情報は、今回交換しているSDPの中に含まれています。実際に私のPCで取得したSDPの一部を掲載します。(※IPアドレスは一部マスクしています)

a=candidate: で始まる行がICE candidateになります。(※仮想化ソフトを入れている影響で複数のネットワークが候補になっています)。 SDPを最初に取得したときにはICE candidateの行は含まれず、その後ICE candidateが収集されるにしたがって、SDPの中に追加されます。
今回は全てのICE candidateが出そろった後に、SDPとまとめて交換しています。このような方式を “Vanilla ICE” と呼びます。
hand2016_vanilla

これに対して、初期のSDPを交換し、その後ICE Candidateを順次交換する方式を “Trickle ICE” と呼びます。すべてのICE candidateを交換し終わる前にP2P通信が始まることがあるので、Trickle ICEの方が一般的に早く接続が確立します。
hand2016_trickle

Offer と Answer

SDPには通信を始める側(Offer)と、通信を受け入れる側(Answer)があります。必ずOffer → Answerの順番でやりとりする必要があります。

ソースコードを追いかけてみよう

Offer SDPの生成

それでは、SDP(+ ICE candidate)のやり取りをソースコードで見てみましょう。まずは[Connect]ボタンを押してSDPを生成するところまでです。(ソースコードは抜粋しています)

発信側で[Connect]ボタンをクリックすると、次の処理が行われます。

  • RTCPeerConnection のオブジェクトを生成
  • RTCPeerConnection.createOffer() で Offer SDPを生成
  • 生成したOffer SDPを、RTCPeerConnection.setLocalDescription()で覚える
  • Trickle ICEの場合はすぐにSDPを送信するが、今回は送信しない

createOffer(), setLocalDescription()は非同期で処理が行われます。従来はコールバックで後続処理を記述していましたが、現在はPromiseを返すので、then()の中に処理を記述します。
2014年の記事ではsetLocalDescription()が非同期であることを意識しおらず、誤った記述になっていました。

ICE candidateの収集

次は ICE candidateの収集です。ICE candidateの収集も非同期に行われるため、RTCPeerConnectionのイベントハンドラで行います。

今回のコードではprepareNewConnection()の中でRTCPeerConnectionオブジェクトを生成し、各種イベントハンドラを設定しています。ICE candidateのためRTCPeerConnection.onicecandidateにイベントハンドラを記述しています。このイベントは複数回発生します。
全てのICE candidateを収集し終わると空のイベントが渡ってきます。このタイミングで最終的なSDPを相手に送信します。今回の手動シグナリングではsendSdp()の中でテキストエリアに表示しています。

Offser SDPの受信

応答側にOffer SDPをペーストして[Receive remote SDP]ボタンをクリックすると、onSdpText() → setOffer() と呼び出されます。

さらに setOffer()の中では次の処理が行われています。

  • PeerConnectionのオブジェクトを生成
  • 受け取ったOffer SDPを setRemoteDescription()で覚える。Promiseを使った非同期処理を行う
  • 成功したら makeAnswer()の中でAnswer SDPを生成

Answer SDPの生成→送信

makeAnswer()の中ではOfferの時と同様な処理が行われます。

  • RTCPeerConnection.createAnswer() で Answer SDPを生成
  • 生成したAnswer SDPを、RTCPeerConnection.setLocalDescription()で覚える。Promiseを使った非同期処理を行う
  • Trickle ICEの場合はすぐにSDPを送信するが、今回は送信しない

この後 RTCPeerConnection.onicecandidate()でICE candidateを収集し、すべて揃ったらsendSdp()でOffer側に送り返します。

Answer SDPの受信

発信側にAnser SDPをペーストして[Receive remote SDP]ボタンをクリックすると、onSdpText() → setAnswer() と呼び出されます。

setAnswer()の中ではRTCPeerConnection.setRemoteDescription()で受け取ったSDPを覚えます。

映像/音声の送受信

PeerConnectionのオブジェクトを生成した際に、送信する映像/音声ストリームをRTCPeerConnection.addStream()で指定しておきます。

SDPの交換が終わると、P2P通信に相手の映像/音声が含まれていればイベントが発生します。従来はRTCPeerConnection.onaddstream() にハンドラを記述していましたが、新しいイベントが策定されRTCPeerConnection.ontrack() にハンドラを記述するようになっています。Firefoxはontrack()がすでに使えるようになっていて、onaddstream()は非推奨になっています。(Chromeは未対応です)

以上で主要な処理の解説は終わりです。

次回は

今回は手動で情報交換を行い、原始的なビデオチャットを動かしてみました。P2P通信が確立するまでの動きを実感していただけたのではないでしょうか?

実際の利用場面では手動シグナリングなんかやってらません。次回はシグナリングサーバーを使って、通信を行ってみたいと思います。

Powered byNTT Communications

tag list

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