コンテンツにスキップ

ライブの経緯

stream.md の仕組みを入れた順序と、採用しなかったもの・やめた作り・決めたときの測定・残っているリスクの記録です。

映像音声 (ffmpeg → fMP4 → WebSocket → MSE) → 字幕 (同じ ffmpeg の2つ目の出口から サイドチャネルへ) → データ放送 (1局に絞った TS を自前の BmlDecoder でデコードする。データ放送の統合) の順に入れ、どれも入っています。 4段目の低遅延の追い込みはやめました (WebRTC / WHEP による低遅延化)。

一度は入れかけたが、挟む理由はどれも ffmpeg 単体で足りた。

欲しかったもの 結論
デュアルモノ分離 ffmpeg で足りる。選ばれた側を両耳へ配る(多重音声はサーバで1本に決める)
局の選び分け ffmpeg で足りる。-map 0:p:<局>:v:0
音声・字幕が消えたときに無音を挿す tsreadex にしかできない。ただし実害を確認していない
aac: Input buffer exhausted 直らない。壊れた AAC フレームの修復は tsreadex の仕事ではない

3行目の実害を踏んだら入れる。

ブラウザ側での字幕デコード(aribb24.js)— 不採用

Section titled “ブラウザ側での字幕デコード(aribb24.js)— 不採用”

B24 をそのままブラウザへ送り、クライアントでデコードして描く構成。デコード方法が受け側ごとに別実装になり (ブラウザとテレビのアプリで2つ)、置き場所や外字の置き換えが揃わない。 デコードするのはサーバ (libaribcaption を 写したもの) に寄せ、受け側は描くだけにした (下の比較)。

H.264 + MPEG-TS + mpegts.js で先に全機能を通す — 不採用

Section titled “H.264 + MPEG-TS + mpegts.js で先に全機能を通す — 不採用”

前例が豊富(KonomiTV / web-bml と同一構成)で、段階を踏む案として有力だった。 字幕の経路が録画側で先に完成したので、あとで捨てるコードを書く理由が無くなった。 H.264 は段階ではなく、既定のコーデックとして残した。

「tsdemux の private pad で B24 を映像と同一クロック上で取れる」ことを理由に有力視したが、字幕は ffmpeg の 2つ目の出口で同じ時計に乗り、データ放送は denpa の中で自前でデコードするので、この論拠は無くなった。 周辺 (tsreadex / psisiarc / KonomiTV) も CLI パイプラインの構成で、GStreamer には ARIB/ISDB の知識も前例も無い。

WebRTC / WHEP による低遅延化 — 不採用

Section titled “WebRTC / WHEP による低遅延化 — 不採用”

縮むのは待っている間だけで、開いてから映像が出るまではむしろ伸びる。 WHEP で消せるのは受け側の貯め (ts/pacing.ts。宅内で 0.2〜0.9 秒)だけで、エンコードするのに掛かるぶんは運び方では消えない。 代わりに開くたびに ICE と DTLS の往復が増え、「押してから映るまで」が遅くなる。

いま(WebSocket + MSE) WHEP にすると
積むもの Bun.serve の WebSocket 1本 MediaMTX がもう1プロセス(+ SFU の設定・鍵・NAT)
字幕・データ放送 同じ1本で一緒に送る(WebSocket プロトコル) 別建て(DataChannel を自分で組む)
止めて見る・追っかけ・5分戻れる MSE の buffered がそのまま 全部書き直し(溜めが無いので追い越されて終わる)
音声・エンコード方式の選び直し 再エンコードして繋ぎ直すだけ 交渉からやり直し

「止めた所から見られる」「戻れる」は貯めているから成り立ち、低遅延はその貯めを削ることなので、両者は同じものの 両端にある。実測では 0.2 秒では止まり続け、落ち着いたのは 0.6 秒だった (一瞬止まって遅延が増えていく)。1秒足らずのためにその2つを捨てるのは割に合わない。 本当に縮めたいなら、エンコードするのをやめるほうが桁違いに効く(放送そのまま(MPEG-2)を送る)。 ffmpeg の WHIP muxer も H.264/Opus 固定の experimental で AV1 が使えない。

