#show tech >>log

Raspberry Pi 3B で自宅のテレビをどこでも見る ── 完結編

>> はじめに - チューナーが届いた

構築編・後編 まではデモ用の素材を流して組み立てていた.チューナーが無いのに配信の仕組みだけ先に作っていたわけだ.

そのチューナーが届いた.ここからは実際の放送を受信する話になる.

先に結論から書くと,映った.8局が並び,音が出て,番組表も出て,スマホとPCの両方から見られるようになった.

ただ,そこに至るまでに道具が2つ続けて使えず,「1つしかない」ものを3回踏んだ.後者がこの記事の本題になる.

>> 物理チャンネルを探す - 道具が2つとも使えなかった

まず自宅にどのチャンネルが飛んできているかを調べる必要がある.地デジは物理チャンネル(13〜62ch)で飛んでいて,リモコンの「1」「4」といった番号とは別物だ.

>>> recisdb も isdb-scanner も動かなかった

日本の地デジ環境ではよく使われる recisdbisdb-scanner を入れてみた.

Unexpected Linux error: EPERM
50チャンネル中 50チャンネル失敗 (所要 2.09)

50チャンネル全部が2秒で失敗した.速すぎる.実際に電波を測ったのではなく,デバイスを開く段階で弾かれている.

原因はチューナーのチップだった.今回使ったのは Siano 系のチップで,これらのツールが想定している PLEX 系・PX-S1UD 系とは制御の仕方が違う.ツール側が期待する ioctl が通らず EPERM で落ちる.

日本の地デジ環境は情報が豊富なぶん,「みんなが使っている構成」から外れると急に足場が無くなる.ここは素直に諦めて別の道具を探した.

>>> dvbv5-zap は動く.ただし嘘をつく

Linux 標準の DVB ツールである dvbv5-zap は動いた.こちらはチップ非依存の汎用ツールだ.

ただ,こちらにも罠があった.

ch17   Lock (0x1f)   Signal -80.00dBm   C/N  0.00dB   Quality  0%
ch26   Lock (0x1f)   Signal -80.00dBm   C/N  0.00dB   Quality  0%

Lock と言っている.でも Signal は -80dBm で C/N は 0.00dB.つまり何も受信していない.

Lock のビットは「同期が取れた」という意味だが,このチューナーは信号が無くても立ててくる.素直に信じると,存在しないチャンネルを全部「あり」と判定してしまう.

判定は C/N と Quality で行うようにした.結果,自宅で実際に取れる物理チャンネルが確定した.

物理ch リモコン
21 011 東海テレビ
13 021 NHK Eテレ
28 031 NHK総合・津
19 041 中京テレビ
18 051 CBCテレビ
22 061 メ〜テレ
27 071 三重テレビ
23 101 テレビ愛知

8局.三重県なので在名局が全部入るのがありがたい.

>> B-CAS の復号は拍子抜けするほど軽かった

地デジは Multi2 という方式でスクランブルがかかっている.これを B-CAS カードで解く必要がある.

後編で書いたとおり,mirakc の公式イメージには復号が入っていないので,ホスト側の libaribb25 をパイプの途中に挟む形にしていた.

ここが一番の懸念だった.Pi 3B の CPU で,17Mbps の TS をリアルタイムに復号し続けられるのか.事前の見積もりでは 1コアの 10〜15% くらい食うだろうと踏んでいた.

実測はこうだった.

arib-b25-stream-test   CPU 3.4%1コアの 3.4%

見積もりの3分の1以下だった.Multi2 の復号自体はそれほど重い処理ではないらしい.懸念は杞憂に終わった.

>> 「1つしかない」ものを3回踏んだ

ここからが本題になる.

このシステムには共有できない資源がいくつかある.チューナーは1本しかないし,B-CAS カードも1枚しかない.頭では分かっていたのに,3回踏んだ.

>>> ① チューナー - ワンセグで本番を2回止めた

後編で「外で見るときの本命はワンセグだと思っている」と書いた.これは間違いだった.

ワンセグは同じ物理チャンネルの中に一緒に飛んできている.mirakc から取れるはずだと考えて,実装してボタンを付けた.

ところが mirakc が返してきたサービスは21件すべてが type=1 (フルセグ)だった.ワンセグに相当する部分受信のサービスが1つも出てこない.

