加工と字幕
パイプラインは、ストリームを受け取って加工し、結果を別のストリームとして出す機能です。講義の配信なら、板書のカメラを正面から見た形に補正し、その右下に講師の映像を小さく重ね(ピクチャー・イン・ピクチャー)、AV1 に変換して流す、といったことができます。OBS のようなシーンの切り替えや音量のフェーダーも、配信中に API から操作できます。
処理の内容はグラフで書きます。デコード、板書の補正、拡大縮小、切り抜き、合成、音声のミックス、エンコードといった部品を矢印でつないだもので、矢印をたどって元に戻る経路があってはいけません(こうした形を DAG と呼びます)。以下、グラフの部品(JSON の nodes の各要素)を処理ノードと呼びます。SENCA のサーバー(senca-node)を指す「ノード」とは別物です。
作っただけでは動きません。desired_state の既定は stopped なので、動かすときは "running" を指定してください。
例で使う API・TOKEN・H は、取り込みと再生 と同じです。
仕組み
グラフは senca-codec-worker(権限を絞った別プロセス)の中で動き、デコードした生のフレームがこのプロセスの外に出ることはありません。senca-codec-worker のないノードではパイプラインは動かず、GET /capabilities の pipelines が false になります。
source は SENCA のストリームから AU(エンコード済みの 1 フレーム分のデータ)を読み、sink は処理した結果の AU を出力先のストリームに配信者として送ります。出力先はふつうのストリームなので、HLS・録画・出力・字幕にそのまま使えます。ただし、出力先に別の配信者がつながっているとパイプラインは出力できません。
パイプラインはグリッドのワークロード(pipeline:{id})で、入力のストリームを受けているノードに優先して割り当てられます(クラスターとグリッド を参照)。処理のプロセスが異常終了したり処理に失敗したりすると observed_state が error になり、1・2・4…32 秒と間隔を空けながら起動し直します。
API
パイプラインを操作する API です。
| 操作 | リクエスト | 権限 |
|---|---|---|
| 作成 | POST /pipelines {name, graph, desired_state?, placement?} | stream:write |
| 一覧 | GET /pipelines?limit&cursor | stream:read |
| 取得 | GET /pipelines/{id} | stream:read |
| 変更 | PATCH /pipelines/{id} {name?, graph?, desired_state?, placement?}(If-Match が必須) | stream:write |
| 削除 | DELETE /pipelines/{id} | stream:write |
| 検証 | POST /pipelines/{id}/validate(本文で別のグラフを渡せます。省略すると、保存してあるグラフを検証します) | stream:read |
| 動作中の操作 | POST /pipelines/{id}/control | stream:write |
name の決まりはストリームの名前と同じで、テナントの中で重複できません。パイプラインは 1 つのテナントで 100 個まで作れます。desired_state の既定は stopped です。
グラフは作成と変更のたびに検証します。source・sink に書くストリームは、同じテナントのものでなければなりません。graph を変更するとパイプラインを止めて新しいグラフで起動し直すので、その間は出力が途切れます。
応答に入るフィールド:
| フィールド | 内容 |
|---|---|
observed_state | 今の状態(starting・running・error・stopped) |
last_error | 直近のエラー |
plan | 検証のときに見積もった、必要な資源 |
status・meters | 処理の状態と、音量などのメーターの値(既定では 1 秒に約 10 回更新) |
placement・placement_status | 割り当ての指定と結果 |
検証に失敗すると problem+json でエラーが返り、details.issues に [{code, node, message}] が入ります。ステータスは、デバイスが足りないとき(issues に CAPACITY_UNAVAILABLE があるとき)だけ 503 CAPACITY_UNAVAILABLE(retryable: false)で、ほかは 400 INVALID_CONFIG です。
issues[].code の意味:
issues[].code | 意味 |
|---|---|
MALFORMED | JSON として読めない、存在しないフィールドがある、version が 1 でない、または 16 MiB を超えている |
INVALID_PARAM | 値が範囲の外にある、または書き方が正しくない(ID の書き方も含む) |
DUPLICATE_ID | 同じ id の処理ノードが 2 つある |
UNKNOWN_REFERENCE | 存在しない処理ノード・ポート・シーン・画像を参照している |
TYPE_MISMATCH | つないだポートの型が合わない(エンコード済みのデータを合成に入れた、映像を Opus でエンコードしようとした、sink に生のフレームを入れた、など) |
CYCLE | 矢印をたどると元に戻る経路がある |
UNCONSUMED_OUTPUT | 終端(sink・snapshot_on_change)以外の出力が、どこからも使われていない(source は書いたトラックごとに、compose は program について調べます) |
POLICY_REJECTED | ロイヤリティフリーのコーデックに限っているのに、H.264・H.265・AAC を使っている(変換せずにそのまま通す場合も含む) |
UNSUPPORTED_CODEC | AAC をデコード・エンコードしようとした、または知らないコーデックを指定した |
CAPACITY_UNAVAILABLE | H.264/H.265 のハードウェアがない、その解像度に対応するエンコーダー・デコーダーがない、またはハードウェアで同時に使えるセッションの数を超える |
LIMIT_EXCEEDED | 処理ノードの数、解像度、画像・シーン・入力の数の上限を超えている |
source か sink のストリームが 1 つでも codec_profile: royalty_free であれば、グラフ全体を、ロイヤリティフリーのコーデックだけを使うものとして検証します。
グラフの書き方
グラフの JSON はこの形です。
{"version": 1, "options": {...}, "nodes": [ {"type": "...", "id": "...", ...}, ... ]}
version は 1 で、知らないフィールドがあれば拒否します。id は英数字・_・- からなる 1〜64 文字で、処理ノードは 256 個まで置けます。解像度は縦横とも 2 以上、最大 7680×4320、fps は 240 まで(合成のキャンバスは 120 まで)です。
ある処理ノードの出力を別の処理ノードにつなぐときは、"処理ノード" か "処理ノード.ポート" と書きます。source だけはポートを省けないので、cam.video・cam.audio のように書きます。compose のポートは program(既定)と preview、そのほかはすべて out で、省略できます。
ポート(処理ノードの出入り口)には型があり、合わないものはつなげません。
| 型 | 出力する処理ノード |
|---|---|
| エンコード済みの映像・音声(コーデック付き) | source(video・audio)、encode |
| 生の映像 | decode(映像)・board・scale・crop・compose |
| 生の音声(48 kHz) | decode(音声)・audio_mix(48 kHz ステレオ) |
options(全体の設定)
| 項目 | 既定 | 範囲 | 意味 |
|---|---|---|---|
clock | realtime | realtime・input | 出力のタイミングの決め方です。realtime は、実際の時間に合わせて進めます(ライブ向け)。input は、入力のタイムスタンプに合わせて進め、すべての入力がそろってから出力します(ファイルの処理や試験向け) |
overflow | drop | drop・block | キューがいっぱいのときの動きです。drop は、古いものを捨てます(映像は次のキーフレームまで捨てます)。block は、上流を待たせます(block_timeout_ms を超えたら捨てます) |
latency_ms | 100 | 0〜10000 | 入力の届くタイミングのばらつきを吸収するための遅延です。合成とミキサーは、この分だけ前の時刻のフレームを描きます |
missing_timeout_ms | 2000 | 100〜60000 | 入力が届かないときに、途切れたと判断するまでの時間です |
block_timeout_ms | 2000 | 10〜60000 | block で待つ時間の上限です |
input_queue_aus | 256 | 8〜100000 | 入力の映像の AU をためておくキューの長さです |
frame_queue | 8 | 1〜256 | 生のフレームをためておくキューの長さです(処理ノードの入力ごと) |
status_interval_ms | 100 | 0 または 20〜60000 | 状態とメーターを知らせる間隔です。0 にすると知らせません |
commit_timeout_ms | 10000 | 100〜600000 | 新しいグラフへの切り替えを待つ時間の上限です。管理 API の操作では使いません |
処理ノード
source(入力)
| 項目 | 必須 | 意味 |
|---|---|---|
stream | はい | 入力にする SENCA のストリームの ID です |
video・audio | どちらか一方は必須 | 使うトラックです。{codec, track?, width?, height?, fps?, sample_rate?, channels?} の形で書きます |
codec には、h264・h265・av1・vp8・vp9・opus・aac など、入力のストリームに実際に届くコーデックを書きます。track を省くと、その種類の最初のトラックを使います。width・height・fps・sample_rate(8000〜192000)・channels(1〜8)は、検証と必要な資源の見積もりにだけ使います。
decode(デコード)
エンコード済みの入力を、生の映像や音声に戻します。書き方は {input, max_width?, max_height?, threads?} です。max_width・max_height は受け付ける解像度の上限(既定 3840×2160)、threads はソフトウェアのコーデックが使うスレッドの数(0 で自動、64 まで)です。AAC はデコードできず、UNSUPPORTED_CODEC になります。
board(板書の補正)
斜めから撮ったホワイトボードや黒板を、正面から見た形に直して読みやすくします。書き方は {input, params} です。
params の項目 | 既定 | 意味 |
|---|---|---|
mode | whiteboard | ホワイトボード(whiteboard)か黒板(blackboard)かを指定します |
corners | なし | 板の 4 隅の座標です。左上・右上・右下・左下の順に、入力の画素単位で [[x,y],…] と書きます |
auto_detect | false | 4 隅を自動で見つけます(見つかるまで毎フレーム試します) |
redetect_frames | 0 | 4 隅を見つけ直す間隔(フレーム数)です。0 にすると見つけ直しません |
output_width・output_height | 1280・720 | 出力の大きさです(16 以上)。corners も auto_detect も指定しない場合は、入力の大きさのままです |
enhance | true | 照明のむらや反射を補正し、文字をくっきりさせます |
stroke_gain・chroma_gain | なし | 文字の濃さと、文字の色の強さの倍率です(0〜10) |
glare_suppression | なし | 照明の反射を抑えるかどうかです(true・false) |
invert_to_white | false | 黒板を、白地に暗い文字の画像に変換します |
clahe | false | 明るさに CLAHE(部分ごとにコントラストを整える処理)をかけます |
scale・crop
scale(拡大縮小)は {input, width, height, filter?} と書きます。filter は nearest・bilinear(既定)・bicubic・lanczos3・area から選びます。
crop(切り抜き)は {input, x, y, width, height} を入力の画素単位で書きます。大きさは偶数に切り下げます。入力の大きさがわかっていれば、範囲からはみ出したときに INVALID_PARAM になります。
compose(合成)
| 項目 | 既定 | 意味 |
|---|---|---|
inputs | (必須) | シーンの中で使う名前と、その映像を出す上流の処理ノード(生の映像)の対応です。32 個まで |
canvas | (必須) | 出力する画面の設定です。{width, height, fps_num, fps_den?(1), background?("#000000")} の形で書きます。幅と高さは偶数にします |
images | {} | ロゴなどの静止画です。名前と {png_base64} か {jpeg_base64} の対応で書きます。32 枚まで、1 枚あたり 8 MiB(base64 の状態で)までです。検証のときに、実際にデコードして確かめます |
scenes | (必須) | シーンの名前と {layers: [...]} の対応です。1〜64 個 |
program | (必須) | 出力するシーン(本番に出す画面)です |
preview | なし | preview ポートに描くシーン(次に出す画面の下見)です |
transition | {kind: cut, duration_ms: 0} | take で使う、既定の切り替えの効果です。kind は cut・fade・wipe_left・wipe_right のどれかで、duration_ms は 60000 までです |
missing | {hold_ms: 2000, slate_color: "#000000"} | 入力が届かないときの表示です。hold_ms(600000 まで)の間は最後のフレームを出し続け、その後は代わりの画面(スレート)を出します。slate_image に images の名前を書くと、色の代わりにその画像を出します |
threads | 0 | 描画に使うスレッドの数です(0 で CPU の数、64 まで) |
入力・画像・シーンの名前は、英数字・_・- を使った 1〜64 文字です。
シーンは、重ねて描くレイヤー(layers[])でできています。各レイヤーに描くものは、source(inputs の名前)・image(images の名前)・color(#RRGGBB)のどれか 1 つです。レイヤーの項目:
| 項目 | 既定 | 意味 |
|---|---|---|
x・y・width・height | (必須) | 画面の上での位置と大きさです |
crop | 0 | {left, top, right, bottom} の形で、各辺から割合で切り取ります |
fit | contain | 枠への収め方です。stretch(引き伸ばす)・contain(全体を収める)・cover(枠を埋める)のどれかです |
z | 0 | 重なりの順番です(大きいほど手前) |
opacity | 1.0 | 不透明度です(0〜1) |
corner_radius | 0 | 角の丸みの半径です(4096 まで) |
border | なし | 枠線です。{width (512 まで), color} の形で書きます |
visible | true | 表示するかどうかです |
filter | bicubic | 拡大縮小に使うフィルターです |
audio_mix(音声のミックス)
| 項目 | 既定 | 意味 |
|---|---|---|
inputs | (必須) | 名前と {from, gain_db?(0), mute?, solo?, pan?(0), delay_ms?(0)} の対応です。from には生の音声を指定します。1〜32 個 |
master_gain_db | 0 | 全体の音量(dB)です(-120〜24) |
limiter | true | 音が大きくなりすぎないように抑えるかどうかです |
gain_db(音量、dB)は -120〜24、pan(左右の位置)は -1〜1、delay_ms(遅らせる時間)は 0〜5000 です。出力は 48 kHz のステレオです。
encode(エンコード)
| 項目 | 既定 | 意味 |
|---|---|---|
input | (必須) | 生の映像か、生の音声です |
codec | (必須) | 映像は av1・vp8・vp9・h264・h265、音声は opus です |
bitrate_kbps | (必須) | ビットレート(kbps)です(1〜800000) |
gop | 2 秒分 | キーフレームの間隔です(フレーム数、100000 まで) |
low_latency | true | 遅延を小さくする設定でエンコードするかどうかです |
rate_control | cbr | ビットレートの決め方です。cbr(一定)か vbr(可変)です |
width・height | 入力の大きさ | 出力の大きさです。2 つとも指定します。大きさの違うフレームは拡大縮小します |
frame_ms | 20 | Opus の 1 パケットの長さ(ミリ秒)です。2.5・5・10・20・40・60 のどれかです |
threads | 0 | 使うスレッドの数です(0 で自動、64 まで) |
h264・h265 は GPU(NVENC・oneVPL・AMF)がないと CAPACITY_UNAVAILABLE になります。ほかは、av1 が SVT-AV1(かハードウェア)、vp8・vp9 が libvpx、opus が libopus でのエンコードです。AAC のエンコードには対応していません(コーデックと特許 を参照)。
snapshot_on_change(板書の変化を静止画に保存)
生の映像を見張り、板書などの内容が変わって落ち着いたところで JPEG を保存します。先生が板書を書き足して手を離したタイミングで 1 枚、という具合です。書き方は {input, params} で、sink のないグラフでも使えます。
保存先はストリームではなくパイプラインごと({data-dir}/pipeline-snapshots/{pipeline_id}/)です。処理ノードごとに params.keep 枚まで残し、超えたら古いものから消します。パイプラインを削除すると、動かしていたノードにある静止画も一緒に消えます。
保存した静止画を扱う API です。
| 操作 | API | 権限 | 結果 |
|---|---|---|---|
| 一覧 | GET /pipelines/{id}/snapshots | stream:read | {"items":[{id, taken_at, reason: "board_change", node, width, height, bytes, changed_fraction}]}(古い順。node は処理ノードの ID) |
| 1 枚を取得 | GET /pipelines/{id}/snapshots/{snapshot_id} | stream:read | JPEG |
| 削除 | DELETE /pipelines/{id}/snapshots/{snapshot_id} | stream:write | 204 |
静止画は、パイプラインを動かしているノードのディスクにあります。クラスターでは、そのノード(placement_status.node_name)の API に問い合わせてください。
params の項目:
params の項目 | 既定 | 範囲と意味 |
|---|---|---|
mode | whiteboard | ホワイトボード(whiteboard)か黒板(blackboard)かを指定します |
enabled | true | false にした場合は、動作中の操作 snapshot で始めます |
analysis_fps | 2.0 | 1 秒に何フレームを調べるかです。0〜120(0 ではすべてのフレームを調べます) |
stable_frames | 3 | 何フレーム続けて変化がなければ落ち着いたとみなすかです。1〜10000 |
change_fraction | 0.004 | 画面のどれだけの割合が変われば変化とみなすかです。0 より大きく 1 より小さい値 |
jpeg_quality | 85 | JPEG の画質です。1〜100 |
keep | 1000 | 残す枚数です。1〜10000 |
sink(出力)
{inputs, stream} と書きます。stream は出力先のストリームの ID です。inputs にはエンコード済みの映像 1 つと音声 1 つまで(1〜2 個)を並べ、その順にトラック番号 0、1 が付きます。source のトラックを直接つなげば、再エンコードせずにそのまま通ります。
出力先のストリームは、HLS のコンテナーがそのコーデックを入れられる設定にしておいてください。たとえば AV1・VP9 を出すなら hls.container: fmp4 が必要です(取り込みと再生 を参照)。
例: ピクチャー・イン・ピクチャー
板書のカメラ(jb)を画面いっぱいに映し、講師(jt)を右下に小さく重ねます。音声は講師のものを Opus に、合成した映像は AV1 にエンコードして jout に出します。3 つのストリームは、どれも codec_profile: royalty_free・hls.container: fmp4 で作ります。
for n in jt jb jout; do
curl -s -X POST "$API/streams" "${H[@]}" -d '{"name":"'$n'","codec_profile":"royalty_free","hls":{"container":"fmp4"}}' > $n.json
done
id() { python3 -c "import json;print(json.load(open('$1.json'))['id'])"; }
cat > graph.json <<EOF
{"version":1,"options":{"clock":"realtime"},"nodes":[
{"type":"source","id":"t","stream":"$(id jt)","video":{"codec":"vp9"},"audio":{"codec":"opus"}},
{"type":"source","id":"b","stream":"$(id jb)","video":{"codec":"vp9"}},
{"type":"decode","id":"dt","input":"t.video"},
{"type":"decode","id":"db","input":"b.video"},
{"type":"decode","id":"at","input":"t.audio"},
{"type":"compose","id":"prod","inputs":{"board":"db","teacher":"dt"},
"canvas":{"width":640,"height":360,"fps_num":30},
"scenes":{
"pinp":{"layers":[{"source":"board","x":0,"y":0,"width":640,"height":360},
{"source":"teacher","x":440,"y":250,"width":192,"height":108,"z":1}]},
"teacher":{"layers":[{"source":"teacher","x":0,"y":0,"width":640,"height":360}]}},
"program":"pinp","preview":"teacher","transition":{"kind":"fade","duration_ms":300}},
{"type":"audio_mix","id":"mix","inputs":{"mic":{"from":"at","gain_db":0}}},
{"type":"encode","id":"venc","input":"prod","codec":"av1","bitrate_kbps":800,"gop":30},
{"type":"encode","id":"aenc","input":"mix","codec":"opus","bitrate_kbps":96},
{"type":"sink","id":"out","inputs":["venc","aenc"],"stream":"$(id jout)"}]}
EOF
python3 -c "import json;print(json.dumps({'name':'pinp','graph':json.load(open('graph.json')),'desired_state':'running'}))" > body.json
curl -s -X POST "$API/pipelines" "${H[@]}" --data-binary @body.json
jt と jb に VP9(と Opus)で配信すると jout が配信中になり、再生 URL を開けば合成した結果が見られます。
動かすには、VP9 のデコード(libvpx)、AV1 のエンコード(SVT-AV1)、Opus のエンコード(libopus)のライブラリが要ります。入れ方は コーデックと特許 を参照してください。
動作中の操作(control)
配信中にシーンを切り替えたり音量を変えたりするには、POST /pipelines/{id}/control に操作の JSON を送ります。操作の種類は "op" で選び、知らないフィールドがあれば 400 です。合成とミキサーへの操作は、次の出力フレームの切れ目で反映されます。
上から順に、下見のシーンを本番に出す、ワイプで pinp に切り替える、マイクを 6 dB 下げる、という操作です。
curl -s -X POST "$API/pipelines/$PID/control" "${H[@]}" -d '{"op":"take","node":"prod"}'
curl -s -X POST "$API/pipelines/$PID/control" "${H[@]}" -d '{"op":"switch_program","node":"prod","scene":"pinp","transition":{"kind":"wipe_left","duration_ms":500}}'
curl -s -X POST "$API/pipelines/$PID/control" "${H[@]}" -d '{"op":"set_audio","node":"mix","input":"mic","gain_db":-6}'
node には操作する処理ノードの id を書きます。使える操作:
op | 項目 | 意味 |
|---|---|---|
switch_program | node、scene、transition? | 本番に出すシーンを切り替えます(transition を省略すると、効果なしで切り替えます) |
set_preview | node、scene | 下見(プレビュー)のシーンを設定します |
take | node、transition? | 下見のシーンを本番に出します(効果の既定は合成の transition)。下見には、それまで本番に出ていたシーンが入ります |
update_layer | node、scene、layer(番号)、patch | レイヤーの一部を変更します。patch に書けるのは x・y・width・height・z・opacity・visible・source です |
set_scene | node、scene、layers | シーンを作るか、置き換えます |
set_audio | node(audio_mix)、input、gain_db?・mute?・solo?・pan?・delay_ms? | 音声の入力の設定を変えます(指定した項目だけ) |
set_master | node、gain_db | 全体の音量を設定します |
snapshot | node(snapshot_on_change)、enabled | 変化の検出を始めたり止めたりします |
request_keyframe | node? | キーフレームを要求します(node を省略すると、映像のエンコードすべてに要求します) |
成功すると 200 {"ok": true, "reply": {...}} が返ります。パイプラインが動いていなければ 409、存在しない処理ノードやシーンを指定したなど内容に誤りがあれば 400 で、理由は details.reason に入ります。
ここでの操作は、動いているパイプラインにだけ効きます。set_scene や update_layer で変えても保存してあるグラフはそのままなので、起動し直すと元に戻ります。変更を残したいなら PATCH でグラフを書き換えてください。なお prepare・commit・rollback(グラフの切り替えに使う内部の操作)はこの API からは使えず、400 になります。
デグレード(degrade)
デバイスやノードの資源が足りなくなったとき、グリッドはパイプラインを止めずに、一部の処理ノードを外して動かし続けられます。どこまで外すかは、あらかじめ決めておくデグレードレベルで指定します。次の例は、板書の入力を外すレベルを 1 つ設定しています。
curl -s -X PATCH "$API/pipelines/$PID" "${H[@]}" -H "If-Match: $ETAG" -d '{
"placement": {"priority": "high", "degrade": [
{"label": "no-board", "disable": ["b"]}
]}
}'
placement.degrade は、品質の高い順に並べたレベルの配列(8 レベルまで)で、各レベルを {label, disable} で書きます。label は重複できず、full(何も外さないレベル 0 の名前)も使えません。disable は空にできず、グラフにある処理ノードの ID しか書けません。
レベルは積み重なります。レベル 2 では、レベル 1 とレベル 2 の両方の disable が外れます。
指定した処理ノードを外すと、入力が全部なくなった下流の処理ノードも一緒に外れます。複数の入力を持つ sink・compose・audio_mix のように入力の一部だけが外れるものは、その入力を除いて残ります。compose では、外れた入力を使うレイヤーが各シーンから取り除かれます。
外すときは上流(source など)から指定してください。 外したあとのグラフも検証するので、下流だけを外すと上流の出力が宙に浮き、UNCONSUMED_OUTPUT で動きません。上の例では板書の source(b)を外しているので、つながっている decode(db)も外れ、合成から板書のレイヤーが消えて講師だけの映像になります。
レベルが変わるとパイプラインは新しいグラフで起動し直し、結果が placement_status(state: degraded、level、level_label)に出ます。各レベルで必要な資源(CPU・メモリ・GPU のセッション)は、処理ノードを外したあとのグラフから見積もります。
どのレベルが選ばれるか、優先度、デバイスが止まったときの動きについては、クラスターとグリッド を参照してください。
関連するページ
この文書の原文は、リポジトリの docs/guide/ja/07-pipelines.md にあります。内容は現在の版(v0.0.1)に合わせています。