console.lealog();

@leader22のWeb系に関する勉強めもブログですのだ

“Unified Plan” Transition Guide (JavaScript) の日本語訳

“Unified Plan” Transition Guide (JavaScript) - Google ドキュメント

なんかそのうち仕事で見返しそうな感じがあったので訳してみた。

背景

WebRTCの仕様はここ数年で大きく変化してきた。`addStream()`などのStream系のAPIから、`addTrack()`などのTrack系のAPIへの移行がその一つ。これによって、再ネゴシエーションなしに設定変更が可能になった。

ただWebRTC 1.0へ向けてはまだ大きな変更が残ってて、それが今から話すSDPのフォーマット変更。

仕様の上で「Unified Plan」と呼んでるSDPのフォーマットがある。これは今のChromeが利用している「Plan B」というフォーマットとは別のもの。このフォーマットの移行は、たくさんのアプリケーションに破壊的な変更をもたらす可能性がある。

そのほか、この「Unified Plan」に沿って追加される新しいAPIもある。(`Transceiver`関連)

この記事は、そんなフォーマット移行の助けとなるべく書かれている。

Firefoxは既に「Unified Plan」をサポートしていて、他のブラウザも追ってそれに追従するだろう。

  • 複数のaudio/videoトラックを扱っている
  • ローカル・リモート問わず、トラックのIDが変わらないと仮定している
  • SDPを手動で修正している

あなたのアプリケーションがこれらに当てはまる場合は、注意が必要。

どちらのフォーマットを使うか

アプリケーションで`sdpSemantics`を指定する

`RTCPeerConnection`のコンストラクタに`sdpSemantics`オプションを指定することで、どちらのSDPフォーマットを使うか選ぶことができる。

new RTCPeerConnection({ sdpSemantics: 'unified-plan' });
new RTCPeerConnection({ sdpSemantics: 'plan-b' });

ただしこれはChromeでのみ有効で、他のブラウザでは無視される。

実装が適切に両方のフォーマットに対応していないのであれば、Chromeの仕様変更に備える意味でも、どちらかを指定しておくことを推奨する。

`chrome://flags`によるデフォルト設定

ChromeのM71から`chrome://flags`にて、「WebRTC: Use Unified Plan SDP Semantics by default」というフラグが指定できるようになる。

このフラグを有効にするか、`--enable-features=RTCUnified Plan ByDefault`つきでChromeを起動することでも有効にできる。

これらの指定がない場合も、デフォルトで「Unified Plan」が使われる。

ちなみに、M71未満のChromeの場合は、`--enable-blink-features=RTCUnified Plan ByDefault`というフラグを指定することでも有効にできる。

これは、`sdpSemantics`をコード内で指定してない場合に、その対応をしないとどうなるのかを調べたい場合に使える。

フォーマットの判別

ChromeのM69から`Transceiver`関連のAPIが追加されたが、これらは「Unified Plan」の時のみサポートされる。

「とりあえず`addTransceiver()`を呼んでみて、例外を投げないかどうか」をチェックするのが、「Unified Plan」かどうかをチェックする方法のひとつ。

M70だと、`getConfiguration()`を呼ぶことで正式に`sdpSemantics`の値を知ることができる。(`chrome://webrtc-internals`と同じように)

このあたりのコードスニペットはこちら。

Feature Detection

他のやり方としては、SDPをパースして`m=`行を調べることでもできる。

Unified PlanとPlan Bの違い

SDP上の違い

「Plan B」では、audio/videoごとに、1つのSDPの`m=`セクションがあり、`mid`もそれぞれ割り当てられていた。
なので複数のトラックがオファーに含まれていた場合、複数の`a=ssrc`行がそれぞれの`m=`セクションに含まれることになる。

「Unified Plan」では、1つの`m=`セクションが1つのトラックの送(受)信に割り当てられる。
なので、複数のトラックが存在する場合はその分だけ`m=`セクションが作られる。

これがSDPのフォーマットに互換性がない原因で、「Unified Plan」のピアは「Plan B」のフォーマットのオファーが来た場合、「Failed to set remote answer sdp: Media section has more than one track specified with a=ssrc lines which is not supported with Unified Plan.」といって拒否する必要がある。

