メディアパイプライン

ルームの参加者の映像と音声を SENCA のストリームとしてエクスポートし、合成・録画・外部への配信・文字起こしを 1 つのリソースでまとめて指定する方法を説明します。

リアルタイム通信

このページの見出し
  1. AWS との対応
  2. しくみ
  3. リソース
  4. 後片付け
  5. イベント
  6. 例
  7. グリッドでの資源の見積もり
  8. 注意点
  9. 関連するページ

オンライン会議を録画したい、会議の様子をそのまま配信サービスへ流したい。そんなときに使うのがメディアパイプラインです。 ルーム(リアルタイムのルーム)の参加者の映像と音声を SENCA のストリームにエクスポートし、 録画、合成(composite。複数の参加者の映像を 1 つの画面に並べること)、外部への配信(egress)、文字起こしを 1 つのリソースでまとめて指定します。 AWS の Chime SDK Media Pipelines に当たる機能で、API は /api/v1/media-pipelines です。

このページの要点:

  • 参加者 1 人につきストリームを 1 本作り、再エンコードせずにエクスポートします。以後は通常のストリームと同じく HLS・録画・出力で使えます。
  • composite を指定すると、参加者を格子状(grid)または話者を大きく(speaker)並べた 1 本のストリームを作ります。
  • recording を指定すると、参加者ごとのストリームと合成のストリームを自動で録画します。
  • egress を指定すると、合成のストリームを RTMP・SRT・配信サービス(PaaS)へ送ります。captions を指定すると、字幕と翻訳を付けます。
  • 実際の処理は、ルームが配置されているノードが行います。ノードは 1 秒ごとにルームの様子を確認し、指定どおりの状態にそろえます。

AWS との対応

AWS(Chime SDK Media Pipelines)SENCA
Media stream pipeline(Kinesis Video Streams へ送る)参加者ごとのストリーム(artifacts.participant_streams)。作成後は通常のストリームと同じく、HLS・録画・出力・パイプラインで使えます
Media capture pipeline(会議の録画)recording。参加者ごとのストリームと合成のストリームを自動で録画します
Media concatenation pipeline(録画の結合)通常の録画の API。GET /recordings/{id}/export.mp4 で 1 本の MP4 にまとめます
Media live connector pipeline(会議を RTMP へ送る)egress。合成のストリームを外部へ配信します(RTMP・SRT・PaaS)。composite が必要です
Media insights pipeline(文字起こし)captions。参加者ごとのストリームに字幕と翻訳を付けます(字幕と翻訳)

しくみ

メディアパイプラインの定義はコントロールプレーンに保存されるので、クラスターのどのノードの API からでも作成・変更できます。 一方、実際の処理はルームが配置されているノードが行います(配置先は GET /rooms/{id} の placement_status.node でわかります)。 そのノードが 1 秒ごとにルームの参加者を確かめ、ストリーム・合成・外部への配信を定義どおりの状態にそろえます。 ルームが別のノードへ移れば、移った先のノードが処理を引き継ぎます(クラスターとグリッド)。 処理の対象になるのは desired_state: running のメディアパイプラインだけです。

メディアパイプラインが作るリソース(ストリーム、合成のパイプライン、外部への配信の出力)の ID は、メディアパイプラインの ID から決まります。 何度作り直しても同じ ID になり、応答の artifacts で確認できます。合成のパイプラインと出力は、GET /pipelines/{id}・GET /outputs/{id} でも参照できます。

参加者ごとのストリーム(再エンコードなし)

参加者 1 人につき、ストリームを 1 本作ります。参加者は client_id で見分け、client_id が無ければ connection_id を使います。 participants が空なら全員を、client_id を並べればその参加者だけをエクスポートします。後者の場合、client_id の無い参加者は対象外です。

