録画の置き場と配り方
エンコードが終わったものをどこに置き、どう配るか。 保存先の構成、認証、削除、 録画の節目の通知まで。
エンコード方法そのものは encode.md、外から一覧を取る API は api.md。
コピー禁止の番組は録らないので、置き場にも来ません (市販のレコーダーと同じ。 data.md、Discussion #760)。 置き場にあるものは、端末への保存 (オフライン視聴) を含めてこれまでどおり配ります。
/media/encoded/ ← ENCODED_DIR└── テストアニメ/ ├── テストアニメ - 2026-08-01 - 2130 決戦.mkv ├── テストアニメ - 2026-08-01 - 2130 決戦-poster.jpg ← サムネイル (一覧の画像) └── テストアニメ - 2026-08-01 - 2130 決戦.bml.jsonl ← データ放送 (持っている録画だけ)番組情報はDBが持ち、denpa の画面が出します。 隣に置くのはサムネ
(-poster.jpg、一覧と詳細の画像) と、データ放送の変化ログ (.bml.jsonl) だけ。変化ログは
エンコードするときに元のTSから取り出し、観る画面のデータ放送 (⚙ から出す) が再生位置に合わせて流し直します
(server/recorded-bml.ts)。シリーズ名のフォルダにまとめるのはディスク上で見やすくするためで、
同じシリーズ・同じ分に別の録画がぶつかったときだけ … [39].mkv のように録画IDを足します。
コーデックを両方選ぶと、1本の録画が2ファイルになります (… .mkv = AV1、
… [H264].mkv = H.264。mp4 は ARIB 字幕を持てないので、どちらも Matroska)。主 (再生の
既定) は AV1 で DB の library_path、もう一方は alt_path。ポスターは
両方の隣に置きます。外から片方を消されたら、実体との照合 (files.reconcile) が
消えたほうだけを外し、主なら残ったほうを繰り上げます (一覧の画像は途切れない)。
自分がいま置いてあるファイルは衝突と数えません (再エンコードのたびに名前が変わらないように。library.libraryPath)。
名前を決めるのは、エンコードが終わってからです。作業ファイルにはジョブIDを入れ
(<名前>.<jobId>.<codec>.encoding)、置き場所は置く直前に決め直します。同じ番組を2本同時に
エンコードしても、2つの ffmpeg が1つのファイルに書き込みません。
再エンコードするときは、始める前に前のものを消します (DB の library_path/alt_path も空ける)。
出来上がるまで別名 (.encoding) に書くので、消さないとエンコードしている間は mkv が
2本ぶん場所を取り、画面に出る大きさより使用量が多いことが外から分かりません。
元のTSは別にあるので、失敗・中止してももう一度押せば戻ります (その間の再生は
元のTSを返す)。DB を空けずに残すと、実体との照合が「保存先から消えた」と読んで
録画ごと削除済みに倒し、元のTSまで消します。
観るのはブラウザで
Section titled “観るのはブラウザで”録画一覧の行を押すと /watch/<id> が開き、その場で再生が始まります
(番組表からは詳細の「再生」)。ほかに観られるのはテレビのアプリだけです (下)。
エンコードしたものはブラウザがそのまま読めます。AV1 + Opus の Matroska は
Chromium で canPlayType が probably を返し、実機の録画で再生もシークも通ったので、
端末ごとに渡す先を用意する理由が無くなりました。
| 画面 | 768px から映像の右に番組の中身のサイドバー (長ければそこだけ巻き取る)。狭い画面では映像が上 |
| 開いたとき | 指で触っている端末 (pointer: coarse) はそのまま全画面へ。抜けると2段組 |
| 押したとき | マウスはどこを押しても再生/一時停止。指は操作列の出し入れ (YouTube と同じ) |
| 左右の端を素早く2回 | 10秒 戻す/送る。続けて押せばそのぶん重なる |
| チャプター送り | CM飛ばし。入っているときだけ出す |
| シークバー | 映像の上に重ねる細い半透明の溝。CM の区間は柔らかい琥珀で分け、観たぶんは濃い琥珀 (続いている CM は1本に。色は denpa-tv と同じ #FFB300)。本編の観たぶんは主の色。CM の端とそれ以外の切れ目 (引き継いだ録画の名前の付いたチャプター) で溝を 2px 区切る。マウスを載せた・触っている間だけ太くなり、つまみが出る。読み上げでは位置にチャプターの名前を添える (SeekBar.svelte・seek-track.ts) |
| CMを自動で飛ばす | 既定でオン (切らずにエンコードした録画で毎回ボタンを押さずに済むように)。選んだ値は端末ごとに覚える。ロゴでの判定に失敗した録画だけは、覚えていてもオフで始める (無音だけで当てていて外れやすく、外れると本編を飛ばす)。⚙ の「CMスキップ」で切り替える。CM のコマは1枚も出さない (下の「CM は1コマも見せない」) |
| 削除 | 観たその場で消せる。末尾はたいてい CM なので、流したまま押せる |
| 字幕 | 持っている録画でだけボタンが出る。既定で出す (ライブと同じ)。放送の字幕を放送どおりの置き場所で描くのでルビも出る |
| 続き | 途中で止めたところから始まる。端末をまたいでも続く (覚えるのはサーバ側) |
| 読み込み待ち | 0.4秒 以上詰まったら輪を出す。 詰まりか壊れたかが見えないと待つか決められない。短い詰まりで出すと点滅するだけ |
| 速さ | 1〜2倍。選んだ値は端末ごとに覚える。残り時間も倍速のぶんを割って出す |
| 切り抜き | いまの1コマを字幕ごと PNG に (番組名_YYYYMMDD-HHMMSS.png)。スマホは共有シート (写真へ保存)、PC はファイルに保存してクリップボードにも置く (貼れない繋ぎ (http) では保存だけ) |
操作バーとキー操作は watch.md、なぜそうしたかは player.md。
CM は1コマも見せない
Section titled “CM は1コマも見せない”自動で飛ばすとき、CM のコマは1枚も出しません。 ブラウザは先のコマを何枚かコンポジタへ 渡し終えているので、飛ぶ位置を先読みしても渡し済みのぶんは出ます。予測で消せないので隠します。
| やること | なぜ | |
|---|---|---|
| 跨ぐ手前 | 0.1秒 (3コマ) 前に飛ぶ | 入ってから飛ぶと必ず1コマ映る |
| 飛ぶ直前 | そのコマを写して蓋にする | 止めたコマのほうが飛んだように見える (黒は切れて見える)。写すのは本編にいるときだけ。CM の中で写せば蓋そのものが CM になる |
| 観はじめ | チャプターが届くまで黒い蓋 | チャプターは開いてから取得するので、揃うまで頭の CM がそのまま流れていた |
| 着いたあと | 4コマ数えてから外す | requestVideoFrameCallback は「コンポジタへ渡した」ところで来て、画面に出るのはその先。さらに Windows の Chrome / Edge は映像を DOM とは別の面に出すので、蓋を外す更新が映像より先に画面へ出る (位置も章も本編なのに画面だけ CM のまま、という画面が実機で撮れた) |
飛ぶ先を正しくするのはエンコードするほうの仕事です。チャプターの時刻がずれていれば、 ここで何をしても CM の途中に着地します (cm.md)。
録り終えた録画はどれも /watch で観ます (エンコードを待っている間も、失敗したものも)。
録っている最中のものは予約の側にいて、その行の「追っかけ」で追っかけ再生 (/chase/<id>) が開きます。録り終えるとその画面に
観る画面への案内が出ます。再生の印が消えるのは、消した録画と録画そのものが失敗したものだけです。
配信は /api/recordings/<id>/file (Range 対応。既定で返すものは app.md)。
?source= で名指しもできます。ts = 元のTS、encoded = エンコードしたもの (両方エンコードしたときは AV1)、
alt = もう一方 (H.264)。テレビのアプリと端末に保存も名指しします。再生のためのエンドポイントなので、
添付 (Content-Disposition: attachment) にはしません。
録画をファイルとして外へ持ち出す機能 (ダウンロード) は持ちません。 エンコードした録画 (mkv) にはコピー制御の符号が 引き継がれないためです (元のTSは符号が 入ったまま保存していて、取り除いたり書き換えたりはしていません。Discussion #760)。 端末に保存 (offline.md) は denpa の中だけで観るもので、こちらは残しています。
観る画面はファイルを名指しします (?source=encoded か alt。下)。エンコードしている最中に
名指しなしで開くと元のTSが返り、何も映りません。
何で観るかは、サーバが出せるものから画面が選ぶ
Section titled “何で観るかは、サーバが出せるものから画面が選ぶ”分担: サーバは「何を出せるか・どの形式で出すか・時刻の物差し」を決め、画面は「どれを使うか」を決めます
(端末で再生できるか、人が選んだコーデック)。サーバは端末が何かを知りません。開いたときに
api/recordings/<id>/playback を聞きます (api.md)。
| 録画にあるもの | 出せるもの |
|---|---|
| エンコード済み | ファイルそのまま。読めない端末にはサーバが fMP4 に詰め替えて流す (映像は写すだけ。軽い) |
| エンコード済みと違うコーデックを選んだ | エンコード済みを元にサーバがその場で変換して流す |
| 元の TS だけ (エンコード前・失敗・「エンコードしない」) | サーバが元の TS をその場で変換して流す |
| 元の TS (CM を切らずにエンコードした録画か、エンコード前) | MPEG-2 のまま流す (端末で再生する方式。⚙ のコーデックで選ぶ) |
画面の選び方 (ts/playback.ts の pickOffer): そのまま読めるファイル (canPlayType を Matroska と WebM で。
端末の名前では決めない) → 詰め替え (MSE が受け取れるもの) → 変換 (H.264 → AV1)。
コーデックのメニュー (この端末で観られる方式が2つ以上あるときだけ) で「自動」か H.264・AV1 を選べ、
選んだものは端末ごとに覚えます。AV1 だけエンコードした録画でも H.264 を選べば観られます (サーバが変換する)。
ファイルをそのまま読んでいるときの音声の切り替えは、ブラウザが入れ物の音声を出せるか次第です
(video.audioTracks)。Chrome・Firefox は既定で出さないので、そのままでは2本目 (副音声・解説) を選べません。
そこで2本以上あれば playback の名前で並べ、2本目を選んだら、いまの位置からその場で流すほうへ移ります
(ts/playback.ts の streamForAudio。同じコーデックの詰め替えを先に)。
その場の変換には同時に走らせる本数の上限があります (設定の「録画」の「同時に変換する本数」。既定は encode.md)。埋まっていると 「サーバが混んでいます」と出し、サーバに変換させずに観られるコーデックがあればそれを勧めます。
時刻の物差しは 1 ページで 1 つです。 エンコード済みがあればその時刻、無ければ元の TS の頭を 0。
サーバはどの方式でもストリームの時刻をこれに乗せるので、続きの位置・CM 飛ばし・字幕・データ放送・速さは
currentTime のまま効きます。まだエンコードしていない録画は、チャプター (CM 飛ばし) を検出した区間
(cm_ranges。元の TS の頭からの秒) から組み、字幕を元の TS から抜きます。データ放送は
エンコード済みの時刻で書いてあるので、エンコードするまで出しません。
| その場で流しているとき | |
|---|---|
| バー | 最初から全長。飛んだ先が溜まっていなければ、そこからリクエストし直す (少し先へ送るぶんは届くのを待つ) |
| 続き・字幕・チャプター・CM 飛ばし・データ放送・速さ | そのまま効く (頭から観たときだけ数十ms ずれる) |
| 音声の切り替え | 選ぶと、いまの位置からリクエストし直す (流しているのは選んだ1本だけ) |
| 端末に保存したもの | 使わない。オフラインではそのまま渡し、読めなければ観られる先 (Chrome・Edge かテレビのアプリ) を案内する |
| 切れたとき | 観ていた位置からリクエストし直す (待ちは倍々で10秒が頭打ち、続けて8回で諦める) |
互換モード (再生画面の ⚙ の「互換モード」) では <video> を使いません。 同じ /stream を fetch で読んで canvas に
描きます (H.264・AV1 の fMP4 か、元の TS の MPEG-2。速さは等速だけ)。端末に保存したものも、保存した Matroska を読んで canvas に描きます。
作りは stream.md に。
iPhone の Safari は H.264 の録画をそのまま読めません (#503)。Matroska を WebM としてしか読まず、
WebM に入れてよい映像は VP8 / VP9 / AV1 だけです (H.264 は開発者向けの設定の奥。WebKit の
SourceBufferParserWebM)。そこでサーバが詰め替えて MSE (ManagedMediaSource) に流します。
対応は iOS 17.1 以降です (iOS 16 までの iPhone は MSE の実装を持たないので、今までどおり
「このブラウザでは再生できませんでした」と出し、Chrome・Edge かテレビのアプリを案内する)。実機 (iPhone) ではまだ確かめていません。
ほかに読めなかったときも、そうと出して観られる先を案内します。
テレビのアプリ
Section titled “テレビのアプリ”ブラウザのほかには、テレビのアプリで観られます (danything/denpa-tv)。 Android TV / Fire TV でライブと録画を観られます (続きから・CM 飛ばし・低遅延のライブ)。家の外からは QR で繋いで アクセストークンで入ります (auth.md)。
外部のプレイヤー (VLC など) には対応しません。XSPF などの出力も、ログインせずに開ける URL を渡す仕組みもありません (前にあった「再生リンクをコピー」はやめました。config.md)。
字幕はどこから出るか
Section titled “字幕はどこから出るか”入れ物の中の、放送の ARIB 字幕 (S_ARIBSUB) です (隣に字幕のファイルは置かない)。denpa の観る画面はサーバでデコードして
文字の配置で渡し、ブラウザが描きます (ライブと同じ。api/recordings/<id>/captions.json)。前の字幕の画像 (PGS) の扱いと入れ方は
encode.md に。
誰が取れるか
Section titled “誰が取れるか”ファイルのエンドポイントは、画面と同じ資格で開けます (信頼したネットワーク・ログインの控え・アクセストークン)。
観る画面の <video> も端末に保存も同じ URL にそのままリクエストして、URL に資格情報は埋めません。
決まりごとと理由は auth.md に。
denpa の画面から消します (押し間違い防止に2回押し)。自動削除はありません。
denpa を通さずに消されたものは RECONCILE_INTERVAL (既定5分) ごとの突き合わせで見つけます
(すぐなら「ファイルと照合」)。
見るのは両方向です。 実体の無い行だけでなく、動画が無くなった付属ファイル
(サムネイル・.bml.jsonl などの作業ファイル、昔の .sup .nfo) と空フォルダも
掃きます。付属ファイルは動画と同じ名前で隣に置くので、名前でどの動画のものか分かります
(元のTSを残さない設定だと、付属ファイルだけが録るたびに積もるため)。
前の CM検出 (dtvindex) が作った .dtvi は、元のTSを残している録画の隣なら残ります (要らなければ手で消せます)。
動画そのものには触りません。行の無い動画は手で置いたものかもしれないので、数えるだけです。
録画の節目を Webhook で外に飛ばせます。設定の「連携」の「通知」に Discord や Slack の Incoming Webhook の URL をそのまま入れられます。
- 録画開始 / 録画完了 / 録画失敗 / エンコード完了 / エンコード失敗、および録画の外側の
変事 (ディスク残量わずか / チューナーエージェント切断 / チューナーエージェント復帰)。選ばなければ全部
(種類は
src/lib/webhook-events.ts) - 送信は投げっぱなし。通知先が遅くても録画を止めないよう、10秒で打ち切って結果だけ 記録します。直近の結果は同じ節に出ます
- 録画の失敗は画面を開くまで気づけないので、せめて失敗だけでも入れておくのを勧めます