それに気づかず,ワンセグだと思って掴んだのはフルセグの副チャンネルだった.

ワンセグのつもりで掴んだもの = フルセグの副ch(スクランブル済み・MPEG-2)
      ↓
-c copy で «変換せずそのまま» 流す設計だったので復号も変換もされない
      ↓
パイプが死ぬ → systemd の Restart=always で再起動 → また死ぬ
      ↓
★チューナーは1本しかないので,本番の映像も道連れになる

テレビが止まった.しかも Restart=always が律儀に効いて,止まっては起き,を繰り返す.

2回やった.1回目で原因を突き止めきれず,直したつもりでもう一度踏んだ.

反省点ははっきりしている.未検証の機能を「押せる場所」に置いてはいけない.画面にボタンを1つ足すというのは,誰かが押せるようにするということだ.検証が済むまでボタンにすべきではなかった.

最終的にワンセグは見送った.プリセットの定義はコメントアウトして,「なぜ見送ったか」の理由と一緒に残してある.消してしまうと,半年後に同じことを思いついてまた踏む.

——と,この時は書いた.翌日,これをひっくり返すことになる.

>>> ② B-CAS カード - 調べようとして本番を殺した

別の場面.ある画質プリセットがうまく動かない気がしたので,原因を調べようとした.

本番の配信は動かしたまま,もう1本だけ復号のプロセスを立てて様子を見ようと考えた.読み取るだけだから害は無いだろう,と.

B_CAS_CARD::init() : code=-4

本番が落ちた.

B-CAS カードも「1つしかないもの」だった.チューナーの排他は意識していたのに,カードのことは頭から抜けていた.カードリーダーは同時に1プロセスしか掴めない.2本目を立てた瞬間,1本目が弾き出された.

復旧は,配信を止めて,残っていた復号プロセスを片付けて,pcscd を再起動して,配信を立て直す,という手順になった.

しかもこの事故には後日談がある.このとき「プリセットがうまく動かない」と判断した根拠のログは,先頭行が B-CAS のエラーだった.つまりプリセットのせいではなく,自分が立てた2本目のせいだった.調べようとした行為が,調べたい対象を汚していた.

あとで本番と同じ経路で試し直したら,そのプリセットは普通に動いた.

>>> ③ EPG の優先度 - 永久に空振りしていた収集

番組表を集めようとして,3つ目を踏んだ.

mirakc には EPG を自動収集する仕組みがある.設定して待っていたのだが,いつまで経っても番組表が増えない.ログを見るとこう出ていた.

WARN No tuner available channel=GR/28 Job(epg.update-schedules) priority=-1
EPG 収集   priority = -1
視聴       priority =  0     ← こちらが強い
    ↓
配信が動いている限り,EPG はチューナーを永久に取れない

視聴の優先度が高いので,EPG に順番が回ってこない.24時間配信しっぱなしにしていたので,構造的に永久に空振りする状態だった.

対策として,毎日 04:18 に配信を止めて EPG に譲る「窓」を作った.

ここでも2つ間違えた.

1つ目.窓を開けただけでは何も起きない.mirakc の EPG ジョブは時刻が来て初めて動くので,こちらが勝手に空けた時間帯には走らない.mirakc 自体を再起動するのが確実な引き金だった(起動直後にジョブを実行する仕様).これに気づくまで,18分間ただテレビを止めていた.

2つ目.収集が終わったかどうかを番組数の増減で判定していた.ところが1局分が終わるまで数は動かない.途中の静止を「終わった」と読み違えて,収集中の局を追い出した.

正しくは,mirakc 自身が Done job="update-schedules" とログに書くのを待てばいい.推測せず,本人が終わったと言うのを待つ.

直したら,8局で約8分,6109件の番組が取れた.

>> 番組表 - 持っているのに取りに行けない

集めた番組表を画面に出そうとして,また引っかかった.

mirakc には「いま放送中の番組」を返す /api/onair がある.これを叩けば済むはずだった.

GET /api/onair   →  [] または 404

番組表は入っているのに,空が返ってくる.このビルドでは使えないらしい.

代わりに使えたのは,局ごとの番組一覧を返すエンドポイントだった.ただしこれが 495KB ある.画面から毎分これを引かせるのは論外だ.

サーバ側で絞ることにした.