また「Plan B」側のピアは、「Unified Plan」のオファーに含まれる最初の`m=`セクションだけを解釈して受け入れてもよい。

以下は2つのaudioトラックを送る場合の「Plan B」と「Unified Plan」それぞれのSDPのサンプル。

説明に必要な部分のみ抜粋してあり、`mid`や`ssrc`は簡略化してある。

Plan B
...
a=group:BUNDLE audio
a=msid-semantic: WMS stream-id-2 stream-id-1
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
...
a=mid:audio
...
a=rtpmap:103 ISAC/16000
...
a=ssrc:10 cname:cname
a=ssrc:10 msid:stream-id-1 track-id-1
a=ssrc:10 mslabel:stream-id-1
a=ssrc:10 label:track-id-1
a=ssrc:11 cname:cname
a=ssrc:11 msid:stream-id-2 track-id-2
a=ssrc:11 mslabel:stream-id-2
a=ssrc:11 label:track-id-2

`a=mid:audio`が使われ、トラックは同じ`m=audio`セクションに含まれる。

Unified Plan
...
a=group:BUNDLE 0 1
a=msid-semantic: WMS
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
...
a=mid:0
...
a=sendrecv
a=msid:- <track-id-1>
...
a=rtpmap:103 ISAC/16000
...
a=ssrc:10 cname:cname
a=ssrc:10 msid: track-id-1
a=ssrc:10 mslabel:
a=ssrc:10 label:track-id-1
m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
...
a=mid:1
...
a=sendrecv
a=msid:- track-id-2
...
a=rtpmap:103 ISAC/16000
...
a=ssrc:11 cname:cname
a=ssrc:11 msid: track-id-2
a=ssrc:11 mslabel:
a=ssrc:11 label:track-id-2

`a=mid:0`と`a=mid:1`がそれぞれのトラックを表し、`a=audio`と`a=rtpmap`行は、`m=`行の中で複数回でてくる。

ちなみにこのSDPを生成したコードでは、`addTrack()`の第二引数に`stream`を渡していない。

なので「Unified Plan」の`a=msid`行は`-`となってしまうが、「Plan B」だとトラックごとにランダムな文字列が生成される。引数で`stream`を渡すと、それぞれ`msid`が記載されるようになる。

Chromeの「Unified Plan」は、互換性のために`msid`、`mslabel`、`label`の行を`a=ssrc`行で追加するが、これは「Unified Plan」本来の仕様にはない。

単一のaudioとvideoを送るだけなら、異なるSDPフォーマットを使っていても問題ない。

RTCRtpTransceiver APIと挙動の変更

`Transceiver`はSDPにおける`m=`セクションを表すもの。
これは1つの`m=`セクションが、送信と受信の両方を表すからである。

メディアの流れる方向(`direction`)は、4パターンある。

  • inactive
  • sendonly
  • recvonly
  • sendrecv

`Transceiver`の`Sender`に各トラックが紐付けられ、`replaceTrack()`によって置き換えられたりすることもあれば、そもそもトラックを持たない場合もある。
`addTrack()`か`addTransceiver()`によって、トラックを`Sender`および`Transceiver`に追加できる。

`Receiver`は常に作られるが、そのトラックはデフォルトでは`muted`になっている。

`setLocalDescription()`と`setRemoteDescription()`によって、`Transceiver`の`mid`とSDPの`m=`セクションのそれが紐付けられる。

リモートのSDPを適用するということは、`Transceiver`を作る・更新することにつながり、受信可能なトラックごとに`RTCPeerConnection.ontrack`のイベントが発火することになる。

問題は受信側のトラックで、その`addTrack()`の時点で作られたものが再利用されたかもしれないこと。

そのトラックは`unmute`されるかもしれないが、もし後続の`setRemoteDescription()`で受信しないようにした場合、再び`muted`になる。

トラックのIDはローカルとリモートで一致しない

「Plan B」では、`Sender`がローカルのトラックごとに作られ、それを受けたリモートでもトラックごとに`Receiver`が作られていたので、それらのIDはローカルとリモートで一致していた。

しかし「Unified Plan」では、`Sender`と`Receiver`は`Transceiver`がペアで用意するもので、`transceiver.receiver.track`は、リモートのSDPが来るより先に用意される可能性がある。