エクスポートするのは、参加者が送っている映像の最も品質の高いレイヤーと音声です。サイマルキャストの rid は低い順に並ぶので、最後の rid が選ばれます。 これを再エンコードせずにそのままストリームへ送り、映像は最初のキーフレームからエクスポートします。 使うのは映像トラック・音声トラックとも最初の 1 本だけで、映像も音声も送っていない参加者はエクスポートしません。

エクスポートの間、サーバーは参加者に 2 秒ごとにキーフレームを要求します。途中から見始めた HLS の視聴者や録画が、すぐに映像を表示できるようにするためです。 エクスポートが追いつかずにデータを取りこぼしたときもキーフレームを要求し、次のキーフレームから再開します。

参加者が送るトラックやレイヤーを変えると、その参加者のエクスポートをやり直します。退出すればエクスポートは止まりますが、ストリームは残ります。

カスケード(リアルタイムのルーム)のルームでは、別のノード(エッジノード)に接続している参加者もエクスポートします。 その参加者のトラックは、参加者のいるノードからノード間の中継で受け取ります。

E2EE(エンドツーエンド暗号化)のルームからエクスポートしたストリームには、metadata.e2ee: "true" が付きます。中身は暗号化されたままなので、 静止画・字幕・パイプラインの入力などには使えません(422。リアルタイムのルーム の E2EE の説明を参照)。

作成されるストリームの属性は次のとおりです。

項目値
名前{name}-{参加者}-{ストリーム ID の先頭 6 文字}(英数字と -_. 以外は - に置き換え、前半は 56 文字まで)
metadatamedia_pipeline(メディアパイプラインの ID)、room、participant
codec_profileルームの video_codec が h264・h265 なら any、それ以外(既定は vp8)なら royalty_free
hls.containerfmp4
配信元ルームからのエクスポートだけです。ストリームキーを使って外から配信することはできません(どのキーとも一致しないように設定されています)

export_participants: false にしても、合成の入力として使うために、参加者ごとのストリームは作られます。 ただし、そのストリームを録画(recording.participants)や文字起こし(captions)の対象にはできなくなります。

合成(composite)

合成は、参加者の映像を 1 つの画面に並べて 1 本のストリームにまとめる機能です。 画面に並ぶのは映像を送っている参加者で、音声は映像の無い参加者も含めた全員を 0 dB でミックスします。 内部では、このためのパイプラインが作られます。

参加者を合成に加えるのは、その参加者のストリームにデータが流れ始めてからです。まだ届かない入力を待って合成全体が止まらないようにするためです。 参加者が入れ替われば合成の構成も更新され、映像を送っている人が 1 人もいなくなると、合成のパイプラインは stopped になります。

合成のパイプラインはルームの配置先のノードに固定され(placement.node)、優先度はルームと同じです。 ルームが移ると、元のノードが合成を止め、移った先のノードが配置し直します。

名前は、合成のストリームが {name}-composite-{ID の先頭 6 文字}、パイプラインが {name}-compose-{ID の先頭 6 文字} です。 合成のストリームの codec_profile は、合成の video_codec かルームの video_codec のどちらかが H.264・H.265 なら any、どちらも違えば royalty_free です。

レイアウトは layout で選びます。

layout並べ方
grid映像を送信している人数を n とすると、列数 = ⌈√n⌉、行数 = ⌈n / 列数⌉ の格子に並べます。並び順は参加者の識別子(client_id など)の順です
speaker今話している人(主な話者)を上部に大きく(高さの 3/4)表示し、残りの参加者を下部の帯(高さの 1/4)に等分して並べます。映像が 1 人だけなら grid と同じです

speaker で大きく映す人は、スポットライトと同じ話者の検出(音声の大きさによるもの)で、合成に入っている参加者の中から 1 人選びます。 誰も話していなければ直前の話者のままで、最初は識別子の順で先頭の参加者です。

表示がめまぐるしく変わらないよう、今の話者が話している間と、切り替えてから 2 秒の間は切り替えません。 声が 0.2 秒続けば「話している」、1.5 秒途切れれば「話していない」とみなします。 切り替えは合成の画面構成(シーン)を変えるだけで、パイプラインは作り直しません。切り替えた画面はキーフレームから始まります。 今の話者は status.active_speaker でわかります。