GET /onair?sid=…   その局の «今と次»(数百バイト)
GET /onair         全局の «今»(一覧に出すため)
   ★取り込んだ時点で時刻と題名だけに削って憶える(10分保持)

495KB を数百バイトにして返す薄い層を挟んだ形になる.

ついでに番組名の掃除もした.放送の番組名には [字] [解] [再] といった記号や,全角スペースでの整形, 以降のサブタイトルがくっついてくる.そのまま並べると読みにくいので,表示用に整えている.

>> 切替が12秒かかっていた本当の理由

チャンネルを変えてから映るまで12秒かかっていた.チューナーの同調に時間がかかるのだろうと思い込んでいた.

配信の設定を見直していて,ふと m3u8 を開いた.

#EXTINF:4.000000,
#EXTINF:4.000000,
#EXTINF:4.000000,

2秒を頼んだのに4秒のセグメントが出ていた.

原因は GOP だった.GOP はキーフレームの間隔で,-g 60 と固定で書いていた.

-g 60 を 15fps で使うと   60 ÷ 15 =4秒のGOP
HLS はキーフレームからしかセグメントを切れない
    ↓
2秒を頼んでも 4秒になる
    ↓
プレイヤーは3本ほど溜めてから再生を始めるので  4秒 × 3 = 12

-g はフレーム数で効く.なので fps を下げたときに,秒数のほうが勝手に伸びていた.

直し方は単純だった.

GOP=$(( FPS * 2 ))     # 15fps → 30 / 30fps → 60(どちらも2GOP
#EXTINF:2.000000,    ← 直後に確認

3本で 12秒 → 6秒.

面白かったのは,30fps のプリセットは元から正常だったことだ.60 ÷ 30 = 2秒 で辻褄が合っていた.損をしていたのは 15fps の軽いプリセットだけ ── 外出先で使うことを想定した,いちばん軽くしたものが,いちばん切替が遅かった.

ただしタダではない.GOP を詰めるとキーフレームが増えるので,同じ画質でもビットレートは上がる.切替の速さと通信量の交換になる.

>> 監視が一晩で22通鳴った

死活監視は後編で作っていた.5分ごとに状態を見て,異常があれば通知を飛ばす.

実際に運用に入れた翌朝,通知が22通届いていた.

分けて数えるとこうなった.

21:1400:21   15通   自分の作業(再起動を繰り返していた)
② 04:19/04:24/04:29  3通   ★EPG の窓
③ 23:55/00:16/03:17  4通   ★一瞬だけ異常に見えたもの
>>> ② 自分で止めておいて驚く

②の中身はこうだった.

・配信プロセスが停止(enabled なのに inactive)
・HLS362秒 更新されていない(配信が止まっている)
・live.m3u8 の中身が壊れている

全部その通りだ.でもこれは EPG 収集のために自分で止めている時間帯だった.

監視は「配信が止まっている」ことしか見ていない.それが計画されたものなのか,事故なのかを区別する材料を渡していなかった.

直し方として,閾値を緩めるという手もある.でもそれをやると本物の異常も鈍る.「いまは予定された停止だ」と教える方が筋だと判断した.

if systemctl is-active --quiet tv-epg.service; then
  echo "EPG 収集中なので判定を見送る — 配信は «わざと» 止めている"
  exit 0
fi
>>> ③ 1回の取りこぼしで決めつけない

③は「tailscale の公開設定が消えている」という通知だった.5分後には自動で復旧している.

該当箇所を見たら,1回しか試していなかった.

tailscale serve status | grep -q '127.0.0.1:8080' || add "設定が消えている"

タイムアウト指定も無ければ,再試行も無い.常駐プロセスが一瞬取り込み中で空を返しただけで,「消えた」と断定する作りになっていた.

3秒おいて2度試す形にした.加えて,異常は2回続いたときだけ通知するようにした(復旧の知らせは即座に出す).本物の検知が5分遅れる代わりに,一瞬のまたたきでは鳴らなくなる.家庭用の仕組みなので,この交換は割に合うと判断した.

>>> そして,監視そのものが黙る穴

作り直しているときに,別の問題に気づいた.

notify 1 "⚠ 異常"            # ← ここが失敗しても
echo "$sig" > "$STATEFILE"   # ← 無条件に «送った事» として記録される

通知の送信に失敗しても,送ったことにしていた.

