🍃このブログは移転しました。
3秒後、自動的に移動します・・・。

addTransceiver() と addTrack() の使い分け

`addStream()`は死にました。
5バージョンくらい前のChromeをサポートしたいとか理由がない限り、忘れてしまってよいです。

他人のコードを読んでてコレが出てきたら、メンテされてない or 古いバージョンをサポートしようとしてるの2択です。

これからのWebRTCでメディアを送りたい場合は、`addTrack()`か、`addTransceiver()`を使います。

addTrack()

一番よくあるであろう、`getUserMedia()`で取得したストリームをセットする場合のコードはこう。

(async function() {
  // sdpSemantics for Chrome M72未満
  const pc = new RTCPeerConnection({ sdpSemantics: 'unified-plan' });

  const stream = await navigator.mediaDevieces.getUserMedia({ video: true, audio: true });
  stream.getTracks().forEach(track => pc.addTrack(track, stream));
}());

第2引数でその`MediaStreamTrack`が属する`MediaStream`を渡すのを忘れずに。
忘れるとFirefoxでエラーになります。

ほとんどのユースケースなら、オファー側もアンサー側もこれだけ使ってれば問題ないはず。

では`addTransceiver()`なんてAPIはいつ使うのか?

addTransceiver()

pc.addTrack(track, stream);
pc.addTransceiver(track, { streams: [stream] });

実はこれ、ここだけ見ると同義のコードで、発行されるオファーSDPも同一です。
`addTransceiver()`の第2引数の渡し方がちょっと特殊なだけ。

ちなみにこっちを使う場合は、`streams`の指定を忘れてもFirefoxでエラーにならない。

つまり`addTrack()`は内部的に`addTransceiver()`してると、基本的には考えてしまっておっけー。

ただし注意すべき点があって、それに迫るのがこの記事の本旨です。

受信のみ・送信のみモードのオファー

`addTransceiver()`だからこそできることがあって、それが受信のみ・送信のみモードを実現したい場合。

だいたいのWebRTCのユースケースは送受信(`sendrecv`)だが、1:N配信などの場合にはそうしたくない。
受信のみとか送信のみとかがそれで、`recvonly`とか`sendonly`とかいう単語が出てくるやつ。

そこで、こういうコードを書くことになります。

// 送信のみモードでオファー
(async function() {
  // sdpSemantics for Chrome M72未満
  const pc = new RTCPeerConnection({ sdpSemantics: 'unified-plan' });

  const stream = await navigator.mediaDevieces.getUserMedia({ video: true, audio: true });
  stream.getTracks().forEach(track => pc.addTransceiver(track, {
    streams: [ stream ],
    direction: 'sendonly'
  }));

  await pc.createOffer().then(offer => pc.setLocalDescription(offer));

  // ...
}());

// 受信のみモードでオファー
(async function() {
  // sdpSemantics for Chrome M72未満
  const pc = new RTCPeerConnection({ sdpSemantics: 'unified-plan' });

  pc.addTransceiver('video', { direction: 'recvonly' });
  pc.addTransceiver('audio', { direction: 'recvonly' });

  await pc.createOffer().then(offer => pc.setLocalDescription(offer));

  // ...
}());

この書き方・挙動を実現するためには、`addTrack()`ではなく`addTransceiver()`を使うしかないというわけ。

受信のみ・送信のみモードのアンサー

さっきのはオファー側で、今度はアンサー側の話。

1:1の場合、受信のみ・送信のみのユースケースの組み合わせは以下の通り。
Oがオファー側、Aがアンサー側。

  • O: 送信のみ x A: 受信のみ
  • O: 受信のみ x A: 送信のみ

送信のみどうし・受信のみどうしのケースは、実際にはやる意味がない・・というか、やってみると`inactive`として扱われるので割愛。

受信のみのアンサー

// 受信のみモードでアンサー
(async function() {
  // sdpSemantics for Chrome M72未満
  const pc = new RTCPeerConnection({ sdpSemantics: 'unified-plan' });

  // ...

  await pc.setRemoteDescription(offer);
  await pc.createAnswer().then(answer => pc.setLocalDescription(answer));

  // ...
}());

受信のみで何も送らないのでもちろん`addTrack()`は不要。
そして書き忘れたとかではなく、`addTransceiver()`も不要です。

というのも、`RTCRtpTransceiver`が作られるタイミングがこの3つ。

  • `addTrack()`したとき
    • 後述しますが少し条件がある
  • `addTransceiver()`したとき
  • `setRemoteDescription()`したとき

このケースだと最後の`setRemoteDescription()`のタイミングで自動的に作られるから不要というわけ。
そして、その際に作られる`RTCRtpTransceiver`は`recvonly` = 受信のみモード。

