最近こんな参考実装を書いたので、その学びとハマりをメモ。
はじめに
材料の確認から。
- `mediasoup`
- サーバー側で使う
- Nodeのモジュールとして使えるWebRTCのSFU
- 受け取ったメディアからRTPを抜き出す
- `gst-launch-1.0`
- 録音を担当するのはGStreamer
- RTPを受け取って、ファイルに書き出す
サーバー側はこれだけ。
あとはクライアントとつなぐ部分を実装するだけ。
- RESTのAPIサーバー
- WebRTCまわりは`mediasoup-client`
シンプル構成でいいですね。
クライアント
`mediasoup-client`を使うだけ。
コードは割愛するけど、必要な流れを簡略化して書くとこのように。
- `Device`の初期化
- `SendTransport`の作成
- その上で録音したい`audio`のトラックを`produce()`
最低限で必要なのは以上。
必要であれば自分の確認用に`consume()`したり。
録音関連はRESTで叩けるようにしてあって、`produce()`したときの`id`を知らせるようにした。
サーバー
RESTのハンドラと`mediasoup`の呼び出しをよしなにする。
サーバーは`fastify`で書いたけど、前より便利機能が増えてたし最高だった。
こちらも録音に必要な流れを書くとこのように。
- クライアントの`produce()`を待つ
- サーバーで`Producer`が作られる
- クライアントの録音開始RESTで、`Producer`の`id`がわかる
- `PlainRtpTransport`を作って`connect()`
- RTPが吐かれるポートがわかる
- GStreamerをそのポートに向けて起動
- そこでさっきの`Producer`を`consume()`する
PlainRtpTransport
使ってる部分のコード抜粋。
// クライアントより const producerId = "..."; const rtpTransport = await router .createPlainRtpTransport({ listenIp: serverIp }); // RTPを出したいポートを適当に const remotePort = pickIpFromRange(recMinPort, recMaxPort); await rtpTransport .connect({ ip: serverIp, port: remotePort }); const rtpConsumer = await rtpTransport .consume({ producerId, rtpCapabilities: router.rtpCapabilities }); const ps = spawnGStreamer( rtpTransport.tuple.remotePort, router.rtpCapabilities.codecs[0], `${recordDir}/${producerId}.ogg` );
いま見るとわかりやすいけど、最初は「?」だった。
- `createPlainRtpTransport()`
- 既に立ってるはずの`mediasoup`というWebRTCエンドポイントに向けて橋を架ける
- `connect()`
- その橋を渡ってくるRTPの向き先
- つまり受け取りたいIPとポート
- 1サーバーでやってるので`ip`が同じなだけ
ちなみに、RTP/RTCPを分けて吐き出すこともできる。
GStreamer
こいつのパイプラインを組むのがいちばん大変だった。
最終的に落ち着いたのがコレ。
const cmd = "gst-launch-1.0"; const opts = [ `rtpbin name=rtpbin udpsrc port=${port} caps="application/x-rtp,media=audio,clock-rate=${clockRate},encoding-name=OPUS,payload=${pt}"`, "rtpbin.recv_rtp_sink_0 rtpbin.", "rtpopusdepay", "opusparse", "oggmux", `filesink location=${dest}` ].join(" ! ");
- `rtpbin`というGStreamerのコンポーネントで受ける
- 受けるのは`audio`のRTP
- `caps`には、`mediasoup`の設定をいれる
- コーデックはOPUSに限定してる
- なのでそれを`depeay`して`parse`
- 最終的には`.ogg`にして書き出す
今回はこれを`child_process`で`exec()`した。
大事なのはこれの起動ではなく、録音終了時のお作法。
`gst-launch-1.0`がそういう仕様になってるっぽいけど、`SIGINT`で落とさないといけない。
これを`SIGTERM`や`SIGKILL`で落とすと、ファイルが壊れて再生できない・・・というので半日潰しました。