つまり通知サービスが落ちている間に起きた異常は,二度と通知されない.状態が「通知済み」に進んでしまうので,次回は「変化なし」と判断される.

質が悪いのは,平時はまったく気づかないことだ.壊れているときにだけ壊れる.いちばん報せてほしい場面で黙る.

if notify 1 "⚠ 異常" "$body"; then
  echo "$sig" > "$STATEFILE"      # 通った時 «だけ» 記録する
else
  echo "!! 通知に失敗した.次回また試す"
fi
>>> おまけ - 犯人だと思ったものが違った

余談だが,この調査中にカーネルログにこんな警告が大量に出ているのを見つけた.

WARN::dwc_otg_hcd_urb_dequeue:639: Timed out waiting for FSM NP transfer to complete

USB の転送タイムアウトだ.チューナーは USB 接続なので,これが原因だと確信した.通知の時刻とも近い.

念のため数えてみた.

一晩で 7707件(毎時 約880件・ほぼ一定)
    03:1570件 → 通知が鳴った
    04:200件 → ★0件なのに鳴った

関係なかった.Pi 3B の USB コントローラが高帯域の転送で常に吐いている雑音で,映像は正常に出ている.

もっともらしい筋を思いついたときこそ,先に数えるべきだった.時間帯別に数えるだけで1分で否定できた仮説を,しばらく信じ込んでいた.

>> 翌日 - ワンセグは動いた

見送った翌日,もう一度だけ調べることにした.きっかけは通信量ではなく消費電力だった.

フルセグ   CPU 131%1.3コア)を24時間
ワンセグ   復号も変換も要らない

常時つけっぱなしのサーバで,この差は毎日効き続ける.B-CAS カードリーダーも回さずに済む.通信量では負けても,追う価値はここにあった.

>>> まず,前提を測り直した

物理チャンネルを丸ごと採取して中身を見た.

program_id=3472  h264 320x180 15fps    ★ワンセグは «実在する»
PID 0x0151 映像  scr   0.0%   ★平文
PID 0x0152 音声  scr   0.0%   ★平文
PID 0x0111 フルセグ映像  scr 100.0%   暗号化

ワンセグの映像も音声も暗号化されていない.B-CAS は本当に要らない.ここは後編の見立てが正しかった.

ただし通信量の見積もりは間違っていた.後編に「約 88MB/h」と書いたが,あれは測っていない数字だった.実測はこうだ.

ワンセグ実測                      131 MB/h
mini(フルセグを 320x176 へ変換)  108 MB/h

mini の方が軽い.音声がステレオ AAC で,映像も思ったより持っている.測らずに書いた数字が,記事になって独り歩きしていた.

>>> 5つの罠

動くまでに5つ踏んだ.どれも「これで通るはず」と思った直後に外れた.

① mirakc の id と serviceId は «別物»
     id=3270703096  ≠  serviceId=3096
② ワンセグはサービス一覧に «載っていない» が «名指しすれば出す»
     ✕ /api/services/<sid>/stream                    → 404/api/channels/GR/<ch>/services/<sid>/stream
③ 物理ch丸ごとでは «音声の素性が決まらない»
④ -map 0:p:<SID> «だけ» では落ちる(データ放送が同居)
      :v:0 / :a:0 まで明示する
⑤ 音声の素通しは «そもそも無理»
     ワンセグ音声は AAC-LATM,HLSADTS を要る
     → -c:a aac で焼き直す(映像は素通しのまま)

③が特に厄介だった.16Mbps の中でワンセグ音声はわずか 45kbps.ffmpeg の探索予算をフルセグ勢が食い尽くし,AAC の素性が決まらないまま HLS のヘッダを書こうとして失敗する.

[hls] sample rate not set
[out#0/hls] Could not write header (incorrect codec parameters ?)

探索を厚くしても直らなかった-probesize 60M まで試した).しかも通る時もある.運任せの挙動がいちばん質が悪い.

答えは「探索を増やす」ではなく「探させるものを減らす」だった.mirakc にワンセグだけ切り出させると 16000kbps が 300kbps になり,一発で決まった.

>>> 結果
CPU     通信量      中身
oneseg     19%    131 MB/h    B-CAS不要・映像素通し・音声のみ焼直し
mini      131%    108 MB/h    フルセグを 320x176 へ変換
low       131%    337 MB/h    既定

