WebRTC 接続
WebRTC ではさまざまなプロトコルが相互作用してピアー間の接続を確立し、データやメディアの転送を行いますが、この記事ではその仕組みを解説します。
メモ: このページは、構成の整合性とコンテンツの網羅性を確保するため、大幅な書き直しが必要です。ここにある情報の多くは有益です。しかし、現時点ではいわば情報の寄せ集めのような状態であるため、構成が散らかっています。
シグナリング
残念なことに、WebRTC は中間に何らかのサーバーがなければ接続を作成できません。このサーバーをシグナルチャンネル、またはシグナリングサービスと呼びます。接続を確立する前に情報を交換する伝達手段はどんなものでも構いません。E メール、はがき、伝書鳩でも...決めるのはあなたです。
交換する必要のある情報はオファーとアンサーと呼ばれ、その中身は下記で説明する SDP です。
ピアー A が接続を初期化する側とすると、ピアー A がオファーを作成します。それから選択されたシグナルチャンネルを使ってピアー B にオファーを送ります。ピアー B はシグナルチャンネルからオファーを受け取ると、アンサーを作成します。それからピアー B はピアー A にシグナルチャンネルを使ってアンサーを送り返します。
セッションディスクリプション
WebRTC 接続のエンドポイント設定はセッションディスクリプションと呼ばれます。そこに含まれる情報は、送られるメディアの種類、形式、使用される転送プロトコル、エンドポイントの IP アドレスとポート、またその他メディア転送エンドポイントをディスクリプションするのに必要な情報です。この情報を セッションディスクリプションプロトコル (SDP) を使って交換し、保存します。 SDP データ形式の詳細は RFC 8866 にあります。
ユーザーが WebRTC コールを他のユーザーに開始するとき、オファーと呼ばれる特別なディスクリプションを作成します。コールする側がコールに必要な設定を提案し、そのすべての情報をオファーのディスクリプションに盛り込みます。受け取る側はアンサーを返します。アンサーは受け取る側が用意するディスクリプションです。このようにして、両デバイスがお互いにメディアデータの交換に必要な情報を共有します。この交換は Interactive Connectivity Establishment (ICE) を使って行われます。ICE とは二つのデバイスが Network Address Translation (NAT) によって隔てられていてもオファーとアンサーを交換するために媒介を利用できるようにするプロトコルです。
各ピアーは 2 つのディスクリプションを手に入れます。 ローカルディスクリプション が自分側のディスクリプションで、 リモートディスクリプション が相手側のディスクリプションです。
オファー/アンサーの交換はコールを最初に確立する際に実行されますが、それだけでなくフォーマットや他の設定に変更が必要なときにも随時実行されます。コールの新規作成時でも既存の設定変更時でも、いずれにしてもオファーとアンサーを交換するために以下のような基本的なステップが実行されます。なお、ここでは ICE レイヤーは除外しています。
- 呼び出す側が
MediaDevices.getUserMediaを通じてローカルメディアを取得する - 呼び出す側が
RTCPeerConnectionを作成し、RTCPeerConnection.addTrack()を実行する。(addStreamが非推奨であるため) - 呼び出す側がオファーを作成するために
RTCPeerConnection.createOffer()を実行する - 呼び出す側がオファーを ローカルディスクリプション (ローカル側の接続のディスクリプション) として設定するために
RTCPeerConnection.setLocalDescription()を実行する - 呼び出す側は setLocalDescription() を実行した後、STUN サーバーに問い合わせて ICE 候補を生成する
- 呼び出す側がシグナリングサーバーを使ってオファーを届けたい相手に送る
- 受け取る側がオファーを受け取り、それを リモートディスクリプション (相手側の接続のディスクリプション) として記録するために
RTCPeerConnection.setRemoteDescription()を実行する - 受け取る側がコールに必要なセットアップを行う。ローカルメディアを取得し、
RTCPeerConnection.addTrack()を通じてメディアトラックをピアー接続にアタッチする - 受け取る側が
RTCPeerConnection.createAnswer()を実行することでアンサーを作成する - 受け取る側が
RTCPeerConnection.setLocalDescription()に作成したアンサーを渡して実行し、アンサーを自身の ローカルディスクリプション としてセットする。この時点で受け取る側は両側の接続設定を知ることになる。 - 受け取る側がシグナリングサーバーを使ってアンサーを呼び出す側に送る
- 呼び出す側がアンサーを受け取る。
- 呼び出す側がアンサーを リモートディスクリプション として設定するために
RTCPeerConnection.setRemoteDescription()を実行する。これで呼び出す側も両者の設定を知ることになる。設定した通りにメディアが流れ始める。
待機中および現在のディスクリプション
このプロセスをさらに深く掘り下げてみると、これら 2 つのディスクリプションを返すプロパティである localDescription と remoteDescription は、見た目ほど単純ではないことがわかります。再ネゴシエーションの際、互換性のない形式が提案されたためにオファーが拒否される可能性があるため、それぞれのエンドポイントは新しい形式を提案できる一方で、相手側のピアが受け入れるまでは実際にその形式に切り替えないようにする必要があります。そのため、WebRTC では待機中や現在のディスクリプションが使用されます。
現在のディクリプション(RTCPeerConnection.currentLocalDescription および RTCPeerConnection.currentRemoteDescription プロパティによって返されるもの)は、その接続で現在実際に使用されているディクリプションを表します。これは、双方が完全に合意して使用している最新の接続です。
待機中のディクリプション(RTCPeerConnection.pendingLocalDescription および RTCPeerConnection.pendingRemoteDescription によって返される)は、それぞれ setLocalDescription() または setRemoteDescription() の呼び出し後に、現在検討中のディクリプションを示しています。
ディスクリプション(RTCPeerConnection.localDescription および RTCPeerConnection.remoteDescription によって返されるもの)を読み込む際、待機中のディスクリプションがある場合(つまり、待機中のディスクリプションが null ではない場合)、返される値は pendingLocalDescription/pendingRemoteDescription の値となります。それ以外の場合は、現在のディスクリプション(currentLocalDescription/currentRemoteDescription)が返されます。
setLocalDescription() または setRemoteDescription() を呼んでディスクリプションを変更する場合、指定されたディスクリプションが「待機中のディスクリプション」として設定され、WebRTC レイヤーはそのディスクリプションが受け入れられるかどうかを評価し始めます。提案されたディスクリプションについて合意が成立すると、currentLocalDescription または currentRemoteDescription の値が「待機中のディスクリプション」に変更され、「待機中のディスクリプション」は繰り返し null に設定され、待機中のディスクリプションが存在しないことを示します。
メモ:
pendingLocalDescription には、検討中のオファーやアンサーだけでなく、そのオファーやアンサーが作成されてからこれまでに収集されたすべてのローカル ICE 候補も含まれます。同様に、pendingRemoteDescription には、RTCPeerConnection.addIceCandidate() の呼び出しによって指定されたすべてのリモート ICE 候補が含まれます。
詳細については、これらのプロパティやメソッドに関する個別の記事をご覧ください。また、WebRTC で対応しているコーデックや、各ブラウザーとの互換性に関する情報については、WebRTC で使用されるコーデック を参照してください。コーデックガイドでは、ニーズに最適なコーデックを選択するための指針も提供されています。
ICE 候補
メディアに関する情報(上記の「オファー/アンサー」および SDP で説明した内容)の交換に加え、ピア間はネットワーク接続に関する情報も交換しなければなりません。これは ICE 候補と呼ばれ、そのピアが利用できる通信メソッド(直接、または TURN サーバー経由)の詳細が記載されています。通常、それぞれのピアはまず最適な候補を提案し、その後、順次劣った候補へと移っていきます。理想的には、候補は UDP であるべきです(速度が速く、メディアストリームが中断から比較的容易に回復できるためですが)、ICE 標準では TCP 候補もすることができる。
メモ: 一般的に、TCP を使用する ICE 候補は、UDP が利用できない場合や、メディアストリーミングに適さないような制限がかけられている場合にのみ使用されます。ただし、すべてのブラウザーが TCP 上の ICE を対応していない場合があります。
ICE では、候補が TCP または UDP のいずれかを介して接続を表すことができるが、一般的には UDP が推奨される(また、より広く対応されている)。それぞれのプロトコルはいくつか種類の候補に対応しており、これらの候補の種類によって、データがピア間でどのように伝送されるかが定義される。
UDP の候補型
UDP 候補(protocol が udp に設定されている候補)は、以下のいずれかの型になります。
host-
ホスト候補は、その
ipアドレスが、リモートピアーの実際の直接 IP アドレスであるものを指します。 prflx-
ピア反射型候補とは、その IP アドレスが 2 つのピア間の対称 NAT を経由して決まる候補のことを指し、通常はトリクル ICE の過程で追加の候補として生成されます(つまり、プライマリシグナリングの後、接続検証フェーズが完了する前に発生する追加の候補交換です)。
srflx-
サーバー反射型候補は、STUN/TURN サーバーによって生成されます。接続の開始者は STUN サーバーに候補をリクエストすると、STUN サーバーはそのリクエストをリモートピアーの NAT を経由して転送します。NAT は、IP アドレスがリモートピアーのローカルアドレスである候補を作成して返します。その後、STUN サーバーは、IP アドレスがリモートピアーとは無関係な候補を、開始者のリクエストに対する応答として返します。
relay-
リレー候補は、サーバー反射型候補 (
"srflx") と同様に生成されますが、STUN の代わりに TURN が使用されます。
TCP の候補型
TCP 候補(つまり、protocol が tcp である候補)には、以下の型があります。
候補ペアの選択
ICE レイヤーは、2 つのピアのうち 1 つを 制御エージェント として選択します。これは、接続にどの候補ペアを使用するかを最終的に決定する ICE エージェントです。もう一方のピアは 被制御エージェント と呼ばれます。RTCIceCandidate.transport.role の値を確認することで、接続のどちら側がどちらの役割を担っているかを特定できますが、一般的にはどちらがどちらであっても問題はありません。
制御側エージェントは、どの候補ペアを使用するかという最終決定を行う責任を負うだけでなく、必要に応じて STUN および更新されたオファーを使用して、その選択を被制御側エージェントに通知する責任も負います。被制御側エージェントは、どの候補ペアを使用すべきか指示されるのを待つだけです。
単一の ICE セッションにおいて、制御側エージェントが複数の候補ペアを選択する場合があることを念頭に置いておくことが重要です。制御側エージェントが候補ペアを選択し、その情報を被制御側エージェントと共有するたびに、2 つのピアは、新しい候補ペアで記述された新しい設定を使用するように接続を再構成します。
ICE セッションが完了すると、ICE のリセットが発生しない限り、その時点で有効な設定が最終的なものとなります。
それぞれの候補の生成が終了すると、RTCIceCandidate という形で「候補終了」通知が送信されます。このオブジェクトの candidate プロパティは空文字列となります。この候補は、リモートピアーにその通知を配信するために、通常通り addIceCandidate() メソッドを使用してピア接続に追加する必要があります。
現在のネゴシエーションのやり取りにおいて、まったく候補が期待できない場合、RTCIceCandidate の candidate プロパティが null となるオブジェクトを配信することで、候補終了通知が行われます。このメッセージをリモートピアーに送信する必要_はない_。これは古い状態通知であり、代わりに iceGatheringState が complete に変更されるのを監視するか、icegatheringstatechange イベントを監視することで、この状態を検知することが可能です。
何かに失敗したとき
ネゴシエーションの過程では、どうしてもうまくいかない場合があります。例えば、ハードウェアやネットワーク構成の変更に対応するために接続の再ネゴシエーションを行う際、ネゴシエーションが行き詰まったり、何らかのエラーが発生してネゴシエーション自体がまったく行えなくなったりする可能性があります。また、権限の問題やその他の課題が生じる場合もあります。
ICE ロールバック
すでに確立されている接続の再ネゴシエーション中に、ネゴシエーションが失敗する状況が生じた場合、すでに実行中の呼び出しを強制終了させることは望ましくありません。何しろ、おそらくは単に接続のアップグレードやダウングレード、あるいは進行中のセッションに対するその他の調整を試みていただけでしょう。そのような状況で呼び出しを中止させるのは、過剰な対応となります。
その代わりに、ICE ロールバックを実行することが可能です。ロールバックを行うと、SDP オファー(ひいては接続設定)が、その接続の signalingState が最後に stable だった時点の設定に復元されます。
プログラムでロールバックを開始するには、type が rollback である記述を送信します。記述オブジェクト内のその他のプロパティは無視されます。
さらに、ICE エージェントは、以前にオファーを作成したピアがリモートピアーからオファーを受信すると、自動的にロールバックを開始します。言い換えれば、ローカルピアが have-local-offer 状態(ローカルピアが以前にオファーを送信したことを示す)にある場合、受信したオファーを使用して setRemoteDescription() を呼び出すと、ロールバックがトリガーされ、ネゴシエーションの呼び出し側がリモートピアーからローカルピアへと切り替わります。
ICE 再起動
ICE 再起動のプロセスについて学びましょう。
複雑な図表にまとめられたやり取りの全容