むしろ自分で用意してしまうと、思ったように動かずなんで?ってなるはずです。(より正確には、自分で用意したやつは使われずに余ってるはず)

送信のみのアンサー

`setRemoteDescription()`で`RTCRtpTransceiver`が作られるという前提で読んでください。

// 送信のみモードでアンサー
(async function() {
  // sdpSemantics for Chrome M72未満
  const pc = new RTCPeerConnection({ sdpSemantics: 'unified-plan' });

  // ...

  const stream = await navigator.mediaDevieces.getUserMedia({ video: true, audio: true });

  await pc.setRemoteDescription(offer);

  pc.getTransceivers().forEach(transceiver => {
    const { kind } = transceiver.receiver.track;
    const [ track ] = kind === 'video' ? stream.getVideoTracks() : stream.getAudioTracks();
    transceiver.sender.replaceTrack(track);
    transceiver.direction = 'sendonly';
  });

  await pc.createAnswer().then(answer => pc.setLocalDescription(answer));

  // ...
}());

`setRemoteDescription()`で作られた`recvonly`な`RTCRtpTransceiver`を、送信のみ用に使い回すコードです。
なんで突然こんな複雑なコードに・・って感じですが、そういう仕様です。

`setRemoteDescription()`の前に、`sendonly`で`addTransceiver()`すればOKと思いがちですが、そうではないです。

その場合、送ろうと思っていた`MediaStreamTrack`は、SDPに紐付かない`RTCRtpTransceiver`として残ります。
そして、`setRemoteDescription()`で作成された`RTCRtpTransceiver`を使って、受信のみモードでP2Pがはじまります。

うーん、罠っぽい!けど、仕様としては正しい・・。
まぁご安心を。送信のみではなく、送受信モードで別にいい場合は、`addTrack()`が使えます。

// 送受信モードでアンサー
(async function() {
  // sdpSemantics for Chrome M72未満
  const pc = new RTCPeerConnection({ sdpSemantics: 'unified-plan' });

  // ...

  const stream = await navigator.mediaDevieces.getUserMedia({ video: true, audio: true });
  stream.getTracks().forEach(track => pc.addTrack(track, stream));

  await pc.setRemoteDescription(offer);
  await pc.createAnswer().then(answer => pc.setLocalDescription(answer));

  // ...
}());

`addTrack()`した上で`setRemoteDescription()`したら、余分な`RTCRtpTransceiver`が作られるのでは?と思ったあなたは勘がいいです。

が、実はこの`addTrack()`、「それ用の`RTCRtpTransceiver`が存在しなければ新規作成し、既に再利用可能な`RTCRtpTransceiver`があればそれを使い回す」ので、余分なやつは作られません。

ちなみに、`setRemoteDescription()`と`addTrack()`の呼び出し順も関係なく、いい感じになります。

もうひとつちなむと、送受信モードでアンサーしたところで意味がないです。
オファー側が受信のみモードのため、アンサー側としても受信するものがなく、結果的には送信のみモードでアンサーと同じ挙動になります。
(`RTCRtpTransceiver`の`direction`が`sendrecv`なままになるけど、実情を表す`currentDirection`が`sendonly`になる)

上の方で少し書いた、`sendonly`なオファーに対して`sendonly`でアンサーしようとすると`inactive`になるのと同じように、アンサーはあくまでオファーに対するアンサーなので、オファーを鑑みていい感じになる。

まとめ

Exploring RTCRtpTransceiver. - Advancing WebRTC

「addTrack (even addStream) is just a tweaked version of addTransceiver these days.」って言われてる通り、

  • `addTrack()`がよしなにやってくれるAPI
    • ただし細かい指定はできない
  • `addTransceiver()`は事細かに挙動をコントロールしたい場合に使うAPI
    • ただし`RTCRtpTransceiver`のことを知らないとハマる

という話でした。

今回はアンサー側を厚くあれこれ書きましたが、もちろんオファー側でも、`addTrack()`はよしなにやってくれます。
(`inactive`な`RTCRtpTransceiver`は使い回さないようになってたり)

まあ結局は、

  • 最終的なSDPに何が載るか
  • 適切な`RTCRtpTransceiver`とDOMにある`MediaStream(Track)`がちゃんと紐付いてるか

だけが全てなので、そこに至るまでのAPI呼び出しはなんでもいいです。

`addTrack()`だけを使って無駄なく`RTCRtpTransceiver`を作って、あとは`direction`を手動で修正するとか。
コードの統一感のために、`addTrack()`も`ontrack`も使わないとか。

まぁこんな細かいAPIの使い方はだいたいSDKがいい感じにしてくれてるはずで、だいたいのWebRTC使うぜ!って人には関係ないんですけど・・。