🧊

mediasoupの開発Tips

`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`から。

mediasoup :: Tricks

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のスケーラビリティについて - console.lealog();

`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--
        );
      });
    });
  });
});

みたくどこかで定義しておくと便利。

その他

Firefoxだとローカルで疎通できない

Firefox: ICE failed, add a STUN server and see about:webrtc for more details - mediasoup-client - mediasoup

ローカルで開発してて`localhost`のIPを指定するのが面倒で`127.0.0.1`ってしてる場合、Firefoxだけ通らない。(自分でIP調べて入れれば動く)