`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`になるのと同じように、アンサーはあくまでオファーに対するアンサーなので、オファーを鑑みていい感じになる。
まとめ
「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使うぜ!って人には関係ないんですけど・・。