#show tech >>log

Raspberry Pi 3B で自宅のテレビをどこでも見る ── 構築編・後編

>> はじめに - 映像をブラウザまで届ける

前編 で電源を直して,変換の実力が測れる状態になった.後編は映像を実際にブラウザまで届ける話になる.

先に結論を書いておくと,ここでもう半日溶かした.しかも今度は電源のような物理の話ではなく,コーデックの中身を読む羽目になるやつだった.

なお,この時点でチューナーはまだ届いていない.デモ用の素材を流して組み立てている段階だ.実際の放送を受信する話は完結編に回す.

>> パイプラインが4段になった理由

最終的にこういう形に落ち着いた.

mode=hw (フルセグ・4段)
 段0  mirakc から «スクランブル状態» の生TS(サービス分離済み)
 段1  B-CAS で Multi2 復号(ホスト側 libaribb25)
 段2  MPEG-2H.264(h264_v4l2m2m / HW エンコーダ)
 段3  SPS を各キーフレームへ複製して HLS 化(-c copy)

素直に考えれば「mirakc から取って ffmpeg で変換して HLS にする」の2段で済みそうなものだ.実際そのつもりで始めた.4段になったのには,それぞれ理由がある.

>>> 段1 - mirakc は復号してくれない

mirakc には decode-filter という設定があって,ここに復号コマンドを書けばスクランブルを解いてから渡してくれる.

ところが公式のコンテナイメージに復号コマンドが入っていない.recdvbmirakc-arib は同梱されているのだが,B-CAS のデスクランブルをやる部分は別だ.

イメージを自分でビルドして入れる手もあるが,更新のたびに追隨する必要が出る.ホスト側に libaribb25 を置いて,パイプの途中で復号する形にした.段が1つ増える代わりに,mirakc は公式イメージのまま使える.

>>> 段2 - ここが心臓部

MPEG-2 で飛んでくる映像を H.264 に変える.前編で実力を測ったところだ.h264_v4l2m2m を指定して,Pi 3B の専用回路にやらせる.

ちなみに出力の幅と高さは16の倍数にする必要がある.h264_v4l2m2m の制約で,外すとエンコーダが開かなかったり緑の帯が出たりする.プリセットの 480x272848x480 が中途半端な値なのはこのためだ.480x270 にしたいところだが,270 は16で割れない.

>>> 段3 - これが問題だった

本来なら段2で HLS まで出力できるはずだった.ここを分けることになったのが,今回の山場だ.

>> SPS/PPS の罠

>>> 症状 - 何も再生されない

段2で HLS を吐かせて,ブラウザで開いてみた.何も映らない.

hls.js   codec[tracks]=undefined / bufferAppendError
         HlsJsTrackRemovedError: video SourceBuffer が存在しない
ffprobe  profile=unknown / width=0 / level=-99
ffmpeg   non-existing PPS 0 referenced / decode_slice_header error

width=0 というのが不気味だった.ファイルは生成されているし,サイズもある.なのに幅が 0 と言われる.

>>> 原因 - SPS が最初のフレームにしか入っていない

H.264 には SPS / PPS というパラメータセットがある.解像度やプロファイルといった,映像を解釈するのに必要な情報がここに入っている.

h264_v4l2m2m は既定で sequence_header_mode「Joined With 1st Frame」 になっている.つまり SPS/PPS を最初のフレームに同梱するだけで,以降のフレームには付けてくれない.

これが HLS では致命的になる.

h264_v4l2m2m の出力
    → SPS/PPS は最初のフレームに同梱されるだけ
    → ffmpeg の extradata が «空»
    → 途中のセグメントに SPS が無く,自己完結しない
    → hls.js は SPS から avc1.xxxxxx を組むので判定不能

HLS は映像を数秒ずつのセグメントに切って配る仕組みだ.どのセグメントから再生を始めても成立する必要がある.ところが SPS が最初のフレームにしかなければ,2つ目以降のセグメントは「解像度もプロファイルも分からない映像」になってしまう.

hls.js はセグメントの中身から avc1.xxxxxx というコーデック文字列を組み立てて MediaSource に渡すのだが,材料が無いので undefined になる.だから SourceBuffer が作られず,codec[tracks]=undefined になっていた.

>>> 効かなかった手が3つ

原因が分かれば,あとは SPS を各キーフレームに複製すればよい.ffmpeg には dump_extra というビットストリームフィルタがあって,まさにそれをやってくれる.

-bsf:v dump_extra

効かなかった.

dump_extraextradata を各キーフレームへ複製するフィルタだ.ところが今回はその extradata が空なので,複製する元が無い.何もしないまま通過する.

次に,ドライバ側の設定を疑った.v4l2-ctlrepeat_sequence_header を 1 にすれば,エンコーダが毎回 SPS を付けてくれるはずだ.

v4l2-ctl -c repeat_sequence_header=1

設定は通る.値を読み戻すと 1 が返る.それでも変わらない.

ここでしばらく悩んだ.設定は受け付けられているのに効かない.

理由は V4L2 のコントロールが fd 単位だからだった.v4l2-ctl が開いたファイルディスクリプタに対して設定され,close した時点で捨てられる.後から ffmpeg が別の fd で開けば,そこは既定値に戻っている.

同じ理屈で,systemd の ExecStartPrev4l2-ctl を書くのも無駄だった.別プロセス・別 fd なので原理的に効かない.