なので、`RTCPeerConnection.ontrack`で得られるトラックが、リモートのトラックのIDと同一のIDを持つことは保証されない。そのうえ、`addTransceiver()`と`replaceTrack()`により、そのトラックは複数回送信されるかもしれない。

その用途には、トラックのIDではなく`transceiver.mid`を見るようにすればよい。

リモートのトラックが削除されることはない

「Plan B」では、`RTCPeerConnection.removeTrack()`すると、`Sender`のトラックが削除され、それに応じて接続先のピアでも対応する`Receiver`とトラックが削除されていた。

「Unified Plan」では、送信側が`removeTrack()`によって`Transceiver`の`direction`を変更し、`sender.track`を`null`にする。接続先のピアは、ネゴシエーションによって`Transceiver`の`direction`を変更し、削除する代わりに`muted`にする。なぜならそのトラックは将来的に再利用されるかもしれないから。

この様子は以下のスニペットで確認できる。

Track IDs: Unified Plan vs Plan B

MediaStreamTrack.onended はもはや発火しない

リモートのトラックは、削除される代わりに`muted`にされることになったので、`onended`は発火しない。

その役目は`MediaStreamTrack.onmute`によって代替できる。(`MediaStream.onremovetrack`は引き続き発火する)

確認用のコードスニペットは以下。

Remote track & stream events

レガシーなAPI

`RTCPeerConnection`の`addStream()`と`removeStream()`は古いAPIになる。

後方互換性のため、これらは`addTrack()`と`removeTrack()`を使ってshimされるが、「Unified Plan」では標準のTrack系のAPIの利用を推奨する。

`addstream`イベントや`removestream`イベントも同様にM71までサポートされるが、いずれも新しい`ontrack`、`onmute`イベントや、`MediaStream`の`onaddtrack`、`onremovetrack`イベントの利用を推奨する。

Transceiverのdirection, currentDirection

`Transceiver`は双方向の通信を想定している。

よって作成時、初期値としての`direction`は`sendrecv`で、`currentDirection`は`null`になる。
`direction`はネゴシエーションにおける意思であり、`currentDirection`は現状を表す。

アンサー側がメディアを送信したい場合、オファーに`sendrecv`な`Transceiver`が含まれていれば、その`Transceiver`を使ってメディアを送信できる。
こうすることで、オファー側にアンサーが返るより早くRTPパケットの送信が可能になる。

「Plan B」だと、アンサーが届くまでは`Receiver`を用意できなかったので、これができなかった。

このスニペットでは、オファーアンサーの過程における`Transceiver`の様子がわかる。

Transceivers and events example

受信のみのオファー

アンサー側は、あらたな`m=`セクションをSDPに追加することは許されない。

これは、オファー側が何もメディアを送らない場合、`sendrecv`なセクションは存在せず、アンサー側もその`Transceiver`を使ってメディアを送信できないことを意味する。

その場合、アンサー側での再ネゴシエーションが必要となり、接続確立まで余計な時間がかかる。

ここでは受信のみのオファーを出す方法と、それによって初回のオファーアンサーでより早くメディアを受け取ることを可能にするやり方について述べる。

「Plan B」では、`createOffer()`に古いオプションを渡すことで、任意のトラックを受け取る用の`m=`セクションを作ることができた。

「Unified Plan」では、audio/videoごとに1トラック限定で、`Transceiver`を用意する挙動になる。ただし仕様としては、`addTransceiver()`の利用を推奨する。

Chromeだけが限定的にこのオプションをサポートしていて、オファーはできるがそれ用の`Transceiver`は即座に確認できない。

「Unified Plan」では、`addTransceiver()`を使うことで、任意の`recvonly`な`Transceiver`を用意して使うことができる。

詳しくは以下のスニペットを参照。

Offer To Receive Media

訳してみて

Chromeのおかげ?Chromeのせい?って感じに移行を迫られるわけですが、まぁいよいよかーって感じですね。

ちなみに、この「Unified Plan」がStableのChromeに入るのはM72からで、M72は2019年の1/29あたりのリリースらしいです。

Chromium Development Calendar and Release Info - The Chromium Projects

ここから一気にネット上の記事やらチュートリアルがアテにならなくなるので、皆さまご注意を〜。