合成のエンコードの設定は次のとおりです。

項目値
映像composite.video_codec、composite.bitrate_kbps、GOP = fps × 2(2 秒ごとにキーフレーム)
音声Opus 96 kbps
クロックrealtime(実時間で処理します)

合成の処理は、コーデックワーカー(デコード・合成・エンコードを担当する別のプロセス)が行います。ノードはコーデックワーカーを有効にして起動してください (--codec-workers・--codec-lib-dir。パイプライン)。

H.264・H.265 でエンコードするには GPU が必要です。ただし作成の時点では GPU の有無を検査しません。 GPU が無ければ、合成のパイプラインの配置か検証の段階で失敗し、その理由がパイプラインの placement_status に表示されます。

録画と結合

recording.participants: true にすると参加者ごとのストリームに、recording.composite: true にすると合成のストリームに、 自動録画の設定(recording.enabled: true、retention_days)が入ります。 録画はストリームにデータが流れると始まり、配信元(ルームからのエクスポート)が切断すると確定します(録画)。 保存先はノードのデータディレクトリ(--data-dir)です。

できた録画は通常の録画リソースなので、GET /recordings?stream={ストリーム ID} で探せます。 GET /recordings/{id}/export.mp4 を使えば、録画のセグメントを再エンコードせずにつなぎ、先頭に moov を置いた 1 本の MP4 として取り出せます。 入力が途中で途切れた箇所は、タイムスタンプをずらしてつなぎます。 録画中に呼ぶと 409 CONFLICT、4 GiB を超える録画では 413 PAYLOAD_TOO_LARGE になります。必要な権限(スコープ)は recording:read です。

外部への配信