以前は描画も libaribcaption にやらせ、画像 (PNG) を配っていた。描く側だけを受け側に移した (#586) ときの比較:

画像を配る (前) 文字の配置を配る (いま) ブラウザで B24 をデコードする (aribb24.js)
置き場所・色・外字の置き換え libaribcaption libaribcaption と同じ計算 (写した) 実装依存
全画面での字 1920x1080 の画像の拡大 画面の画素で描く 画面の画素で描く
帯域 1枚 20〜50KB の PNG 1枚 数百バイト〜1KB 数百バイト
サーバ負荷 字幕を描いて PNG に組む 写してデコードするだけ なし
持つコード 出口だけ デコードする側 (表は手で持つ) と描く側 字幕ライブラリ1つ

同じ録画の 103 秒をライブと同じエンコード方式で通すと、CPU は 152 秒 → 101 秒 (1 本あたり 0.5 コア軽い)、字幕に配る量は 1.08 MB → 14.3 KB になった (sub2video が映像のコマごとに 1920x1080 の画像を起こして PNG に組んでいたぶん)。 作り物の字幕と実機の録画 30 枚を同じ丸ゴシックで描いて画像と画素単位で比べると、墨の外枠は全部 ±1 画素、色の差は平均 1〜3/255 だった。 外字を丸める処理は画像の頃はパッチで当てていた (#554) が、描く側を移したときに落ちていたのを ts/drcs-smooth.ts へ移した。

以前は字幕のために映像を作らない ffmpeg をもう1本、チューナーの共有をもう1つリクエストして当てていた。地上波2台のうち1台を番組表集めが 使っていると断られていた([captions] GR:T15: チューナーに空きがありません)。

字幕の時刻を測って合わせようとした頃

Section titled “字幕の時刻を測って合わせようとした頃”

どれも別々の ffmpeg の出口どうしを突き合わせて測っていた:

使った基準 実測のずれ
mp4 の 0(=エンコードされた1コマ目のつもり) 2.4秒。マルチプレクサが 0 に合わせるのは probe の間に溜まった音声
エンコードしているフレームの時刻(映像側の showinfo) 5秒。フィルタはエンコーダより先を走る
字幕側の showinfo が言う放送時刻 1秒ほど遅れる。行とフレームの数が合わず、組がずれる

「待たせる量を渡す」に替えても3回外した。別々に起こした ffmpeg は鍵フレームが 0〜0.5 秒ずれるので、測れないものを測っていた。 直したのは測り方ではなく作りで、1本の ffmpeg に両方を書き出させた(1本にしたのは、時刻を揃えるため)。 -copyts は効かないうえ、実機で Assertion next_dts <= 0x7fffffff failed at libavformat/movenc.c で映像ごと落ちた(放送時刻は 32bit に収まらない)。

「放送から N秒」を TDT と組にしていた頃は、局ごとの固定のずれ(11局で -0.72〜+0.90秒)がそのまま出て、T26 と T15 で「放送から」が 1.3秒違って見えていた。

  • PSI を先に流す手は外した。 前に開いたときの PAT/PMT を先に流し、選局待ちに ffmpeg の入口の仕事を重ねる手で、単体では効いた (888ms → 707ms)。実機では、電波より先に入口を読み終えた ffmpeg が -map 0:p:<局>:v:0 を解決できずに終了した (Failed to set value '0:p:24632:v:0' for option 'map'。測ったときは選局済みだったので見えなかった)
  • dvb_override_tune_delay を 1ms にすると、DVB core が同期の途中で周波数を振り直して 451ms に悪化した。あの値は「諦めるまでの猶予」で、 ロックの検出間隔ではない(検出はこちらが 10ms 間隔で直に読んでいる)
  • インタレ解除・音声・塊の細かさを外してもエンコーダの遅れは縮まなかった

一瞬止まって遅延が増えていったときの測定

Section titled “一瞬止まって遅延が増えていったときの測定”

エージェントのリングバッファ(6分視聴 + 番組表3本で溢れ 0回)・送り出し(H.264 25分で 0.5秒以上の間 0回)・流量 (-maxrate 8M で削っても8分に10回)と順に外して、残ったのがちょうど60秒ごとという周期だった。

途切れ 貯まり 中央 貯まり 最大
直す前 8分に10回 1.4秒 3.2秒
下限を覚える 8分に5回 0.7秒 3.2秒
下限を下げ直すまでを2分→10分 10分に3回(最後の6分は0回) 0.9秒 1.1秒
-maxrate を外す 10分に1回 0.6秒で平ら 0.7秒

MPEG-2 の音を AudioDecoder でデコードしていた頃

Section titled “MPEG-2 の音を AudioDecoder でデコードしていた頃”

WebCodecs の AudioDecoder は安全な繋ぎ(https か localhost)でしか出てこないので http の LAN では使えず、iPhone の Safari には iOS 26 まで無かった。iOS 27 の Safari では出てくるのに ADTS のまま渡すと転んだ(1枚目の映像のあと「音声を再生できませんでした」)。 ブラウザごとの癖を追うより、映像と同じ FFmpeg(WASM)でデコードすることにした(AudioDecoder と切り替えない)。

2026-10 までは描く側を otya128/web-bml から借りていた。web-bml をサイドカーとして起動して iframe で抱えると、web-bml は自前の ffmpeg で HLS を作って自分の <video> に映すので映像を出すプレイヤーが2つになる。そこで描画側 (BMLBrowser) だけを npm から引いていたが、 映像の入れ物を BML の Shadow DOM の中へ移す・onclick を本物の属性のまま DOM に入れる・ES の処理系ごと 700KB ある、といった癖への 手当てが重なったので、自前のエンジン (src/lib/bml/) に置き換えた。

デコードする側 (server/decode_ts.ts) も一度は借りたが、denpa が通るのは3割 (DSM-CC と PMT の記述子) で、そのために @chinachu/aribts (2.3MB) を抱えるより書き直した。自前の BmlDecoder は借りていた decodeTS と、揃うモジュール・ファイル数・ バイト数・種別まで一致することを確かめてある。古い端末に残る denpa_bml_denpa_nvram_… (web-bml の頃の NVRAM) は読まない。

デコードしたフレームを持つ数 の元の表。測り方は stream.md に。

GPU のある Chrome(Edge 155・Radeon RX 5700 XT・D3D11。H.264 は GPU でデコード、AV1 はこの GPU が デコードできないので CPU)。窓が隠れていたので、数えたのは描いた回:

経路 値 回 毎秒のコマ 毎秒 50 コマ未満の回 持ち数の最大
H.264 (GPU) 3 4 35.9 4 8
4 3 39.6 2 9
5 3 59.0 0 10
6 6 49.3 2 10
7〜10 10 58.9〜59.5 0 10〜11
11 3 51.0 1 12
12 4 — — — 4 回ともデコーダが止まり「再生が追いつきません」
AV1 (CPU) 3〜12 5 34.8〜59.5 2 5〜14 値によらず遅い回が混じる
MPEG-2 2 1 31.2 1 2
3・4 4 58.5 0 3〜4 落ち 15 秒で 24 コマ
6〜12 6 59.9〜60 0 4〜5 落ち 1〜3 コマ

GPU の無いコンテナ(--disable-gpu。VideoDecoder は CPU、WebGL は SwiftShader)。3 回ずつ:

経路 値 毎秒のコマ 落ち (15 秒) 持ち数の最大
H.264 (CPU) 2・3・4・6・8・12 58.0・59.5・58.5・56.9・55.5・56.9 30・8・23・47・68・46 3〜13 (値 + 1)
AV1 (CPU) 2・3・4・6・8・12 57.1・56.2・58.4・55.1・57.4・59.1 44・57・25・73・39・13 4〜14 (値 + 2)
MPEG-2 2〜12 14〜15 (描くのが追いつかない) — 2〜3 (WASM はどれも 64MB)

遅い回(毎秒 33 コマ前後)は値によらず混じる。窓が隠れた Edge が刻みを間引いたものと見て、値の差として読まない。 CPU の数だけ空回りのプロセスを並べる(BENCH_LOADS=busy)と、値によらず 36 回中 32 回で諦めた(重すぎて差は見えない)。

項目 内容 対応
Safari の AV1 Opus in mp4 / AV1 が MSE で通らない 既定の H.264 + AAC で観る
データ放送のエンジンの実装範囲 STD-B24 / TR-B14 / TR-B15 の部分実装 (データ放送の統合 の「まだ無いもの」) 実際のチャンネルで動作確認。完全互換は期待しない
規約面 通信系サーバへの非受信機アクセス 個人の視聴環境の範囲に留める (使ってよい範囲)
リポジトリ 用途
otya128/web-bml データ放送 (2026-10 まで描く側を借りていた。自前のエンジンの振る舞いの手本)
xqq/libaribcaption 字幕のデコード方法と表の写し元(ts/b24caption.ts・ts/b24-tables.ts。ffmpeg には組み込まない)
xtne6f/tsreadex / xtne6f/psisiarc TS 整形・データ放送抽出(どちらも要らなくなった)
monyone/aribb24.js / xqq/mpegts.js ブラウザ側で字幕を描く・MPEG-TS を直接再生する場合(不採用)