この記事は、 NTT Communications Advent Calendar 2023 4日目の記事です。
この記事では、Web標準の仕様と実際のブラウザの挙動についての体験談を紹介します。
W3C(World Wide Web Consortium)はWeb StandardsというWebの標準仕様を制定しています。
この中でブラウザのWeb APIの挙動についても定義されています。
挙動が統一されていないなら別ですが、長く使われ標準化もされている技術において、すべてのモダンブラウザ1で挙動が同じ場合、それが仕様化された動作だと思うでしょう。
しかし、実際にはすべてのブラウザが同じ仕様違反をしているという例をWebRTC2で用いられるRTCPeerConnectionを用いて説明します。
SDP3に手を入れているような開発者の方には特に興味深いかもしれません。
目次
はじめに
こんにちは、イノベーションセンター テクノロジー部門の池田です。
普段はSkyWayに関連してWebRTCやその次世代となる技術の調査や検証をしています。
この記事では、Web標準の仕様と実際の全モダンブラウザの挙動の違いについて、気付いた経緯や歴史的経緯などを紹介します。
経緯
気になったタイミングは読書会の中でWebRTC 1.0 APIの仕様を読んでいて、その中でも4.4.2 Interface Definitionを読んでいる時でした。
そこで何度か読み直しても自身の知っているブラウザの挙動と仕様の手順が異なっていることに気づきました。
具体的には以下のようなコードの場合です。
const pc = new RTCPeerConnection(); const offer = await pc.createOffer(); offer.sdp = offer.sdp.replace('<対象の文字列>', '<置換後の文字列>'); // sdpの更新 await pc.setLocalDescription(offer); // 仕様上InvalidModificationErrorになるはず
上のコードは3行目を除けばWebRTCを使う場合にブラウザで実行される一般的なコードです。4
3行目の操作はSDPの文字列を書き換える処理5で、必要に応じて置換する文字列を変えます。
上のコードでの置換には意味はないですが、実際のアプリケーションで操作する際はAPIで制御できないようなところまで変更を加えたい際にこの手法が利用されます。
例えば以前書いたLyraの利用の記事でもLyraを使うための下準備としてSDPを直接変更しています。
WebRTCとSDPの補足
WebRTCについて知っている方は読み飛ばしてください。
ブラウザでWebRTCを用いた映像などのデータを送受信可能にするには事前にそれを可能にする情報を交換しておく必要があります。
SDPはこの情報交換のために利用されます。
各ブラウザはJavaScriptのAPIを使うことで自身の伝えるべき情報をSDPとして準備し、情報交換に備えます。
ここに更なるカスタマイズをしたい場合には、上のコードのようにSDP Mungingをしたり、別のAPIで変更をしたりする必要があります。
差分と仕様上の問題点
上のコードは何が問題なのでしょうか?
2行目にあるcreateOfferのfinal steps to create an offerのstep5には以下のようにあります。
Set the [[LastCreatedOffer]] internal slot to sdpString.
これは[[LastCreatedOffer]]にこの関数で生成されたSDPが格納されることを意味します。
そして、4行目にあるsetLocalDescription(以下sLD)のstep 4.2は以下のようにあります。
If type is "offer", and sdp is not the empty string and not equal to connection.[[LastCreatedOffer]], then return a promise rejected with a newly created InvalidModificationError and abort these steps.
これは引数と[[LastCreatedOffer]]が異なる場合にRejectすることを指示しています。
しかし、前述のとおりこのコードは引数として異なる値を渡しているにも関わらず、全モダンブラウザでRejectされることなく動きます。
つまり、ブラウザの挙動と仕様が全く違います。
あまりにもその差が謎だったため、仕様を管理しているリポジトリにissueを作成しました。
ブラウザの仕様違反
issue作成1時間程でいくつかコメントをいただきました。 その結果、仕様の読み取り方は正しく、ブラウザ側が揃って仕様違反であり、許可されていない操作であることが分かりました。 これは、以前は許可されていたが歴史的経緯によって禁止されたとのことでした。
歴史的経緯
昔はWebRTCのAPIがあまり存在せず、変更を加えるにはSDPを修正する必要がありました。
SDPは何かしらのオブジェクトではなくただの文字列として記述されているため、
特定部分を変更するのが難しく、書き換える際も意図していない部分を書き換えないように気をつける必要があります。
また、行いたい変更をSDPの記法で書き下す必要があり、API以外にSDPのドメイン知識も求められることがより難易度を上げています。
例えばsetCodecPreferences()というAPIの利用例の1つとして特定のコーデックのみを利用したい場合があります。これをAPIを用いずに手動で変更するには、SDPの文字列から該当する部分を特定し、必要な部分だけを残すという処理を文字列の置換で行う必要があります。
その後WebRTCが発展すると、MediaStreamTrackやRTCRtpTransceiver単位での操作をするAPIが生まれ、より細かい修正をAPIでできるようになってきました。
APIの一例と先ほど出てきたsetCodecPreferences()が挙げられています。
API経由での操作だとSDPを扱わなくてもやりたいことができるようになるため、よりWebRTCの開発が容易になると思います。
このようにAPIで操作する/できるようになったため、仕様上ではSDPの直接の変更が禁止されたのだと思います。
しかし、上に書いたLyraのケースのように、APIでは設定できない事項もまだ存在します。
そのため、WebRTC 1.0 APIを拡張する拡張ユースケースなどがAPIをさらに充実させ、APIの範囲を広げることが必要と感じました。
一方、今回紹介したsetCodecPreferences()ですら下図のように現在はFirefoxで利用できないため実際にAPIのみですべてが完了する世界はまだ遠いと感じます。
このような状態でSDPの変更が禁止されるとできることが制限されてしまうため、APIを補うために仕様上禁止されていても実際には変更を許容するのは仕方ないのかなと思います。
余談: 実際の仕様変更の過程について
実際にいつ頃にSDPの変更が禁止されたのかをGitHubにあるリポジトリのコミットを追って調査しました。
細かい文言や章編成の変更コミットなどがあり、変更を追うのが大変でしたが、
変更が提案されたのは2016/11のissueで、
実際に変更されたのは2017/02のPRでした。
この変更はIETF97のスライド内のOption Dによるものらしいです。
この中でsetLocalDescription()の引数は後方互換性のためとありますが、結果的には変更したSDPを適用するために未だに使われています。
まとめ
本記事ではWeb標準の仕様と実際のブラウザの挙動についての体験談を紹介しました。 ブラウザの統一された挙動が仕様通りとは限らないということが伝わったのではないかと思います。 逆に仕様を完全に理解しても、実際のブラウザの挙動が想定できるとは限らないのは辛い点だと感じました。
明日もお楽しみに。