`mediasoup`を使ったサービスを開発する上での役立ちTipsをメモっておきたいなと思ってたので。
(なんか思いついたらまた追記します。)
TransportListenIp
https://mediasoup.org/documentation/v3/mediasoup/api/#TransportListenIp
`Router`の`createWebRtcTransport()`に渡す必須の引数に、`listenIps`というものがあります。
その名の通り、サーバー側でのWebRTC接続をどこでリスンするかという設定。
const transport = await router.createWebRtcTransport({ listenIps : [ { ip: "127.0.0.1", announcedIp: "88.12.10.41" } ], });
みたいな感じで指定する。
`ip`が必須の値で、`announcedIp`が指定されるとそっちがSDPに載る。
ユーザーから見てサーバーを特定できるIPをSDPに載せる必要があるので、実際にサーバーで動かす場合は必須の値。
環境変数で渡すなり、起動時に取得するなり、どうにかして自身のIPを知る処理が必要になる。
ローカルで開発する分には、`ip: 127.0.0.1`だけ指定すれば一挙両得な感じなので、個人的にはこういう指定にすることが多いです。
const transport = await router.createWebRtcTransport({ listenIps : [ { ip: "127.0.0.1" }, { ip: "0.0.0.0", announcedIp: process.env.PUBLIC_IP } ], });
複数の`candidate`がSDPに載ることになるけど、到達できないやつは無視されるだけなので。
コーデック
サーバー側で`Router`を作るときに指定する`mediaCodecs`という指定がすべて。
そこのリストにないものは、クライアント側で対応してても使えない。
で、そこで指定できるものの全欄はというと、実はコードで書かれてる。
mediasoup/supportedRtpCapabilities.ts at v3 · versatica/mediasoup · GitHub
なのでこの全欄から、いくつか推しコーデックを選んで使う or 決め打ちで使うことになる。
決め打ちの場合はそれをクライアント側でも強制できるけど、そうでない場合で、より優先したいコーデックがあったらどうするか。
クライアント側で`Producer`をつくるときに、`codec`オプションを指定する。
const producer = await sendTransport.produce({ track, codec : device.rtpCapabilities.codecs .find(codec => codec.mimeType.toLowerCase() === "video/h264") });
これができるようになったのは最近の`3.6.2`から。
STUN/TURN
もちろん使えます。
サーバー側で指定するもよし、クライアント側で指定するもよし。
APIとしては、`mediasoup-client`側で`Transport`を作るときに渡すようになってる。
// サーバー側でWebRtcTransportを作って、そのプロパティを返す const transportOptions = await signaling.createTransport(); // createRecvTransport()も同様 const transport = device.createSendTransport({ ...transportOptions, iceServers, iceTransportPolicy });
なので、予め指定の設定をサーバー側から返すときに渡してしまってもいいし、クライアント側で指定してもよい。
クライアント側で動的にアップデートしたい場合、TURNのクレデンシャルを更新したい場合はこう。
transport.updateIceServers({ iceServers }); // 必要ならICEをリスタート
ただしFirefoxは、現時点でも`RTCPeerConnection#setConfiguration()`が実装されてないので使えない!
クライアントの切断検知
サーバー側で、クライアント側が切断したいことを検知したい場合。
切断されたならリソース解放とかしたいし、ユースケースとしてはよくわかる。
ただ結論から書くと、`mediasoup`単体だとコレ!っていう解決策はありません。
WebSocketを貼るなり、ping/pongするREST APIを作るなりするのが王道。
少しむずかしい解説すると、`mediasoup`の実装はICE-Liteなので、サーバー側からクライアント側に対して、その経路の死活監視はしてない。
`webRtcTransport.on("icestatechange")`で状態変更は追えるけど、ちょっとネットワークが揺れて寸断したかの区別がつかない。
`webRtcTransport.on("dtlsstatechange")`なら追えるか?と思ったけど、Chromeにバグ(仕様ともいえる)があってダメだった。
689017 - chromium - An open-source project to help move the web forward. - Monorail
これと似て非なるものかもしれないけど、ページ遷移は拾えるけど、タブ・ブラウザを閉じてしまうとアラートを投げる暇なくプロセスが終了してしまって、サーバー側で検知できない。
まあいつパケロスしてもおかしくないUDPランドで、こういうことするなという話ですね。
スケーラビリティ
過去にドキュメントの概要を記事にしてるので、まずはそちらを。
`mediasoup`を使うときは、あわせてシグナリング用のWebSocketサーバーやWebサーバーを用意すると思うんですが、そうなるとつい手癖で`cluster.fork()`したくなってしまう。
ただ実際のところ、メディアサーバーとして運用する場合、トラフィックより先にリソースが枯渇してくるので、マルチプロセス化する意義は薄いかなと。
Nodeのプロセス1つに対して、`mediasoup`の`Worker`も1つずつ使うようにしてもいいけど、それをやるかどうかは要件との相談になるはず。
1Workerでさばける接続数が決まってる + Workerをまたぐためにはそれ用のコードが必要になるので。
1プロセス内での`Worker`もとい`Router`の接続であれば、`mediasoup`がAPIとして用意してる`pipeToRouter()`が使える。
https://mediasoup.org/documentation/v3/mediasoup/api/#router-pipeToRouter
ただプロセスをまたぐならば、そのシグナリング処理を自分で書かないといけなくなるので、そこが判断のポイント。
まあ、Nodeのプロセスは1つにしてしまって、トラフィックを裁くことは別のサーバーでやるなり設計で回避するほうが筋が良いかなー。
接続状況の可視化
このサーバーで、いまどれだけの`Router`が用意されてて、それぞれにどれだけ`Transport`や`Consumer`がいるのかを知りたい場合。
それぞれを作る処理を書くときに、あわせてそれ用の状態を管理するのも手ではあるけど、`observer`というAPIがあるのでそっちを使うほうが便利です。
const resouceMap = {}; mediasoup.observer.on("newworker", worker => { const { pid } = worker; resouceMap[pid] = { router: 0, transport: 0, producer: 0, consumer: 0 }; worker.observer.on("close", () => delete resouceMap[pid]); worker.observer.on("newrouter", router => { resouceMap[pid].router++; router.observer.on("close", () => resouceMap[pid].router--); router.observer.on("newtransport", transport => { resouceMap[pid].transport++; transport.observer.on( "close", () => resouceMap[pid].transport-- ); transport.observer.on("newproducer", producer => { resouceMap[pid].producer++; producer.observer.on( "close", () => resouceMap[pid].producer-- ); }); transport.observer.on("newconsumer", consumer => { resouceMap[pid].consumer++; consumer.observer.on( "close", () => resouceMap[pid].consumer-- ); }); }); }); });
みたくどこかで定義しておくと便利。