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

mediasoupとGStreamerで録音する

GitHub - leader22/mediasoup-recording

最近こんな参考実装を書いたので、その学びとハマりをメモ。

はじめに

材料の確認から。

  • `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を分けて吐き出すこともできる。

mediasoup :: API

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`で落とすと、ファイルが壊れて再生できない・・・というので半日潰しました。

まとめ

OSSのWebRTCのSFU、もはや`mediasoup`一択なのでは・・?と思う今日この頃。
GStreamer、ドキュメントは豊富なものの、豊富すぎてまったく読み解けない。

今回は録音しか試してないけど、録画も同じような感じでできるかな・・?