温度  60℃ → 53

CPU は 1/7 になった.通信量では mini に負けるが,狙いは電力だったので目的は達している.

>>> ワンセグが「元からあった穴」を暴いた

動かしてみると,今度はブラウザ側が固まった.

mediaSourceRequiresReset / bufferAppendError

原因は2つあって,どちらもワンセグとは関係なく,ずっとそこにあったものだった.

hls.js の致命的エラーを記録するだけで,回復を一度も呼んでいなかった

掴み直す時に <video> を空にしていなかったhls.destroy() だけでは古い MediaSource が残る)

ワンセグは H.264 Constrained Baseline,フルセグは High.MediaSource は途中でコーデックを変えられないので,ここで初めて表に出た.フルセグ同士の切り替えでは何も起きなかったから,気づきようがなかった.

新しい段を足すと,それまでの前提が試される.

>> 測り方のほうが壊れていた

この日いちばん時間を溶かしたのは,本体ではなく測定の道具だった.6つ数えた.

① 結果を trap で消した(証拠ごと片付けた)
② pgrep が外れても «0KB» と表示
     →「来ていない」と「測れていない」が区別できない
③ ラベルに "/var/tmp" と書き,それがリダイレクト先のパスになって
     → 4本とも «起動すらしていなかった»
④ head -8 / head -5 で «肝心の行» を切り落とした
⑤ timeout は bash -c "a | b" の bash にしか SIGTERM を送らない
     → ffmpeg は生き残り,測った «後» に16本書いた(0本と誤認)
⑥ 比較の «土台» が動いていないかを確かめずに4条件を並べた

⑤が特に効いた.「パイプでは通らない」と何度も結論しかけたが,実際には通っていて,自分が早く数えていただけだった.ログの行数が測定後に増えているのを見て,ようやく気づいた.

測定を組んだら,まず測定自体が動いているかを確かめる.当たり前のことだが,原因を追っている最中はつい「道具は正しい」と仮定してしまう.

>> 「いつも出ている」は「今も無害」の証明にならない

もう一つ,同じ日に3度同じ過ちをした.

Consider increasing 'analyzeduration' / 'probesize'   → ③の答えだった
non-existing PPS 0 referenced                         → SPS 複製落としの答えだった
AAC bitstream not in ADTS format                      → ⑤の答えだった

3つとも,除外リストに入れて grep で捨てていた行だ.フルセグでは無害な警告として毎回出ていたので,ノイズだと思い込んでいた.

でも条件が変われば,同じ行の意味も変わる.「フルセグで無害だった」ことは「ワンセグでも無害」の証明ではない.ffmpeg は最初から答えを言っていて,こちらが耳を塞いでいた.

>> まとめ

完結編は「1つしかないもの」に3回ぶつかった話だった.

  • チューナーは1本.未検証の機能をボタンにした結果,本番を2回止めた
  • B-CAS カードも1枚.調べるために立てた2本目が,調べたい対象を壊した
  • 優先度で負けると永久に順番が来ない.EPG は構造的に空振りし続けていた

共通しているのは,「読むだけだから害は無いだろう」という油断だった.読むだけの操作でも,資源を掴む以上は排他が起きる.

技術的な学びとしてはこのあたりになる.

  • Lock を信じない.信号が無くても立ってくる.判定は C/N と Quality で
  • -g はフレーム数で効く.fps を変えると,GOP の «秒数» が勝手に変わる
  • 完了は「本人が終わったと言う」まで待つ.数の増減で推測しない
  • 監視は「計画された停止」を知る必要がある.閾値を緩めるのは筋が悪い
  • 通知の失敗を「送った」ことにしない.壊れているときにだけ壊れる
  • もっともらしい仮説ほど,先に数える
  • 測る前に,測定自体が動いているかを確かめる
  • 「いつも出ている警告」は「今も無害」の証明にならない
  • 探索が足りないときは,増やすより「探させるものを減らす」
  • 新しい段を足すと,それまでの前提が試される

引き出しに転がっていた Pi 3B が,8局のテレビになった.外出先からも見られるし,番組表も出る.4日以上,一度も落ちずに動いている.

思ったより長い道のりだったが,踏んだものはだいたい「1つしかないもの」だったというのが,終わってみての感想だ.