試したこと 結果
単段に -bsf:v dump_extra を足す ✕ 複製する元(extradata)が空なので何もしない
v4l2-ctl -c repeat_sequence_header=1 を外から設定 ✕ V4L2 コントロールは fd 単位.close で捨てられる
ExecStartPrev4l2-ctl を叩く ✕ 同上.別プロセス・別 fd ゆえ原理的に効かん
>>> 効いた手 - 「書く側」と「読む側」で扱いが違う

半日ほど行き詰まったあと,気づいたのはこれだった.

同じ SPS でも「エンコーダが出す側」では extradata に入らないが,「デマクサが読む側」では extradata に入る.

1(書く側)  h264_v4l2m2m → mpegts   SPS1フレーム目に同梱されて出る
段2(読む側)  mpegts を -i で読む     → h264 パーサが SPS を拾い extradata を作る
               → dump_extra が各キーフレームへ複製 → 自己完結した HLS

つまり 一度 mpegts として書き出して,それをもう一度 ffmpeg に読ませる.読む側の ffmpeg は H.264 パーサを通すので,1フレーム目に同梱された SPS をちゃんと拾って extradata を組み立ててくれる.そうなれば dump_extra が仕事をする.

パイプで繋ぐだけなので,ディスクには何も書かない.

>>> 代償は1コアの7%だった

段を分けると CPU が増える.そこが心配だったのだが,測ってみたらこうだった.

修正後  seg_00011.ts:  profile=High / 640x360 / level=40 / デコードエラーゼロ ✅
CPU1 = 218%2.18コア) /2 = 7.2%0.07コア)

段を分けた代償は 1コアの 7% だけ.段2は再エンコードせず -c copy で通すだけなので,ほとんど何もしていない.これなら許容できる.

ドライバ自体は repeat_sequence_header を持っていて sequence_header_mode=0 (Separate) も選べるようになっている(min=0 max=1).ffmpeg 側が設定しきれていないのが真因だと思うが,手元では直せないので外側で辻祺を合わせた形だ.

>> 外で見るための逃げ道 - ワンセグ素通し

ここまでで映像は届くようになった.ただ,外出先で見ることを考えると通信量が気になる.いちばん軽い eco でも 約197MB/h ある.1時間見たら 200MB 近い.

そこでワンセグを素通しする段を足した.

mode=copy(ワンセグ・2段)
 段0  mirakc から生TS1  変換せず HLS 化(-c copy)

ワンセグが都合がいいのは2点ある.

  • スクランブルされていないので B-CAS の復号が要らない
  • 既に H.264 の 320x180 なので変換も要らない

つまり段1(復号)と段2(変換)を丸ごと飛ばせる.4段が2段になる.Pi の CPU はほぼゼロだ.

通信量は 約 88MB/h.eco の2分の1以下になる.画質は当然ワンセグ相当だが,外で「見えればいい」ときの本命はこれだと思っている.

なお mode=copy の段では,設定ファイルに書いた解像度や fps は表示用の参考値でしかない.放送されてきたものをそのまま流すので ffmpeg には渡していない.画面のボタンにも 320x180 素通し と出すようにして,他の段と性質が違うことが分かるようにした.

>> 監視の「正常」を固定値で持ってはいけない

最後に,死活監視で踏んだ話を書いておく.

5分ごとに Pi の状態を見て,異常があれば通知を飛ばすスクリプトを入れた.検査項目のひとつが vcgencmd get_throttled で,前編に出てきた低電圧の値だ.

導入したのは電源がまだ直っていない時期だった.当時は常に 0x50005 が出ていたので,それを「既知の状態」として黙らせる書き方にしていた.毎回鳴っても仕方がないからだ.

if throttled == 0x50005 then 黙る   ← 当時はこれで正しかった

そして電源が直った.正常値が 0x0 になった.

この瞬間,監視に穴が空いた.もし電源の問題が再発して 0x50005 が戻ってきても,スクリプトは「既知の値だ」と判断して黙ってしまう.いちばん知りたい異常を,いちばん見逃す形になっていた.

書き直した.

if 現在ビット(03)1つでも立っていれば鳴らす   ← こちらが正しい

「正常」を固定値で覚えさせてはいけない.環境が変われば正常値も変わる.見るべきは「異常を示すビットが立っているか」であって,「既知の値と一致するか」ではなかった.

似た話は他にもありそうで,自分で書いた監視を一度は疑ってみるべきだと思っている.

>> まとめ

後編はコーデックの中身を読む羽目になった話だった.

  • h264_v4l2m2m は SPS を最初のフレームにしか付けない.HLS は各セグメントが自己完結する必要があるので,これでは再生できない
  • dump_extra は万能ではない.複製する元の extradata が空なら何もしない
  • V4L2 のコントロールは fd 単位.外から v4l2-ctl で設定しても,別プロセスの ffmpeg には効かない
  • 「書く側」と「読む側」で extradata の扱いが違う.一度 mpegts に書いて読み直せば,パーサが SPS を拾ってくれる
  • 監視の「正常」を固定値で持たない.環境が変われば正常値も変わる

パイプが4段というのは一見すると回りくどいが,それぞれの段に「そうしなければ動かない理由」がある.段1は公式イメージに復号が無いから,段3は extradata が空だから.設計として選んだというより,踏んだ結果そうなったという方が近い.

次は完結編.チューナーが届いたら,実際に放送を受信して映るところまで書く.物理チャンネルのスキャンと B-CAS カードリーダーが残っている.

📄Arrow icon of a page linkRaspberry Pi 3B で自宅のテレビをどこでも見る ── 構想編

📄Arrow icon of a page linkRaspberry Pi 3B で自宅のテレビをどこでも見る ── 構築編・前編

📄Arrow icon of a page linkRaspberry Pi 3B で自宅のテレビをどこでも見る ── 構築編・後編