🧊

WebRTCで帯域を指定する方法2つ

またも、まったく目新しいトピックではないです。

個人でメンテしてるWebRTCのモジュールに機能を追加するときに思い出したしせっかくなのでメモ。

方法は2つ

  • SDPによる`b=AS`/`b=TIAS`の指定
  • `RTCRtpSender`の`getParameters()` + `setParameters()`

短いWebRTCの歴史の中でも、前者は割と昔から使えて、後者は割と新しいめのAPIを使うとできる。

TPOにあわせて選べばよくて、どちらかだけ使え!というものではないです。

SDPによる指定

いわゆるSDP mungingといって、SDPを手動で書き換える必要がある。

普通にブラウザやらが提供するAPIを触ってるだけならそんな機会は不要なはずですが、こればっかりはAPIが用意されてないのでやるしかない。

RFC 3556 - Session Description Protocol (SDP) Bandwidth Modifiers for RTP Control Protocol (RTCP) Bandwidth
RFC 3890 - A Transport Independent Bandwidth Modifier for the Session Description Protocol (SDP)

書き足したくなるであろう属性は主に2つ。

  • `b=AS:1000`
  • `b=TIAS:1000`

`AS`は、トランスポート全体の帯域で、単位は`kbps`。
`AS`は`Application Specific Maximum`の略らしい。(Mどこいったんや)

`TIAS`は、メディアだけの帯域で、単位は`bps`。
`TI`は`Transport Independent`の略で、`TIAS`のほうが`AS`より上位互換って感じ。

なんで2つもあるかというと、Chromeでは`AS`しか指定できず、Firefoxでは`TIAS`しか効かないなどの都合があるから。
ただJSEP的にも、`AS`しかなかったらよしなに計算して`TIAS`と解釈しましょうねみたいな記述もあったりして、我々はブラウザさまの意のままに・・・。

SDP的には、`b=`では他にも`RS`やら`RR`やらでRTCPの帯域を指定できるらしい。

ちなみに対象となるのはオファーを出した当人 = 自身が受け取るもののみ。
つまり、この帯域に絞って送ってくれ!っていうためのもの。

自分も帯域を絞って送信したい場合は、アンサーでも同じようにSDPを修正してもらう必要がある。

これは淡い記憶による蛇足ですが、この属性はメディアレベルに書けるやつであるものの、実際はトランスポート単位の指定になってた気がしてて、基本的にBUNDLE(ポート多重化)される昨今だと、特定の映像だけ帯域を抑えたいみたいなことができなかった気がする。

なのでざっくりP2P間で流れる帯域をまとめて指定したいならともかく、それ以外のケースでは少し使いにくい・・。

まぁそんなとき(ってかだいたいそんなときなんやけど)に使えるのが次に紹介するAPI

RTCRtpSenderで指定

送信してるメディアについての話。

既にネゴシエーションが済んでてメディアが送信できてる場合、そこには`RTCRtpSender`がいるはず。
映像なら`video`の、音声なら`audio`の`RTCRtpSender`がいるはず。

const [sender] = pc.getSenders();
// or
const [{ sender }] = pc.getTransceivers();

で、これに生えてるAPIから、どんな具合にメディアを送ってるのかが取れる。
このパラメータを修正して、セットすることで帯域の指定ができる。

const params = sender.getParameters();
params.encodings[0].maxBitrate = 1 * 1000; // bps

await sender.setParameters(params);

これで、特定の映像だけ調整できるし、SDPは変更しないので再ネゴシエーションも不要!万事解決!

というわけで、ほとんどの需要に応えるにはこっちを使えばおっけー。

おまけ: 1

今回もあります落とし穴。
1つは、APIの仕様としてトランザクション的なものを意識しないといけない点。

端的に書くとコレがダメ。

const params = sender.getParameters();
params.encodings[0].maxBitrate = 1 * 1000; // bps

// 何かの拍子にgetしちゃうと
sender.getParameters();

// この最初のparamsの賞味期限が切れて怒られる
await sender.setParameters(params);

なので`getParameters()`と`setParameters()`は必ず連続的に呼ぶ必要がある。

おまけ: 2

ブラウザによって`getParameters()`で返ってくる値が違う(ことがある)。
ことがあるっていうのは、このパラメータを指定するチャンスは、最初にメディアを送信するときにもできるから。

端的に書くと、単に`addTransceiver(track)`しただけのとき、Firefoxは空のオブジェクトを返してくる。

なので、

// params = {};
params.encodings[0].maxBitrate = 1 * 1000; // encodingsはundefinedなのでSyntaxエラー

っていうことにつまづかないようにコードを書く必要がある。

const params = sender.getParameters();
if (!params.encodings)
  params.encodings = [{}];

params.encodings[0].maxBitrate = 1 * 1000;
await sender.setParameters(params);

みたいに。

おまけ: 3

そういうことなら、こんなコードで統一すれば全ブラウザいけるのでは?って思ったそこのあなた向け。

const params = sender.getParameters();
params.encodings = [{ maxBitrate: 1 * 1000 }];
await sender.setParameters(params);

Chromeはそれを許しません!

というのも、Chromeの`getParameters()`は多分に情報の詰まったオブジェクトを返すので、このコードだと既存のオブジェクトに入ってたはずの、ReadOnlyなプロパティを消して`setParameters()`することになってしまって怒られる。

なので、

  • 既存のプロパティは尊重して
  • プロパティがなかったときだけ新規作成する

おまけ:2のコードにしないと、クロスブラウザで動作しないです。