egress の要素は 1 つずつ、合成のストリームの出力になります。書き方と検査の内容は出力リソースと同じです(入力と出力)。 protocol には rtmp_push(rtmp://・rtmps://)、srt_push(srt://)、paas(provider は cloudflare_stream・aws_ivs・aws_medialive)が使えます。

URL に資格情報は書けません(user@host の形は 400)。資格情報は登録済みのシークレットに入れておき、credential_ref: "secret:{名前}" で参照してください。

接続先の IP アドレスは、接続するときに検査されます。ループバックやプライベートアドレスなどへ配信できるのは、管理者が --egress-allow CIDR で許可した場合だけです (セキュリティ)。

文字起こし

captions の書き方は、ストリームの captions と同じです(字幕と翻訳)。 設定は参加者ごとのストリームそれぞれに入り、合成のストリームには入りません。使うには export_participants: true が必要です。

リソース

フィールド

フィールド型既定意味
namestring(必須)1〜40 文字の英数字と -_.。. で始めることはできません。テナント内で一意です(作成するストリームの名前に使うため、ストリーム名の上限 64 文字より短くしています)
roomstring(必須)ルームの ID。テナントのルームでなければ 400 です。作成後は変更できません
participantsstring[][](全員)エクスポートする参加者の client_id。100 人まで、各 1〜255 バイト
export_participantsbooltrue参加者ごとのストリームを、録画や文字起こしに使える成果物として扱います。recording.participants・captions を使うときに必要です
compositeobjectなし合成の設定(下の表)
recordingobjectなし録画の設定(下の表)
egressobject[][]外部への配信。8 個まで。composite が必要です
captionsobjectなし文字起こしと翻訳の設定
desired_statestringrunningrunning または stopped

composite のフィールドは次のとおりです。

フィールド既定範囲
layoutgridgrid・speaker
width128064〜3840 の偶数
height72064〜2160 の偶数
fps301〜60
video_codecav1av1・vp9・vp8・h264・h265(H.264・H.265 には GPU が必要)
bitrate_kbps2500100〜50000

recording のフィールドは次のとおりです。

フィールド既定意味
participantstrue参加者ごとのストリームを録画します。export_participants: true が必要です
compositetrue合成のストリームを録画します。composite が必要です
retention_daysなし録画を残す日数。1〜36500

recording の participants・composite は、省略すると true になります。 composite を設定していないメディアパイプラインで recording を指定するときは、"composite": false を明示してください(明示しないと 400)。

egress[] の各要素のフィールドは次のとおりです。

フィールド意味
protocolrtmp_push・srt_push・paas
urlpaas 以外で必須。2048 文字まで。空白・制御文字・資格情報を含めることはできません
credential_refsecret:{名前}。テナントに該当するシークレットが無ければ 400 になります。paas では必須です(配信サービスの API トークンを指定します)
provider配信サービスの種類。paas では必須です
options追加の設定。16 個まで、キーは 1〜64 文字、値は 256 文字まで

未知のフィールドがあると 400(INVALID_REQUEST)になります。Content-Type が application/json でなければ 415 になります。

応答

{
  "id": "…", "name": "mtg", "room": "meet", "generation": 1,
  "participants": [], "export_participants": true,
  "composite": {"layout":"grid","width":640,"height":360,"fps":30,"video_codec":"av1","bitrate_kbps":800},
  "recording": {"participants":true,"composite":true},
  "egress": [{"protocol":"rtmp_push","url":"rtmp://…"}],
  "desired_state": "running",
  "created_at": "…", "updated_at": "…",
  "artifacts": {
    "participant_streams": [{"stream":"…","name":"mtg-alice-1a2b3c","participant":"alice"}],
    "composite": {"stream":"…","pipeline":"…","outputs":["…"]}
  },
  "status": {
    "participants": [{"participant":"alice","stream":"…","video":true}],
    "composite_members": 2,
    "active_speaker": "alice"
  },
  "managed": false
}
  • participants・egress は空のとき、composite・recording・captions は設定が無いときに、応答から省かれます。 | フィールド | 意味 | |---|---| | artifacts.participant_streams | このメディアパイプラインが作った参加者ごとのストリーム。コントロールプレーンから読むので、どのノードに問い合わせても同じ内容です | | artifacts.composite | composite を設定したときだけ含まれます。合成のストリームとパイプラインの ID、出力の ID(egress と同じ順)が入ります | | status | 応答したノードでの実行状況。ルームの配置先以外のノードが応答したときや、停止中は null です。composite_members は今の合成の入力数(音声だけの参加者も数えます。合成が無ければ null)、active_speaker は speaker の並びで大きく映している参加者の識別子です(grid のときと、決まる前は null) | | managed | ルームの設定 recording から自動で作られたものなら true(ID は room-rec.{ルームの ID})。API から変更・削除はできず(409)、ルームの設定に合わせて更新され、none にすると削除されます(リアルタイムのルーム) |

外部への配信のシークレットは応答に含まれず、credential_ref の名前だけが返ります。

API

メソッドとパス権限内容
POST /api/v1/media-pipelinesstream:write作成します。201 と、Location・ETag を返します
GET /api/v1/media-pipelinesstream:read一覧を ID の昇順で返します。クエリは room・limit(1〜1000、既定 100)・cursor です。応答は {items, next_cursor} です
GET /api/v1/media-pipelines/{id}stream:read定義・成果物・実行状況を返します。ETag 付き
PATCH /api/v1/media-pipelines/{id}stream:write指定したフィールドだけを置き換えます。If-Match が必須です
DELETE /api/v1/media-pipelines/{id}stream:write削除します。204 を返します

作成時には、次の順で検査し、問題があればエラーを返します。

  1. 本文の形式(400 INVALID_REQUEST、415)
  2. room がテナントのルームか(400 INVALID_CONFIG)
  3. 内容の検査(400 INVALID_CONFIG。上のフィールドの範囲と依存関係)
  4. 名前の重複(409 CONFLICT)
  5. 1 つのルームあたり 8 個までの上限(403 QUOTA_EXCEEDED)

PATCH の検査の順は、If-Match の有無(428)、本文の形式(400・415)、存在するか(404)、世代の一致(412)、 作成時と同じ内容の検査(400)、名前を変えた場合の重複(409)です。 room は変更できません(未知のフィールドとして 400 になります)。composite・recording・captions は null を指定すると設定が外れます。

POST では Idempotency-Key を使えます(管理 API と SDK)。

後片付け

操作合成のパイプライン・外部への配信の出力参加者ごとのストリーム・合成のストリーム・録画
DELETE /media-pipelines/{id}削除します(PaaS の出力は、配信先のリソースも片付けます)残します
PATCH で desired_state: stopped削除します残します
PATCH で composite を外す、egress を変える変更前の定義で作った分を削除します(実行中なら、次の 1 秒ごとの確認で作り直します)残します
PATCH で participants を減らす—対象から外れた参加者のストリームも残します
DELETE /rooms/{id}ルームのメディアパイプラインを削除し、それが作ったリソースも削除します残します

表のとおり、エクスポートしたストリームと録画はどの操作でも残ります。要らなくなったら DELETE /streams/{id}・DELETE /recordings/{id} で削除してください。 停止すると参加者のエクスポートも止まり、ストリームの配信元が切断した扱いになって録画が確定します。 DELETE /rooms/{id} で消えたメディアパイプラインについては、media_pipeline.deleted は送られず、room.deleted だけが届きます。

参加トークンの発行で自動的に作られたルーム(auto: true)には注意が必要です。こうしたルームは、参加者がいなくなって empty_timeout_secs(既定 60 秒)が過ぎると削除され、 メディアパイプラインも一緒に削除されます。ところが合成のパイプライン(停止した状態)と外部への配信の出力は残ってしまいます(下の「注意点」)。 メディアパイプラインを使うルームは、POST /rooms で明示的に作ってください。

イベント

GET /api/v1/events(SSE。管理 API と SDK)に、次のイベントが送られます。

イベントdata送られるとき
media_pipeline.updated{room, desired_state}作成したとき、更新したとき
media_pipeline.deleted{}削除したとき

メディアパイプラインが作ったリソースは、それぞれのイベント(stream.*・pipeline.updated・output.*・録画のイベント)を送ります。

例

この例では $API=http://127.0.0.1:8080/api/v1 とし、$TOKEN には stream:write・stream:read・subscribe・recording:read を持つトークン (ここでは最初の管理者トークン)を入れておきます。 合成を試すので、ノードはコーデックワーカーを有効にして起動してください。配信先を同じマシンに置くなら --egress-allow 127.0.0.0/8 も必要です。

api() { m=$1; p=$2; shift 2; curl -s -X "$m" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' "$API$p" "$@"; }

# ルームを作る(明示的に作成する。auto のルームは空になると削除される)
api POST /rooms -d '{"id":"meet","settings":{"max_participants":8,"video_codec":"vp8"}}'

# メディアパイプライン: 参加者ごとのストリーム、2 人を並べた AV1 の合成、両方の録画、合成を RTMP へ配信
api POST /media-pipelines -d '{
  "name": "mtg", "room": "meet",
  "composite": {"layout":"grid","width":640,"height":360,"fps":30,"video_codec":"av1","bitrate_kbps":800},
  "recording": {"participants":true,"composite":true},
  "egress": [{"protocol":"rtmp_push","url":"rtmp://live.example.com/app/STREAMKEY"}]
}' | tee mp.json
MP=$(jq -r .id mp.json); CSID=$(jq -r .artifacts.composite.stream mp.json)

# 参加者のトークン(client_id で参加者を識別する)
api POST /rooms/meet/tokens -d '{"role":"sendrecv","client_id":"alice"}'

# 参加中: 成果物と実行状況を見る
api GET /media-pipelines/$MP | jq '.artifacts, .status'

# 合成のストリームを HLS で視聴する
api POST /streams/$CSID/playback-url | jq -r .playback.hls

# 退出後: 合成の録画を 1 本の MP4 にエクスポートする
RID=$(api GET "/recordings?stream=$CSID" | jq -r '.items[0].id')
curl -s -H "Authorization: Bearer $TOKEN" -o composite.mp4 "$API/recordings/$RID/export.mp4"

# 停止する(If-Match に現在の世代を指定)
G=$(api GET /media-pipelines/$MP -D - -o /dev/null | sed -n 's/^[Ee][Tt]ag: *//p' | tr -d '\r')
api PATCH /media-pipelines/$MP -H "If-Match: $G" -d '{"desired_state":"stopped"}'

# 削除する(合成のパイプラインと外部への配信は削除され、ストリームと録画は残る)
api DELETE /media-pipelines/$MP

そのほかの設定例:

// 録画だけ(合成なし)。composite: false を明示する
{"name":"rec","room":"meet","recording":{"participants":true,"composite":false,"retention_days":30}}

// 特定の参加者だけをエクスポートして文字起こしする
{"name":"cap","room":"meet","participants":["teacher"],
 "captions":{"asr_provider":"whisper_cpp","asr_endpoint":"http://asr.internal:8080","language":"ja","targets":["en"],
             "translate_provider":"deepl","translate_endpoint":"https://api.deepl.com","translate_credential_ref":"secret:deepl"}}

// 話者を大きく表示し、H.264 の合成を PaaS へ配信する(GPU が必要)
{"name":"show","room":"meet",
 "composite":{"layout":"speaker","video_codec":"h264","bitrate_kbps":4000},
 "egress":[{"protocol":"paas","provider":"cloudflare_stream","credential_ref":"secret:cf-token"}]}

グリッドでの資源の見積もり

メディアパイプライン自体はグリッドのワークロードではありません。グリッドは、その中身を 2 つに分けて見積もります。

参加者のエクスポートはルームと同じノードで動くので、ルームのワークロードの見積もりに上乗せします。実行中のメディアパイプライン 1 つにつき、 ルームの max_participants(既定 50)を m として、CPU 20 × m ミリコア、メモリ 8 × m MB です。

合成は別のワークロード(パイプライン)として、処理のグラフの構成から次の表で見積もります。

グラフの処理ノードCPU(ミリコア)メモリ(MB)GPU
基本200128—
デコード(参加者 1 人につき映像・音声の 2 つ)400(H.264・H.265 は 100)128H.264・H.265 はデコード 1
compose30064—
audio_mix50——
映像のエンコード(AV1・VP8・VP9)1500128—
映像のエンコード(H.264・H.265)100128エンコード 1
音声のエンコード(Opus)50128—

ネットワークは送信・受信とも 10 Mbit/s と見積もります。たとえば VP8 のルームで 2 人を AV1 で合成すると、CPU は 200 + 400 × 4 + 300 + 50 + 1500 + 50 = 3700 ミリコア、 メモリは 128 + 128 × 4 + 64 + 128 × 2 = 960 MB です。

注意点

  • メディアパイプラインは、ルームの配置先のノード(ホームノード)で動きます。カスケードのルームで別のノード(エッジノード)に接続している参加者の映像は、ホームノードがそのエッジノードから受け取ってエクスポートします。
  • status は、ルームの配置先のノードが応答したときだけ値を持ちます。ロードバランサーの背後では、別のノードが応答して null になることがあります。
  • 自動で作られたルームが空になって削除されると、メディアパイプラインは削除されますが、合成のパイプライン(停止状態)と外部への配信の出力のリソースは残ります。 GET /pipelines・GET /outputs で探して削除してください。
  • 録画の保存先はノードのデータディレクトリです。S3 互換ストレージへの保存と、WHIP での外部への配信には対応していません。

関連するページ

この文書の原文は、リポジトリの docs/guide/ja/11-media-pipelines.md にあります。内容は現在の版(v0.0.1)に合わせています。