ライブの経緯
stream.md の仕組みを入れた順序と、採用しなかったもの・やめた作り・決めたときの測定・残っているリスクの記録です。
映像音声 (ffmpeg → fMP4 → WebSocket → MSE) → 字幕 (同じ ffmpeg の2つ目の出口から
サイドチャネルへ) → データ放送 (1局に絞った TS を自前の BmlDecoder でデコードする。データ放送の統合)
の順に入れ、どれも入っています。 4段目の低遅延の追い込みはやめました (WebRTC / WHEP による低遅延化)。
採用しなかったもの
Section titled “採用しなかったもの”tsreadex は挟まない
Section titled “tsreadex は挟まない”一度は入れかけたが、挟む理由はどれも 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 は段階ではなく、既定のコーデックとして残した。
GStreamer — 不採用
Section titled “GStreamer — 不採用”「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 が使えない。
字幕を画像で配っていた頃
Section titled “字幕を画像で配っていた頃”以前は描画も 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秒違って見えていた。
選局の待ちで試したこと
Section titled “選局の待ちで試したこと”- 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 と切り替えない)。
web-bml を使っていた頃
Section titled “web-bml を使っていた頃”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) は読まない。
DECODED_MOST を決めたときの測定
Section titled “DECODED_MOST を決めたときの測定”デコードしたフレームを持つ数 の元の表。測り方は 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 回で諦めた(重すぎて差は見えない)。
既知のリスク・未解決事項
Section titled “既知のリスク・未解決事項”| 項目 | 内容 | 対応 |
|---|---|---|
| Safari の AV1 | Opus in mp4 / AV1 が MSE で通らない | 既定の H.264 + AAC で観る |
| データ放送のエンジンの実装範囲 | STD-B24 / TR-B14 / TR-B15 の部分実装 (データ放送の統合 の「まだ無いもの」) | 実際のチャンネルで動作確認。完全互換は期待しない |
| 規約面 | 通信系サーバへの非受信機アクセス | 個人の視聴環境の範囲に留める (使ってよい範囲) |
参考リポジトリ
Section titled “参考リポジトリ”| リポジトリ | 用途 |
|---|---|
| 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 を直接再生する場合(不採用) |