コンテンツにスキップ

チューナーエージェント

電波を受けるところ。機材に触るのはここだけです。 なぜこの構成なのかと、 bun から .NET に移るまでに測ったことを書きます。

使えるチューナー・B-CASカード・困ったときは tuners.md、Mac と Windows は install-mac.md と install-windows.md。 番組表・スキャン・持ち物は data.md、ファイルは app.md、環境変数は config.md。

次の3つは denpa からは触れず、チューナーを挿した機械のエージェントでしか扱えません。

  • チューナーデバイス — /dev/dvb/* と USB の機材に触れるのはあちらだけ
  • B-CASカード — USB のリーダーも px4d の内蔵リーダーも、あちらが直接扱います (下記)
  • 選局そのもの — DVB はデバイスを ioctl で直接操作します (下記)。PX-Q3U4 などは同梱の px4-userland、 PX-S1UD は (カーネルが使っていなければ) 同梱の siano-userland に USB を扱わせます (下記)

持っている仕事は3つ。

  • 選局して TS を流す
  • 優先度つきの取り合い (低いものを切って高いものを通す)
  • どのチャンネルが使えるかの控え (スキャンの結果を預かって配る。中身は作らない)

TS は1バイトも解釈しません。 NIT も SDT も EIT も読まず、src/lib/ts にも 依存しません。総当たりのスキャンでも、言われた物理チャンネルを順に開いて流すだけです。

局も番組表も時刻も、覚えているのは denpa だけです。同じものを2箇所が持つと 「あちらがまだ知らない局は、こちらから増やせない」縛りが必ず出ます。

全部持っているので、こちらで決められます。

  • 番組表は、チューナーが空いているだけ並列に集める。急がせるのに再起動は要らない (data.md)
  • 録画中でもスキャンできる
  • 優先度は素直な数のまま。下限も特別な値も無い (下記)

優先度は大きいほうが勝ちで、下限はありません。番号は denpa が決めます (config.priority)。

値
録画 10 放送は二度と来ないので、ここだけは譲らせない
ライブ視聴 9 画面の前で人が待っている。録画にだけ譲る (stream.md)
番組表 (今すぐ) 8 人が押して待っている番組表集め。録画とライブ以外からは取り上げる
チャンネルスキャン 5 人が押して待っている。番組表には譲らせるが、録画中のチューナーは使わない
番組表 3
局ロゴ 1 ロゴが出なくても番組表は読めるが、逆は成り立たない

どのチューナーを使うかは次の節の約束どおりです (同じチャンネルなら共有、なければ選ぶ順 rank がいちばん前の組)。 組の中 (受けられる種別がまったく同じチューナー) では、同じチャンネルの余韻 → もう合っているチューナー → ほかの余韻 → 空いているチューナー → いちばん優先度の低い相手のチューナー、の順です。余韻 (誰も読まなくなってから 5秒残る選局) を空いているチューナーより先に使い回すのは、1人の視聴で2台塞がないためです。 代わりに、直前のチャンネルへ戻るのも選局し直しになります (実測 802ms。同期を待つ長さはどちらでも同じ)。

余韻の使い回しは取りこぼすことがあります。切った直後に次をリクエストすると、切断に気付く前に要求が届き、 Sinks がまだ 0 でないためです。そのときは同じ組の別のチューナーか、空いていなければ次の順位のチューナーに入ります。 映らなくはなりませんが、地上波専用が1台だけの機材でライブのチャンネルを素早く替えると、兼用のチューナーに入ることがあります。

どのチューナーを使うかを決めるのはエージェントで、denpa は名指ししません。 ライブ・番組表集め・ スキャンも同じチューナーを使うので、denpa が「このチューナーで」と決めると食い違います。

代わりに denpa は、エージェントがこの約束どおりに使う前提で予約を割り振ります (src/lib/server/conflict.ts の assign。ルールの下見 (rule-preview.ts) の contentions も同じ)。 約束が崩れると、予約表では通っているのに実行時にチューナーが使えなくなります (#668)。

エージェントが守ること (agent/conformance.test.ts が押さえる):

  1. 選ぶ順を公開する。 GET /denpa/tuners の各チューナーに rank (0 から。小さいほど先に使う)。 無効なチューナーは null。同じ rank のチューナーは、受けられる種別がまったく同じでなければならない。
  2. 自分と同じか優先度の高い利用者が同じ物理チャンネルを読んでいなければ、使ってよいチューナーのうち、その種別を 受けられて rank がいちばん小さいチューナーを使う。 使ってよいチューナーは、空いているチューナー (選局が無いか、利用者が いなくなって余韻で残っているだけ) と、自分より優先度の低い利用者しかいないチューナー (要れば優先度の低い利用者を切断して取り上げる)。 選んだチューナーが同じチャンネルの選局なら、選局し直さずにその選局を共有する (優先度の低い利用者も残る)。 同じ rank のチューナーはどれを使ってもよい。
  3. 1回の選局 (/denpa/stream) の利用者は、終わるまで同じチューナーにいる (途中で別のチューナーへ移さない)。 自分と同じか優先度の高い利用者が同じ物理チャンネルを読んでいれば、どのチューナーであってもその選局を共有する。
  4. after=<利用者の use> (何度でも書ける) に挙げた利用者は、もう離れたものとして 2・3 を決める (引き継ぎ)。 選んだチューナーにその利用者がいれば、その利用者を正常に終わらせて (取り上げたのではない) 同じチューナーで選局し直す (同じチャンネルなら選局し直さずにそのまま共有する)。 選ばなかったチューナーの利用者と、3 で共有したときに同じチューナーにいた利用者には触らない (denpa が先に切っている)。その利用者がもういない・そのチューナーが新しい種別を受けられないなら、 ふつうに選ぶのと同じ。選局し直しに失敗したら新しい利用者には 500 を返す (前の利用者はもう終わっている)。

順の決め方はエージェントのものです。 いまは「受けられる種別の少ないチューナーほど前。数が同じなら その種別の組が定義に先に出てくるほう」(TunerPool.Ranks)。PX-MLT のように地上波も衛星も受ける 1台を地上波で塞ぐと、地上波専用が空いていても衛星が録れなくなるためです。決め方を変えても rank を読む denpa は直さなくて済みます。denpa のほうに「種別の少ないチューナーから」というコードは ありません。

3で優先度の低い利用者の選局を共有しないわけも同じです。 番組表集めが兼用のチューナーで同じチャンネルを読んでいるとき、 録画がその選局を共有すると、地上波専用が空いていても兼用のチューナーが塞がります。優先度の低い利用者しかいない選局は、 そのチューナーが順位で先頭の組のときだけ共有します。

4 (引き継ぎ) が要るわけ。 denpa は録画を止めて、同じ定期チェックで次の録画を始めます。止めたこと (HTTP の切断) がエージェントに届くのは非同期なので、次の選局が先に着くと前のチューナーがまだ塞がって見え、次の順位のチューナーに入って いました。denpa の割り振りは、離すのと取るのが同じ時刻なら離すほうを先に数えています。after で渡せば、 エージェントの選び方もそれと同じになります。どれを組にするかは denpa は決めず、その定期チェックで止めた録画を 全部渡します。どのチューナーに入るかは順位で決まるので、組を決める決まりを denpa に持たなくて済みます。 録画が同じチャンネルで続くときは、マージン (既定 10秒/15秒) で前後が重なるので、後ろは前の選局を共有するだけです (引き継ぎは要らず、前が離れても後ろはそのチューナーに残る)。マージンを 0 にしていて重ならないときは、同じ定期チェックでの 引き継ぎになり、前のチューナーが順位で先頭の組ならそのまま共有します。

古いエージェントは after を知りません。 読み飛ばしてふつうに選ぶので、引き継ぎが無かった頃と同じに 振る舞います (止めたのが届く前に次が着くと、次の順位のチューナーに入ることがある)。

2の「優先度の低い相手しかいないチューナー」が要るわけ。 番組表集めが地上波専用のチューナーを使っている最中に地上波の 録画が始まると、空いている兼用のチューナーへ逃げたくなります。逃げると、そのあとの衛星の録画が兼用のチューナーを 取れません。denpa は予約しか知らない (ライブや番組表集めの予定は読めない) ので、それらが前のチューナーに いたかどうかで録画の入るチューナーが変わると、予約表で通した組が録れなくなります。優先度の低い相手からチューナーを取り上げて順どおりに 入れば、録画の入るチューナーは録画どうしだけで決まります。 代わりに、ライブや番組表集めはもっと広いチューナーが 空いていても録画に譲ります。ライブは同じ局をリクエストし直し、入れなければチューナーを取り上げた録画の追っかけに切り替えます (どの録画かは denpa が自分の利用者がいたチューナーから読む。stream.md)。

ずれたら気づく仕掛け。 同じ場面を両方のテストに通しています (tests/fake/tuner-scenes.ts)。 適合テストは本物のエージェントに開かせて入ったチューナーの rank を見て、conflict.test.ts は denpa の なぞり方で同じ答えになるかを見ます。約束を変えるときは、この節・conflict.ts の頭・ TunerPool.Pick を揃えて直してください。

denpa 側が持つこと (app.md):

  • どの予約を録るか・どこまで録るか (優先度・入るところまで録る・マージン)。開始順になぞって、 どの予約も最後まで1台のチューナーを使えるかを見る。録画中の予約は、users の rec <録画ID> から いまいるチューナーを読んで、なぞり始めの状態にする
  • エージェントに rank が無い (古い版) か、繋がらないときは台数不明として何も競合にしない

限界。

  • 同じ優先度の利用者どうしはチューナーを取り合いません (録画どうしは denpa が割り振るので、ぶつからないようにしてある)
  • 定期チェックの外で止めた録画 (予約の取り消しなど) は引き継ぎに入りません。その直後に次の録画が始まると、 止めたことが届く前なら前のチューナーがまだ塞がって見えて、次の順位のチューナーに入ることがあります
  • 録画が途中で切れて取り直したときは、新しい1回の選局です。別のチューナーに入ることがあります
  • 同じ時刻に始まる録画の順はばらばらです。専用と兼用が入れ子 (地上波専用 ⊂ 兼用 ⊃ 衛星専用) の ふつうの組み方なら、どの順でも入る組は変わりません

チューナーは数台しかありません。 番組表・局ロゴ・録画・視聴は、どれも 物理チャンネルを開くところは同じなので、同じチャンネルなら1台を全員で共有します。

エージェントは1回の選局 (Lease) に利用者 (Sink) を何人でもぶら下げます。 B25 の復号は選局につき1回で、あとは人数ぶん配るだけです (MULTI2 は TS 全部に 掛かるので、利用者ごとに復号すると台無しになります)。

誰が 優先度 開き方 上限
録画 10 必ず自分で開く。ほかが共有する側 番組の終わりまで
番組表 3 (押されている間は 8) 開く。同じチャンネルが開いていれば只 1チャンネル10分
局ロゴ (PNG) 1 開いている選局を共有するだけ (logo.ride) 3分
〃 (自分から取得するとき) 1 開く 地上波6分 / 衛星20分
チャンネルスキャン 5 開く。同じチャンネルを録画が開いていれば共有する (約束3) 1チャンネル30秒

CM検出用のロゴはチューナーを使いません。録れたものから覚えます (logo.md)。

いちばん効くのは録画です。 物理チャンネルを丸ごと開いているので、EIT も只で 読めます (data.md)。

ロゴは録画のストリームからは取得しません。 衛星のカルーセルは数百KBあり、録画の ストリームに混ぜたくないためです。tuners の通知を合図に、同じチャンネルの選局を 別の接続で共有します。増えるのはチューナーではなく、TS がもう一度 HTTP を渡るぶんだけです。

番組表は「開いているものを先に」しません。 薄い局から先に行くほうが大事で (入れた直後は全部空)、順番の理由が2つあるとどちらも中途半端になります。同じ チャンネルが開いていれば結局ただで共有できるので、損はありません。

選局は、利用者が切るまで終わらない

Section titled “選局は、利用者が切るまで終わらない”

向こうから終わったなら、それは失敗です。 チューナーを取り上げられた・子プロセス (siano-ts) が落ちた・px4d が TS を止めた・デバイスが黙った、のどれかで、録り終えたのではありません。

エージェントは取り上げた相手のストリームを接続ごと壊して畳みます (http.Abort())。 きれいに閉じると正常終了として届き、尻切れの録画が「録れた」ことになるためです。

壊すのは、送り始めたあとだけです。 1バイトも送る前に落ちたら、理由を付けて 500 で 返します (接続を壊すと、記録に socket connection was closed unexpectedly しか残らない)。

それでも約束は理由ではなく「終わったという事実」に置きます。 届く保証があるのは 終わったことだけです (電源やネットワークで切れることもある)。denpa は EOF も例外も 録り終えたと読まず、どちらも取り直して終了時刻まで録り続けます (recorder.ts)。

例外は引き継ぎ (after) だけです。 次の選局が「この利用者はもう離れた」と言って同じチューナーを引き継ぐと、 エージェントはその利用者を正常に閉じます (選ぶ順の約束 の4)。渡すのは denpa が止めた録画 だけなので、そちらはもう取り直しません (止める側の接続を先に閉じている)。 このやり方なら、エージェントを何語で書いても成り立ちます。

リングバッファが溢れても畳まない

Section titled “リングバッファが溢れても畳まない”

read の EOVERFLOW は「読むのが追いつかず、ドライバが捨てた」で、読み続ければ 続きが来ます (終わりとして扱っていた頃は、番組表を4本まとめて開いた直後に1〜2本が落ちていた)。

溢れるのは dvr のリングバッファです。 全PIDを DMX_OUT_TS_TAP で流すので、demux の フィルタではなく dvr に積まれます。そこで dvr にも DMX_SET_BUFFER_SIZE で 8MB を言います (カーネルの既定は ≒1.8MB で地上波 0.9 秒ぶん。8MB なら 3.5 秒ぶん、4台でも 32MB)。断られても選局は落としません。

溢れの多くは、局を変えている間に積もったぶんでした。 デバイスは選局を跨いで 開きっぱなしなので、局を変えている間は誰も読まず、読み始めの1回が必ず溢れになります。 読み始めで測り直し (DeviceStream.Begin)、選局前のぶんは「選局前にリングバッファが N 回溢れていました」と別に 書きます (測り直すと 50分で1回・空いた時間 0.0秒)。

溢れたときは読む側がどれだけ空いていたかも出します。リングバッファは 3.5 秒ぶんなので、

空いた時間 読み
1秒あたりで頭打ち 広げたはずの溜めが効いていない (既定の 1.8MB のまま)
3.5秒を超える 読む側が止まる理由が本題。疑わしいのは GC・CPU の取り合い。B25 の復号は別のスレッドで、間の 64MB のキューが埋まったときだけ読み出しを待たせる (そのときは「キューが埋まった」と出る)

溢れた記録には「その間にいた利用者」を名乗らせます (use。下記)。報告は1分に 1回なので、報告の瞬間の利用者だけを見ると、溢れた本人が抜けたあとで「利用者なし」になります。

前の利用者が抜けるまで、次の選局を始めない

Section titled “前の利用者が抜けるまで、次の選局を始めない”

デバイスはチューナー1台につき1つで、選局を跨いで開きっぱなしです。前の選局の読み出しが 回っているうちに次を始めると、同じデバイスを2つの読み出しで取り合い、前の局のパケットが 次の選局に混ざります (デコーダは選局ごとに作ります)。

  • 読む側から抜ける手段を持たせます (DeviceStream.Read)。無いと、電波が 来ない間 read は 0.2秒ごとに掛け直すだけで戻らず、取り上げられた側が 残り続けます。待つのは取り合いの錠を放してから (握ったまま待つと、どのチューナーも開けない)
  • 復号できなくなったデコーダは作り直します。 途中で投げたものは解析が半端なので、 reset して使い回すと壊れたまま次の選局へ持ち越します。デバイスは使ったまま (取り直すと Device or resource busy)
  • 誰も読まなくなってから5秒は使ったままにします。 連続した番組を録るときに、 閉じきっていないデバイスを開きに行って弾かれないように
  • 偽の選局 (FAKE_TUNE、適合テスト用) はプロセスグループごと終わらせます。 sh -c 越しに起こすので、中身が生き残って次の選局とぶつからないように。 イメージにはシェルが無いので、FAKE_TUNE はホストで組んだ実行ファイルで使います (適合テストもそう)

既定は /config/tuners.json と /config/channels.json (TUNERS_FILE / CHANNELS_FILE で変えられる。Mac と Windows のインストーラは ~/denpa/config/)。どちらも無くて構いません (機材は自分で見つけ、 チャンネルはスキャンするまで空。ただし衛星を受けられる機材があれば、BS / CS の局が 無いうちに denpa が標準の表を預けてくる。src/lib/server/channel-seed.ts)。

ファイル 中身
tuners.json 繋いである機材。denpa の画面から書き換えます (PUT /denpa/tuners)
channels.json スキャンで見つかったチャンネルと局 (PUT /denpa/channels)

どちらも denpa が書き、どちらも JSON です。 人が手で書かないので、コメントを守る テキスト操作や自前の YAML 読みは要りません (丸ごと書き直すほうが壊れにくい)。

分けてあるのは、片方を書き換えるときにもう片方を巻き添えにしないためです。 同居させると、スキャンのたびに機材の定義が飛びかねません。

選局コマンドは画面から渡させない

Section titled “選局コマンドは画面から渡させない”

設定に持つのはデバイスと種別と LNB だけで、選局の仕方はエージェントが決めます。 画面から自由な文字列を受け取ると、denpa に入れた人がチューナー側で好きな コマンドを走らせられます (しかもあちらは機材に触れる。Helm では privileged)。command の欄は外しました。 tuners.json に残っていても読まずに捨て、次に画面から保存すると消えます。外の コマンドを起こすのは適合テストの FAKE_TUNE (環境変数) だけです。

チューナーは書かなくても動きます (DeviceProbe.Detect)。定義が無ければ刺さって いるものを見つけ、地上波か衛星かまで判別します。

機材 聞き方
DVB /dev/dvb/* を開いて DTV_ENUM_DELSYS で受けられる方式を聞く (実機の PT3 で adapter0/2 が ISDB-S、adapter1/3 が ISDB-T)。sysfs にはこれを答える項目が無い
px4-userland px4d --list-json (0.1.9 以上が要る。下記)
siano-userland siano-ts --list (下記)
asicen-userland /sys/bus/usb/devices の USB ID を読む (試験的。配布物が無ければ挙げない。下記)

書いてあれば書いたほうが勝ちます (LNB や1台だけ止めるのは人にしか決められない)。

チューナーを開くときは用途を名乗ります (use)。無いと、誰が使っているのか 画面から読めません。rec 12 logo GR/T16 epg BS15_0 scan T21 と短く渡し、 読める言葉には設定の「受信」の「チューナー」が直します (番組名は DB から引き直す)。

.NET で書き直すまで (経緯と実測)

Section titled “.NET で書き直すまで (経緯と実測)”

エージェントは .NET です (agent/)。Native AOT で実行ファイル1個、依存はありません。 bun 版は消し、適合テスト (agent/conformance.test.ts) がいまも本物を起こして HTTP API に当てています。

recisdb も外しました。 選局のたびに起こすのをやめ、自分でデバイスを開いて 復号します (Tuning.cs / ChannelTable.cs / B25.cs)。

実機 (PT3・地上波) で denpa と同じ経路で録り、掛かったままのパケットは最初の ECM で鍵が決まるまでの 0.004% だけでした。カードが開けなくても選局は続き、掛かったまま録れて設定の「チューナー」に理由が出ます (tuners.md)。

デバイスを開いたままチャンネルを変える

Section titled “デバイスを開いたままチャンネルを変える”

PT3 はカーネルの earth_pt3 が標準の Linux DVB (v5) として出していて (/dev/dvb/adapterN/{frontend0,demux0,dvr0})、開いたまま FE_SET_PROPERTY で 選局し直せます。1回1チャンネルの recisdb をやめて開けっ放しにすると、

  1. 使い手が入れ替わる隙間が無くなる。 次が開くまでデバイスが宙に浮くことも、 recisdb が利用者の去ったあと生き残ってチューナーをふさぐ事故も無くなります
  2. 1回の選局を何人でも読める (共有の決まり)
  3. プロセスの立ち上げとデバイスを開く時間が消える

同期そのものは短くなりません (PT3 の実測で衛星は 190〜230 ms、地上波は 370 ms ほど)。tc90522 は TSID を書き換えても選局の手順を丸ごとやり直すので、同じ周波数の中でも跨いでも差がありません。

既製の常駐 (tvheadend など) は番組表も録画も持っているので、乗せると同じものを 2箇所が持つ構成に戻ります。欲しいのは「開いたまま選局して、TSを配る」薄い層だけです。

チャンネル名から周波数を出す表です (ChannelTable.cs)。数字は recisdb から 取りました。 導き直すより、実機で選局できていたものと同じ数字にするほうが大事です。

  • 地上波 … 473142857 + (ch-13) × 6 MHz (Hz)
  • BS … 1049480 + 38360 × n (kHz。中継は 38.36 MHz 刻み)
  • CS … 1613000 + 40000 × n (kHz。40 MHz 刻み)

単位が2つあるのは DVB API の決まりです (地上波は Hz、衛星は kHz)。取り違えても ioctl は通り、同期しないだけなので気付きにくい。

衛星ではもう1つ TSID が要ります。denpa が言ってくるのは BS15_0 のような 相対番号なので、ここで直します。引く先は2つ。

  1. 自分の channels.json — スキャン結果には TSID が入っています。こちらが勝ちます
  2. 埋め込んだ表 — まだ1度もスキャンしていないときだけ

BS は再編があるので埋め込んだ表はいつか古くなります (実際 recisdb の表には、手元で 受かっている BS13_3 BS21_3 BS23_3 がありません)。スキャンした結果のほうが必ず新しい。

選局は取り合いの錠の外でやる

Section titled “選局は取り合いの錠の外でやる”

取り合いの錠を握ったまま選局すると、同期待ち (上限 5 秒) の間プールに触る処理が全部 止まります (/denpa/tuners も同じ錠を取る)。

錠の中ではチューナーを押さえるだけで、選局はその外 (TunerPool.Open)。押さえた印に 利用者を先に入れてから錠を放します。入れずに放すと、選局中のチューナーが「誰も読んでいない」 と見えて次の要求に取られます。選局し直す間の順番はチューナーごとの錠で守ります (別のチューナーは 待たない)。

使い方・設定・ファームウェアは tuner-px4.md。

使うのは2つ。 px4d が筐体 (USB 機能・受信機・内蔵カードリーダー) を所有する デーモンで、筐体1台につき1つ起こします。px4ctl は状態を聞きます。受信機は px4d の制御ソケット (control.sock) で直に取って選局し、TS も px4d から直に 受けます (Px4Tuner、px4-userland SPEC 6 節の portable IPC)。同梱の px4-ts (1回1チャンネルの CLI) は使いません。

機種のことは何も持ちません。 刺さっている筐体と各受信機が受けられる方式は px4d --list-json に聞きます (Px4Userland.Enclosures)。px4-userland 0.1.9 以上が要ります (それより前の px4d は知らないオプションとして断り、筐体が挙がりません。理由は記録に残ります)。 テキストの px4d --list は上流がいずれ廃止すると言っているので読みません。 Q3U4 なら8つで受信機ごとに地上波か衛星か決まっており、MLT5 系なら5つでどれも両方 受けられます。その違いは答えに入るだけなので、対応機種が増えても px4-userland を 上げれば追従します。一覧は筐体を使わないので、px4d を起こす前に顔ぶれが決まります。

  • --list-json の形式が違っても止まりません。 schema_version は無く、知らないキーは 無視する決まり (SPEC 4.6)。無いキー・型の違う値は既定で埋めて読み進め、版のずれを疑う旨を 1度だけ記録します。飛ばすのは筐体の番号・受信機の番号が読めないものだけ
  • 知らない方式は飛ばして続けます。 方式が増えても分かる受信機は使えるように。 飛ばしたことは記録に残します
  • 受信機と違う方式は、px4d に要求する前に断ります (Px4Tuner.Check)。 受信機を聞けていなければ px4d に任せます
  • 受信機は取ったまま選局し直します。 最初の選局で ACQUIRE → TUNE → START_STREAM → ATTACH_STREAM、2回目からは STOP_STREAM → TUNE → START_STREAM → ATTACH_STREAM (SPEC 6.3。px4-userland 0.1.6 から)。返すのはデバイスを閉じるとき (RELEASE) だけです。lease を返すと、最後の受信機なら px4d がチューナーの電源を落とし、 次の選局が電源投入と全チューナーの初期化からになって局替えが遅くなるため
  • TS は別のソケット (stream.sock) に TS_DATA の枠で来ます。 枠をほどいて エージェントの中の溜め (System.IO.Pipelines の Pipe、8MB = 地上波で 3.5 秒ぶん) に書き、その読む側を DeviceStream に載せるので (Px4Stream)、読む側の振る舞いは DVB と同じです (電波が来なくても畳める・ チューナーを取り上げられたら 200ms で抜ける)。枠の中身はソケットから溜めのメモリ (プールのもの) へ直に 受け、枠ごとの確保はしません。選局のたびに新しい溜めなので、前の局の TS は混ざりません。 OS の pipe は使わないので、深さも OS や CAP_SYS_RESOURCE に左右されません
  • 同期の判定は TUNE の答え。 px4d は同期するまで答えず、5 秒 (timeout_ms) で 諦めて TIMEOUT を返します。そのあと最初の TS が 3 秒以内に来れば成功。同期しなくても 受信機は取ったままなので、総当たりのスキャンで同期しないチャンネルが続いても電源は 入れ直しません。px4d のエラーは番号の名前と日本語を添えて画面に出ます (Px4Control.Reason。BUSY = 他が使っている、USB_IO、DISCONNECTED …)
  • 誰も読まなくなったら TS のストリームを畳みます。 溜めが埋まったまま 5 秒空かなければ こちらから stream.sock を閉じます (放っておくと px4d の溜めが溢れて SLOW_CONSUMER になり、その通知も読まないソケットの奥に詰まる)。受信機は 取ったままで、次に同じチャンネルを要求されたら同じ lease で選局し直します (ITuneDevice.Tuned を TunerPool が見る。DVB は常に true)
  • px4d との接続が切れていたら取り直します。 px4d が落ちて起こし直された・lease が 無くなった、は次の選局で気づき、その場で1度だけ HELLO → ACQUIRE からやり直します
  • 内蔵カードリーダーも px4d が持っています。 control socket に APDU を 投げて読みます (Px4Card.cs、px4-userland SPEC 6 節の portable IPC)。同梱の pcscd 用 IFD ハンドラは使いません (筐体が増えるたびに pcscd を入れ直すことになる。#195)。 px4d が起きたあと、次にカードが要ったときに見つけます
  • 起動時に顔ぶれの筐体ぶん px4d を起こします (UserlandDaemon.Prepare に Px4Daemon.For を渡す)。 ファームウェアを流し込むので ready まで数秒かかります。起こせなくても 他のチューナー (PT3 など) は動き、選局のときにもう一度試します

使い方・設定は tuner-px-s1ud.md。

  • 機材は siano-ts --list に聞きます (px4ctl list と同じ key=value の形式)。 USB ID の表は持たず、ポートと受けられる方式も siano-ts の答えを使います。 ポートの分からない機材 (port=-) と対応外の機材 (rejected) は、理由を記録に残して 使いません。カーネルが使っている機材は sysfs (インターフェースの driver) で見て 並べません。--list はそこを教えず、並べると同じ機材が DVB と siano の2つになるため
  • siano-ts には機材をポートで渡します (siano-ts --device 1-2.3、0.1.8 から)。 番号 (--device N) は libusb が並べた順で、カーネルが使っている S1UD も入って ずれるので使いません。起動後に抜けた・挿し替えた・smsusb が使った、は siano-ts が終了コードで答えるので (3 = 見当たらない、4 = 使われている、 10 = ファームウェア)、手当てを添えて記録と選局の失敗に出します (SianoUserland.Hint)
  • siano-ts は起こしたまま選局し直します (--control)。起こすのは最初の選局だけで、 以降は標準入力に tune <Hz> を1行書き、stderr の tuned <Hz> を待ちます (駄目なら control: tune failed: …。どちらでも siano-ts は生きたまま)。USB を 取り直さないので、番組表の総当たりのように次々変えるときに速く、TS は同じ 標準出力に流れ続けるので読み出すパイプも1つを使い回します (DVB と同じ)。 選局の上限は 15 秒 (siano-ts 自身が同期を 10 秒待つ。初回はファームウェアの流し込みも入る)。 siano-ts が死んでいたら (USB が抜けた等) 次の選局で起こし直します
  • 選局し直す間は標準出力を読んで捨てます。 siano-ts は「TS を書く → 標準入力を見る」を 1つのループで回していて、pipe が埋まると書くところで止まり tune を読みに来ません。 誰も読まない間 (前の利用者が抜けてから次の選局まで) に pipe は埋まるので、読まずに 選局を指示しても進まない。読んで捨てれば前のチャンネルの残りも消えます (siano-ts は同期を待つ間 何も書かないので、tuned の時点で前の TS は読み尽くしてある)。tuned の直後を 少し捨てることがありますが、録画の頭が数ミリ秒ずれるだけです
  • 誰も読まなくても siano-ts は死にません。 書くところで待ち、中の溜めから 溢れたぶんは捨てます。選局は生きています (DVB のリングバッファが溢れたときと同じ)

いまの様子・設定・ファームウェアは tuners.md。

  • IPC は px4d と同じ。 asicen-userland は px4-userland の portable IPC を取り込み、枠の頭 (ASCN) と ソケットの置き場 (<ランタイム>/asicen-userland/<instance>/) だけ変えています。受信機の取得と返却・ 選局し直し・TS・内蔵カードは Px4Tuner / Px4Card をそのまま使い、違いは Px4Wire に閉じ込めました。 同梱の asicen-ts (1回1チャンネルの CLI。チャンネルの書き方は px4-ts と同じ) は使いません (px4-ts を使わないのと同じ理由)。上流の asicend --mock と話すテストがあります (AsicenMockTests。 CI では上流の決めたコミットを組んで回す agent-asicen-mock。必須にはしない)
  • 機材は sysfs で見つけます (AsicenUserland.Scan)。上流の配布物 (bin/ の asicend・asicenctl・asicen-ts) には 機材を並べる道具が無く (asicen-probe はファームウェアの流し込みにだけ使う。asicend --list も実機ではまだ並べない)、asicend には USB のアドレスとポートを2組とも渡す決まり (--primary が .1、--sibling が .2) なので、 /sys/bus/usb/devices の USB ID とアドレスを読みます。見る USB ID は上流の asicend が受け付けるもの (0b06:0005、ファームウェア待ちの 1738:5211 / 1738:5216) だけ。.1 がファームウェア待ちの筐体は使いません (.2 は待ちのままでも asicend が受け付ける)。上流が一覧を返す API を出したら、そちらに聞くよう変えます
  • asicend は筐体1台につき1つ、起動時に起こします (AsicenDaemon)。アドレスは挿し直すたびに変わるので 起こす直前に読み直し、ready は asicenctl list が通ることで見ます (status はカードの様子も要る)。 受信機も asicenctl list に聞きます (形式は px4ctl list と同じ)
  • カードは asicend の内蔵リーダーを、px4d と同じ IPC で読みます (AsicenUserland.Cards)。上流が用意する pcscd 用の libifd-asicen.so (recisdb で復号するための部品) は使いません。B25 は今までどおりエージェントが復号します (下記)

B25 の復号もカードリーダーも自前

Section titled “B25 の復号もカードリーダーも自前”

libaribb25 も pcscd も使いません (Linux。Mac と Windows は OS の PC/SC を使う)。ECM を取り出して MULTI2 で復号する (B25.cs / Multi2.cs)、B-CAS へのコマンド (BCas.cs)、カードリーダーとのやり取り (Ccid.cs / T1.cs / UsbFs.cs / Pcsc.cs / Px4Card.cs) を全部エージェントが持ちます。

Descrambler (B25.cs) PAT → PMT → CA 記述子で ES と ECM を結び、鍵で MULTI2 を復号する
│ ECM
IKeySource 手元のカード (LocalCard → BCas) か、別の拠点 (RemoteCard)
│ APDU
ICardLink USB のリーダーを usbfs で直に (Ccid。Linux) か OS の PC/SC 越し (PcscLink。Mac・Windows) / px4d・asicend の内蔵リーダー (Px4Card・asicen)

libaribb25 はスレッドセーフでなく、カードに繋ぎ直さず、配布物も OS と arch ごとだったのでやめました。 いまはデコーダを選局ごとに作り、リーダーが切れたら探し直して INT からやり直します (BCas.cs)。

挙動は libaribb25 と揃えてあります。 カードに渡す ECM のバイト列 (節の頭 8 バイトと CRC を除いた中身)、鍵の読み方、画面に出るカードの番号は、libaribb25 と答え合わせを しました。MULTI2 の答えと、PMT 2つ・ES ごとの CA・ECM の変化・アダプテーション・ ゴミを混ぜた TS を両方に通すと、カードに渡った ECM と出てきたパケットがバイト単位で 一致します (単体テストにはその答えだけを入れてある)。拠点間で鍵をやり取りする仕組みも 同じなので、前の版のエージェントとも話せます。

復号する処理は libaribb25 と同じく、ストリームの順に同期で鍵を貰います。 届いた順に PAT・PMT・ECM を読み、ECM の中身が変わったらその場でカードに聞き、以降のパケットを その鍵で復号します。待つ間は次のパケットへ進まないので、鍵とストリームの位置が食い違いません。 貰えなかったら鍵を捨てて掛かったまま流し (古い鍵で化けたものを「復号できた」として 流さない)、同じ中身は 2 秒ぶん流れてから聞き直します。録画の頭は PAT・PMT と すべての ECM の最初の答えが揃うまで (上限 8MB) 溜め、最初の ECM を挟んで続いている 偶奇の並びだけを最初の鍵で復号します (libaribb25 は溜めた頭を丸ごと復号するので、 その前の並びが化けていた)。

読み出しとは別のスレッドで復号し、間に 64MB のキューを置きます (TunerPool.cs)。 カードや配り役が固まると1回の問い合わせに何秒もかかり、読み出しが待つとデバイスの溜め (3.5 秒ぶん) が溢れて録画が欠けます。欠けたものは後から復号しても戻りません。復号する側が 待つ長さには上限があり (CCID は 5 秒、配り役は 10 秒)、64MB は地上波で 25 秒ぶん なので丸ごと呑めます。繋ぎ直しに失敗したら 10 秒は探し直さず、同じ理由で断ります (いくつものストリームが順番にリーダーを開き直さない)。契約の無い局 (同じ TS に入っている有料局) は「復号できなかった」に数えないので、後からの復号がそれで失敗扱いになりません。

速さ。 MULTI2 を Vector<uint> で並べて回し、Native AOT で1コアあたり 約 400 MB/s (Ryzen 5 5600X)。地上波1チャンネルは 2.5 MB/s 弱なので桁で余ります。

リーダーの扱いと、カードと話せているかの確かめ方は tuners.md。

復号するのは選局ごとに1回です (共有の決まり)。番組表とロゴには カードは要りません。 PSI/SI (EIT・SDT・NIT・CDT・DSM-CC) はスクランブルされて いないので、カードが死んでいても番組表は埋まります。

カードは1枚でいい。鍵を配る API を入れました

Section titled “カードは1枚でいい。鍵を配る API を入れました”

想定しているのは「1台にチューナーを4つ挿す」ではなく、拠点ごとにエージェントとチューナーが あり、それが同じ世帯の中に何箇所かある構成です。するとカードは1箇所にしかありません。

そこでカードごと持っていく代わりに、ECM を投げて鍵を貰います (CardShare.cs)。

拠点B (カード無し) 拠点A (カードあり)
選局 → TS ─┬─ MULTI2 で復号する B-CASカード
└─ ECM 200B ──────────→ ECM (0x34)
鍵 16B ←──────────
  • ネットワークを通るのは鍵のやり取りだけ。 ECM は 200 バイトほど、返るのは 16 バイト
  • 重いほうは動かしません。 MULTI2 は TS 全部に掛かるので各拠点の手元に残す
  • 同じ ECM には同じ鍵を返します (数秒だけ覚える)。世帯の中の何台かが 同じ局を観ていても、カードに聞くのは1回で済みます

鍵の出どころは IKeySource の1つのインターフェースなので、手元のカード (LocalCard) と 別の拠点 (RemoteCard) を差し替えるだけで、復号の本体は変わりません。

実機では、カードを持たない側として起こしたほうが、持っている側から鍵を貰って 88.2% → 0.1% まで復号できました。

denpa-agent --tune /dev/dvb/adapter1/frontend0 T27 --decode --card-url http://拠点A:25253

常駐させるときは環境変数 CARD_URL で指定します。指定しなければ手元の カードを読みます (そのほうが速く、配り役が落ちても録れる)。配る側は CARD_SHARE_PORT を書いたときだけ配ります (鍵を配るのは LAN の中だけ)。

配る側は denpa ではなくエージェントです。 理由は2つあります。リーダーは USB の 機材なので、機材に触るのはエージェントという境界を保ちたいこと。denpa (bun) から リーダーを触るにはネイティブの API を足すことになり、カードを読めるものを二重に持つ ことになること。カードだけ挿さっていてチューナーが無いマシンでも、エージェントを 1つ置けば配り役になります。

鍵を使えるのは、カードの持ち主と同じ世帯の機器だけです。 B-CAS カードの使用許諾契約で 使えるのは「お客様およびお客様と同一世帯の方」で、世帯の外の機器に鍵を配ると法的に危うくなります。 境目は LAN ではなく世帯です。配るのを LAN の中に絞っている (次の節) のはその近似で、 シェアハウスのように別の世帯が同じ LAN にいるなら、その人の機器には配らないでください。 法的な整理は Discussion #760 にあります。

世帯の外からカードを使い回せないように、3つ重ねています。どれも設定で変えられますが、 既定は安全側で、LAN の外へは広げられません (CardLan.cs・CardShareServer.cs)。

  1. 専用のポートで受けます。 鍵を配る API (/denpa/card/init・/denpa/card/ecm) は 普通の API (AGENT_PORT、25252) には無く、CARD_SHARE_PORT を書いたときだけそのポートで開きます。 選局の API を外に出す構成でも、鍵まで一緒に出ません。このポートは鍵を配る API にしか答えません (/denpa/card の状態は普通のポートのまま)
  2. 相手のアドレスを確かめます。 既定で答えるのはループバックと LAN の中のアドレスだけで、 ほかは 403 で断ります。IPv4 射影アドレス (::ffff:192.168.1.2) は IPv4 として見ます。 CGNAT (100.64.0.0/10) は入れません — Tailscale などは拠点の外を同じ網に見せるからです
  3. TTL (IPv6 は hop limit) を 1 にします。 ルーターを1つも越えません。配る側は待ち受けと 受けた接続の両方に付け (受けたあとにも付け直して読み直し、付かなければ何も答えずに切る)、 受け取る側も繋ぐ前に付けます (SYN から)。IPv4 と IPv6 は別々のソケットで待ち受けます — 1つで両方を受けると、IPv4 の相手には IPv6 の hop limit が効かないためです

ほかに、往復時間の上限を選べます (DTCP-IP と同じ考え。既定はオフ)。測るのは OS が TCP の ACK から測っている往復時間 (Linux は TCP_INFO、macOS は TCP_CONNECTION_INFO、Windows は SIO_TCP_INFO) で、問い合わせのたびに読みます。相手が遅らせて大きく見せることはできても、 小さく見せることはできません。上限を決めていて測れなければ断ります (macOS は 1ms 刻みなので、0 は 1ms 未満として通します)。

環境変数 既定 何
CARD_SHARE_PORT なし (配らない) 鍵を配るポート。配る拠点だけ書く。例は 25253。AGENT_PORT と同じにはできない
CARD_SHARE_NETWORKS 127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,::1/128,fc00::/7,fe80::/10 鍵を配る相手。範囲 (192.168.1.0/24) かアドレス1つをカンマで区切る。書くと既定を置き換える ので全部書く。書けるのは既定の範囲の中だけで、狭めることはできても広げられない
CARD_TTL 1 鍵のやり取りに使う接続の TTL (1〜8)。配る側・受け取る側の両方で使う
CARD_SHARE_MAX_RTT なし (見ない) 往復時間の上限 (ミリ秒)。例は 7。超える相手には配らない

読めない値や、LAN の外へ広げる値なら起動しません。 理由はログに出ます。書き間違えた範囲を 黙って既定に戻すと、狭めたつもりが広いままになるためです。

  • CARD_SHARE_NETWORKS に、ループバック・RFC 1918 (10.0.0.0/8・172.16.0.0/12・192.168.0.0/16)・ IPv6 の ULA (fc00::/7)・リンクローカル (fe80::/10) からはみ出す範囲 (グローバルアドレス・ CGNAT の 100.64.0.0/10・0.0.0.0/0 など) が1つでもあれば止まります
  • CARD_TTL の上限は 8 です。Docker のブリッジを両側で使うと 3、Kubernetes でノードをまたぐと 3〜4 要ります (下の「限界」)。そこに家の中のルーターを越える余裕を足して 8 にしました。 64 や 255 まで上げられると、TTL で届く先を絞る意味が無くなります。届く先を決めるのは あくまでアドレスの範囲で、TTL はそれを後ろから支えるものです

断ったときは、配る側のログに「どこから・どの決まりで」が出ます (同じ相手を同じ理由で 断ったものは 10 分に1回)。受け取る側は、設定の「チューナー」の「カードリーダー」に理由が出ます。

[agent] 鍵を配るのを断りました: 203.0.113.5 (CARD_SHARE_NETWORKS) 203.0.113.5 は鍵を配る範囲 (CARD_SHARE_NETWORKS) の外です。…
受け取る側の画面 何が起きているか
鍵を配る拠点に断られました: … 配る側が決まりに沿って断った (理由はそのあとに続く)
… では鍵を配っていません (404)。… CARD_URL が普通の API のポート (25252) のまま
鍵を配る拠点に繋がりません (…)。配る側で CARD_SHARE_PORT を… 配る側でポートが開いていない
鍵を配る拠点から答えがありません。ルーターを越えた先や LAN の外には届きません TTL でルーターを越えられない (次の「限界」)

限界もあります。

  • VPN は同じネットワークに見えます。 WireGuard や OpenVPN でプライベートアドレスを 渡されると、アドレスでも TTL でも区別できません (トンネルの中は1ホップ)。往復時間の上限を 付けると、遠くからの VPN はたいてい落とせます
  • Docker のブリッジでは、コンテナの出入りで TTL が1つ減ります。 同じ世帯の機器の間で、片方の拠点がブリッジなら CARD_TTL=2、両方なら 3 にします (どちらでも、間にルーターがあれば越えません)。 配る側は network_mode: host にすれば 1 のままでよく、いちばん確かです
  • Kubernetes でノードをまたぐと TTL 1 では届きません。 Pod から出るところや CNI の 経路で減ります。hostNetwork: true にするか、同じ世帯の機器の間の経路のホップ数に合わせて CARD_TTL を上げます。 上げたぶんだけ遠くにも届くので、要るぶんより上げないでください (上限は 8)
  • アドレスと TTL が効くのは、相手と直に TCP で繋がっているときだけです。 逆プロキシや Docker の userland-proxy (IPv4 だけのコンテナに IPv6 で繋いだときなど) を挟むと、繋いでくるのは プロキシになります。アドレスはプライベートに見え、TTL もプロキシの先には効きません。 鍵を配るポートの前には何も置かず、Docker のブリッジで出すなら IPv4 だけにします ('0.0.0.0:25253:25253')。X-Forwarded-For は見ません
  • 同じ LAN の中のことは区別しません。LAN に誰を入れるかは、ルーターの側で決めることです。 シェアハウスのように別の世帯が同じ LAN にいるなら、CARD_SHARE_NETWORKS を自分の世帯の機器の アドレスだけに狭めてください

前の版から移るとき (鍵を配る API が普通のポートにあった版)。前の版のままの CARD_URL では「… では鍵を配っていません (404)」と出て鍵を受け取れません。

  1. 配る拠点に CARD_SHARE_PORT=25253 を足し、そのポートを LAN に出す (compose なら ports に '0.0.0.0:25253:25253'、ファイアウォールも)。TTL は上の「限界」に合わせる
  2. 受け取る拠点の CARD_URL のポートを 25253 に書き換える (TTL も同じく)
  3. 受け取る拠点が LAN の外 (別の世帯・VPN・CGNAT の先) にあったなら、その構成はもう使えません。 別の世帯なら、その世帯のカードを挿してください
  4. CARD_SHARE_NETWORKS に LAN の外の範囲 (100.64.0.0/10 など) を書いていたり、CARD_TTL を 9 以上にしていたりすると起動しません。消すか、範囲の中に直してください

カードを denpa 側に持たせない理由

Section titled “カードを denpa 側に持たせない理由”

「1枚のカードで何台ものチューナーを復号する」は、いまもそうなっています。 カードは順番に使い (BCas の錠)、用があるのは ECM が変わるとき (数秒に1回・ 数十ms) だけなので、チューナーが4台あっても1枚で足ります。

復号するのをエージェント側に置く理由は3つです。

  1. カードはあちらにしかありません。 リーダーは機材のあるマシンに挿さっています。 denpa 側に持たせるとリーダーを denpa へ通すことになり、拠点を分けられません
  2. 復号するのは選局ごとに1回で足ります。 選局の共有を捌くのはエージェントなので、 denpa 側で復号すると送られてきたストリームの数だけ復号することになり、MULTI2 が何倍にもなります
  3. denpa が中身を触らずに済みます。 録るものをそのまま書くだけで、 「復号できているか」を気にするのは受け取ったあとの検査だけです

境界のほうが言語より大事です。

誰が 何を
エージェント 機材に触るところ デバイスを開く・選局する・取り合いを裁く・B25 を復号する → バイト列を配る
denpa (TypeScript) 中身を分かるところ PSI/SI・EIT・ロゴ・番組表・予約・録画・画面

ここを崩してエージェントに EIT の解析まで持たせると、どの言語でも同じものを 2箇所に持つことになります。使ったままにするには ioctl を直接呼ぶ必要があるので、 bun では書けませんでした。

.NET を選んだのは手が動く言語だからです。1人で持つものなので、録画に失敗した夜中に 直せるかがいちばん効きます。この用途で困らないことも確かめました。

  • P/Invoke で ioctl を呼べます。 [LibraryImport] と [StructLayout(LayoutKind.Sequential)] で dtv_properties も usbfs の構造体も素直に組めます
  • Native AOT で1個の実行ファイルになります。 別PCへ置くのに「まず何かを入れて ください」が要りません。AOT は必須です
  • 速さは問題になりません。 チューナー4台でも 10 MB/s 程度で、Span<byte> と ArrayPool<byte> で収まります。GC で気をつけるのは読み取りの経路だけ (188バイト ごとに確保しない)
  • 安全性は Rust とほとんど変わりません。 危ないのはどちらも FFI の境目 (ioctl の呼び出し) だけです

日本語の書き方に気をつけます。 System.Text.Json は既定で テ に逃がすので、 channels.json が人に読めなくなります。Json.cs で UnsafeRelaxedJsonEscaping にしています (全角空白 U+3000 だけはどの JSON エンコーダも逃がすので合わせていない)。

プロセスは分けます。 ioctl は落ちるときは落ちるので、画面や他の録画を 巻き込みません (言語とは別の判断です)。

遅延は伝送路にはありません。 効くのは選局の同期と受け取った側のバッファで、 ループバックの往復とは桁が3つ違います。低遅延に要るのは溜めないことで、 protocol の選び方ではありません。なので、いちばん退屈なものを選びます。

何で 中身
制御 (選局・解放・状態) HTTP + JSON 小さい要求と応答
通知 (使った・離した・局が入れ替わった) SSE (/denpa/events) denpa は自分の画面でもう使っている
TS HTTP の chunked (/denpa/stream) 素のTSだけ。何も包まない。届いた塊ごとに flush する

採らないもの:

案 なぜ
gRPC .NET 側は良いが bun 側が重い。不透明なバイト列に protobuf の枠は何も足さない
HTTP/2 流しっぱなしの接続1本にフロー制御の窓が増えるだけ。h1 の chunked のほうが素直
共有メモリ / リングバッファ いちばん速いが、ローカル専用なので経路が2つになる。10 MB/s のために背負う複雑さではない
自前のバイナリ protocol 覗けなくなるだけ

ローカルを Unix ドメインソケットにする案はまだ入っていません (Program.cs は ListenAnyIP だけ)。 入れても denpa 側は /denpa/stream を fetch するまま、宛先が変わるだけです。

利用者が遅れても落とさず溜めます (Sink.Push)。録画は欠けると戻らないためです。 際限なく溜めるとプロセスごと落ちるので、64MB (TunerPool.MaxLag) を超えたらその利用者だけ 切ります。視聴と録画で分けてはいません。

エージェントは別イメージ・別プロセス

Section titled “エージェントは別イメージ・別プロセス”

2つのイメージ (denpa と denpa-agent) を並べています。1つのコンテナで動かしたいとき用に、 2つを重ねただけの denpa-aio もあります (.github/aio.Dockerfile)。中でも別プロセスで、 TUNER_AGENT_URL が 127.0.0.1 を指すだけです。

  • denpa はエージェントを起こしません。 TUNER_AGENT_URL で繋ぐだけで、 Helm チャートは 2 つの Deployment を一緒に立てます (charts/denpa/templates/agent.yaml)。 1 台のマシンのふつうの構成でも、動くものは 2 つ (プロセスを分ける理由は上記)
  • チューナーの刺さったマシンにエージェントだけ置き、denpa は別の所で動かせます (charts/denpa-agent)。元のTSの置き場 (denpa-raw) は NFS などで両方に見せます (見せられなければ、 録画中にカードが読めなかったものを後から復号できなくなるだけ) (tuners.md)
  • 繋げるエージェントはいまは1つだけです (TUNER_AGENT_URL)。複数のエージェントを 1つの denpa に束ねるのはまだ入っていません。入れるなら 取り合いは denpa が全体を見て決め、どのエージェントの何台目かは denpa 側の帳簿になります