Skip to content

Latest commit

 

History

History
412 lines (309 loc) · 32.1 KB

encode.md

File metadata and controls

412 lines (309 loc) · 32.1 KB

エンコード

録ったあとの話。 何をどうエンコードするか、字幕をどう入れるか。CM の見つけ方は cm.md。 選ばなかった案と、実測して見送ったものも置いてあります。

保存先と配り方は library.md、局ロゴは logo.md。

既定は AV1 + Opus + ARIB 字幕 (そのまま) の Matroska。同じ画質でファイルは小さくなりますが 時間がかかるので、AV1 をデコードできない相手 (古いテレビなど) 向けに設定画面で H.264 も選べます。 音声・字幕・コンテナは変わらず、どちらも 8bit です (下の実測)。

両方を同時に選べます。 1本の録画を AV1 (主) と H.264 (… [H264].mkv) で2本エンコードします。 置き方と主・従の決め方は library.md。 CM は1本目をエンコードしながら探して、2本目は決まった境目でエンコードします (cm.md)。

「エンコードしない」は「どちらも選ばない」ことです。 元のTSをそのまま保存先へ置くので、 CMのチャプターも字幕トラックも付きません (どちらもエンコードのときに入れるため)。 別に「エンコードする」のチェックを持っていた頃は、外してもコーデックの選択だけが画面に残り、 どちらが効いているのか読めませんでした。

GPU でエンコードする (Intel QSV / VA-API)

GPU が見えれば、起動時に自分で見つけて使います。 コンテナに /dev/dri を渡してあれば、 src/lib/server/hwenc.ts が起動のたびに HW_DEVICES (既定 /dev/dri/renderD*) に当たるデバイスを全部見つけ、 デバイスごとに方式 (QSV / VA-API) × コーデック (H.264 / AV1) の4通りを1コマエンコードして確かめます。 ffmpeg -encoders は「組み込んであるか」しか分からず、動くかは世代でも違う (Arc より前は AV1 のエンコードが無い) ためです。イメージの中身は Dockerfile の ffmpeg 段の説明にあります。

結果は設定画面の「GPU」カードにデバイスごとに出て、使えるものには自動で印が付きます (外したいものだけ外す)。 グラボが2枚あればデバイスごとに割り振れ、同じコーデックをエンコードできるデバイスが複数あればジョブごとに先頭を入れ替えます。 使えないものの印は触れないので、GPU を挿し替えればそのまま効きます。挿し直した・権限を直したときは 「確かめ直す」で、再起動せずに見直せます。

試す順は (デバイスごとに) QSV → VA-API、最後にソフトウェアで、どの方式でも「素のまま → 頭を少し捨てて」の 2回が落ちたら次へ移ります。試しのエンコードが通っても素材しだいで落ちることはありますが、GPU が駄目なだけで 録画は失敗しません。Linux では QSV の下に必ず VA-API があるので、速さは同じです。QSV のほうが レート制御が豊富で Intel が手入れしているので先に使い、VA-API は QSV が初期化できない世代の逃げ道にしています。

画質は ICQ (-global_quality 24) で、QSV も VA-API も、H.264 も AV1 も同じ値です。 ICQ は x264 の crf に 当たる「この画質で」の指定のしかたで、1〜51 の同じ物差しなので、コーデックが違っても同じ見た目になり AV1 はそのぶん 小さくなります。手元に GPU が無いので実測はまだです。ソフトウェアでエンコードしたものと大きさが違って見えたら hwenc.hwArgs の値を動かしてください (ソフトウェアの 23 を写さないのは、測らずに写すと当てずっぽうが二重になるため)。 インタレ解除と引き伸ばしは CPU のフィルタで済ませ、エンコードするところだけ GPU に渡します。

ICQ を持たないドライバ (i965。Gemini Lake などの古い Intel) には CQP で指定します。 試しのエンコードで VA-API の ICQ が 落ちたら -rc_mode CQP -qp 24 (AV1 は qp の幅が 0〜255 なので 120) でも試し、通ればそちらでエンコードします。どちらで 通ったかはデバイス・コーデックごとに覚えていて、設定画面の「GPU」カードには「VA-API: H.264 (CQP)」のように出ます。 CQP は場面に応じて量を変えないので、ICQ より動きの多い場面で大きく、静かな場面で小さくなるはずです (実測はまだ)。

ライブも同じ印で GPU に載る

「GPU」カードでそのコーデックに印が付いていれば、ライブと追っかけも GPU でエンコードします (設定項目は増やして いません。印を外せば CPU に戻ります)。低遅延のために録画とは引数を変えています (hwenc.liveHwArgs: -bf 0、 -async_depth 1、QSV は -preset veryfast)。

MPEG-2 を GPU でデコードできるデバイスでは、復号とインタレ解除も GPU に寄せます (VA-API だけ。-hwaccel vaapi -hwaccel_output_format vaapi + deinterlace_vaapi=rate=field)。エンコードするところだけ GPU に移しても、CPU に残る復号・ bwdif・nv12 への変換で非力な機材は3本同時に間に合わないためです (issue #417 の実測。Celeron J4125 で3本同時、 エンコードするだけ GPU が 0.76x、全部 GPU が 1.2x)。使えるかは試しのエンコードで、その場で作ったインタレの MPEG-2 を GPU でデコードして 解除してエンコードして確かめます (hwenc.probeFull。GPU でデコードできないとフレームが GPU に無いので必ず落ち、通った = GPU でデコードできた)。 カードには「ライブは復号から GPU: H.264」と出ます。QSV はエンコードするところだけ GPU のままです。

映像を1つも出さないうちに GPU の ffmpeg が降りたら、ソフトウェアで再エンコードします (live.ts の Session.run)。 ソフトウェアで映像が出たら GPU のせいだったと決めて、画面に一行出し、その方式を15分覚えてその間はソフトウェアで エンコードします (選局のたびに待たせない)。ソフトウェアでも出なければ GPU とは関係なく降りたので (選局直後に映像が まだ来ていない、など) 覚えません。出し始めてから落ちたものも GPU のせいとは限らないので、いつもどおり 「映像を出せませんでした」です。

手元に GPU が無いので、確かめていないもの: 最初の映像が出るまでの時間 (x264 ultrafast と比べて)、 deinterlace_vaapi の画質が bwdif と比べてどうか、ICQ 24 / CQP 24 が x264 の既定 (crf 23) と見た目で揃うか。 MSE に渡す codecs (avc1.640029 = Level 4.1) は GPU でエンコードしたものの level と合わないことがあるが、 ブラウザは level で断らない前提でいます。GPU が init だけ出して降りたときは、画面は init を2回 (GPU のものとソフトウェアのもの) 受け取って SourceBuffer に足し直します。codecs は同じですが、中身 (SPS) の違う init を 足し直したときの MSE の振る舞いも実機では見ていません。

QSV は amd64 のイメージだけです (Debian の QSV まわりが amd64 にしか無い)。arm64 は VA-API だけで、 試しのエンコードで QSV が落ちるだけなので denpa は arch を見ていません。arm64 向けの VA-API ドライバはいま入れていない ので、実際にはソフトウェアでエンコードします (実機では未確認)。

Helm は既定で /dev/dri を hostPath で渡してあります (denpa.gpu.hostPath。 そのぶん Pod は privileged。デバイスプラグインを使うなら false にして resources.limits で)。Compose は devices: - /dev/dri:/dev/dri (compose.prod.yml のコメント。ホストに 無いと起動できないので既定では書いていません)。

録画のその場の変換も同じエンコード方式。本数に上限を置く

録画をエンコード済みと違う形式で流すとき (api/recordings/<id>/stream。server/playback.ts) もライブと同じ エンコード方式・同じ印で GPU に載せ、1 バイトも出さずに降りたらソフトウェアで再エンコードします (落ちた方式を 15 分覚えるのも ライブと共有)。元がエンコード済みならインタレ解除は済んでいるので挟まず、GPU も復号からは載せません。

同時に走らせる本数には上限があります (server/transcode.ts。設定の「GPU」カードの「同時に変換する本数」)。 数えるのは録画の変換と追っかけ再生で、ライブは数えず断りません。詰め替え (映像を写すだけ)・元の TS をそのまま・ 音声だけも軽いので数えません。埋まっていれば 503「サーバが混んでいます」です。

既定は GPU が無ければ 2、あれば 4 です。ドライバに「同時に何本エンコードできるか」を聞く手が無いので、GPU の有無で 決めています。 denpa が使う Intel QSV / VA-API はセッション数の上限を言いません (NVENC の消費者向けの上限も ドライバは公開していません)。実際に何本間に合うかは機材と素材で変わるので、設定で変えられるようにしてあります。 起動時の試しのエンコード (hwenc.probe) で 1 本あたりの速さ (fps) を測り、その倍率から既定を出す手もありますが、 いまの試しのエンコードは小さく短い素材で使えるかを見るだけで速さを測っていないので、採っていません。

字幕は放送の ARIB 字幕をそのまま

エンコードするときに ARIB の字幕を -c:s copy で写します (Matroska では S_ARIBSUB。encoder.captionArgs)。 観る画面はそれをサーバでデコードして文字の配置で描きます (api/recordings/<id>/captions.json。ライブと同じ描き方。 api.md)。外のプレイヤー向けの画像字幕は要らないと決めたので、画像の字幕 (PGS) は作らなくなりました。

ffmpeg -i <録画.m2ts> ... -map 0:p:<局>:s:0? -c:s copy <出力.mkv>   # 字幕は S_ARIBSUB になる
  • CodecPrivate は ffmpeg が書く (部品タグ 0x30 とデータ符号化方式 0x0008 の3バイト。実物で確認)
  • 局と字幕の筋を名指しする (0:p:<局>:s:0)。名指ししない既定の選び方では ARIB の字幕が落ちる。? は字幕の無い番組でも止めないため
  • 時刻は映像と同じだけ詰まる。頭の音声だけの区間を捨てる -ss も、CM を切る concat も筋ごとに同じに効く (CM を切るのはエンコードしたものをキーフレームで切る -map 0 -c copy。チャプターの書き足しも同じで、字幕の筋は落ちない)
  • 字幕の無い番組では字幕トラックが入らない

ずっと前にエンコードした録画は画像の字幕 (PGS) が入っていて、もう読みません (captions.json は 404)。 元のTSが残っていれば、再エンコードすると ARIB の字幕が入ります (詳細の「その他…」→「再エンコード」)。

ffmpeg は ARIB 字幕をデコードしません。 -c:s copy は中身をそのままコンテナに移すだけなので、字幕のデコーダ (--enable-libaribcaption) は要らない (ffprobe もデコーダなしで Subtitle: arib_caption と名乗る)。 デコードするのはライブも録画も denpa (ts/b24caption.ts)。

トラックの名前は放送のものを入れる

音声も字幕も、番組表と同じ言い方で名前を付けます。 付けていなかった頃は プレイヤーの切り替えに「Audio 1」「Audio 2」しか出ず、二カ国語や解説放送でどちらがどちらか 分かりませんでした。

名前 どこから
音声 「主音声 (日本語)」「解説 (日本語)」「ステレオ (日本語)」 番組表の audios (arib.audioTitles)
音声 (デュアルモノ) 「主音声 (日本語)」「副音声 (英語)」 同上。1本を左右に割るので必ず2本
字幕 「字幕」 放送の字幕は日本語しか来ない

番組表の行は24時間で消えるので、音声の構成は録るときに recordings.audios へ 写しておきます (ジャンルと同じ扱い)。写しの無い古い録画は既定の名前 (「音声」/「主音声」「副音声」) に落ちます。

番組表の本数と実際の本数がずれても困りません。ffmpeg は在りもしないトラックへの指定を 黙って読み飛ばします (実機で確認)。

入れ物にも番組名を書きます (mkv の title。title.displayTitle で [字][デ] などの 装飾記号を落としたもの)。 字幕トラックには language=jpn と default の印を付け、並びは映像 → 音声 → 字幕にしてあります。 字幕がトラック0だった頃、テレビ組み込みのデマルチプレクサで字幕を選ぶと固まる症状があったためです。

頭の音声だけの区間は、ずらさずに捨てる

TS は音声のほうが先に始まっているのがふつうです。そのままエンコードすると出来上がりも 音声だけの区間から始まり、1コマ目を 0 秒として数えるプレイヤーではそのぶん 字幕が早く出ます。

測るのは「実際に復号できた1コマ目」です。stream.start_time は最初の パケットの時刻で、そこから何コマかは参照先が録れておらず捨てられます。

入れ物の始まり           6115.509
start_time (パケット)    6115.872   ← これを信じると 0.363
実際に出た1コマ目 (I)     6116.439   ← 本当は 0.930   差 0.567 = 17コマ = 半GOP

ずらす (-output_ts_offset) のでは直りません。 Matroska は負の時刻を持てないので、 下がった音声を muxer が押し戻し、全トラックまとめて元の位置に戻ります。実測:

映像 音声
ずらさない 0.934 0.022
-output_ts_offset -0.363 0.912 0.000
-ss 0.930 で捨てる 0.000 0.000

0.363 渡して動いたのは 0.022 だけでした。捨てれば映像も音声も 0.000 から始まり、 捨てるのは映像がまだ無いところなので見えるものは減りません。

捨てる長さは encoder.headSkip に1箇所だけ置いています (字幕はエンコードする ffmpeg が映像と一緒に写すので、同じだけ詰まる)。 再試行のときの頭捨て (PAT/PMT が固まる前) とは理由が別なので、両方あるときは足します。

CMのチャプターも同じ量を引きます。 検出は境目を入れ物の頭から数えた秒で返すためです (cm.md)。

コマ数は本編の映像から測って決める

bwdif の既定は send_field で、1フィールドから1コマ作って 59.94p にします。 放送は 1080i (毎秒60フィールド) なので、実写・スポーツ・報道は撮られたとおりの 動きになりますが、コマ数が倍なのでエンコードの時間もサイズもおよそ倍です。

ffmpeg 9.0 / 9.0.1 では、60コマでエンコードした長尺の音声が 20〜25 分あたりで黙って終わっていました (CLI の溢れ FIFO の上限。9.0.2 で FIFO ごと無くなったので直しは取り下げ。経緯と数字は patches/README.md)。9.0.2 で1時間超を通しでエンコードした実測はまだ無いので、 次にエンコードしたら音声が最後まであるかを見ること。

実測 (地上波1分、AV1):

所要 サイズ コマ数
send_field (60コマ/秒) 31.8秒 26 MB 3598
send_frame (30コマ/秒) 16.9秒 15 MB 1799
H.264 medium (60コマ/秒) 29.2秒 34 MB 3598
AV1 -preset 8 (60コマ/秒) 59.5秒 27 MB 3598

アニメは毎秒24コマ前後の映像をプルダウンして60フィールドに乗せているだけなので、 フィールドごとにコマを起こしても同じフレームが並ぶだけです。そこで同じフレームが並ぶ素材かどうかを、 エンコードする前に本編から実測します (encoder.measureSmoothMotion)。

放送のTSに「本当のコマ数」は入っていません (本番で実測):

  • EIT の component_descriptor は中身が何でも 1080i (アニメも映画も同じ)
  • avg_frame_rate は常に 30000/1001、field_order は常に tt
  • MPEG-2 の interlaced_frame もほぼ全部立ち、テレシネの印 (repeat_pict) も無い
  • 縞の検出 (idet) も使えない。アニメ本編ですらプログレッシブ 0〜2% (パン・合成・ノイズで縞が出る)

唯一の見分け方が**「60コマに起こして、同じフレームが続く割合を数える」**でした。本編から30秒の窓を5つ取り、 bwdif=send_field → mpdecimate の生存率の中央値が閾値 (FPS_SURVIVE、既定 50%) 以下なら 30コマ。 本番の録画を30秒刻みで全部測ると、30コマのアニメ 17〜28% / 毎秒30枚が違うフレームのアニメ 48% / 実写 60〜79% (下位 1/4 でも 53% 以上) です。

  • 5窓の中央値。 3窓よりアニメが閾値から遠のく (24〜31% → 17〜28%)。下位や最小を採るのは駄目で、 実写でも静止した窓は 42% まで落ち、動く素材を 30コマに落とす
  • 閾値は 50% (45% では 48% のアニメが 60コマに倒れていた)。実写側の余裕は 3〜10 ポイントしか 無いので、これ以上は上げないこと
  • -map 0:v:0 で主映像に固定。 地上波の TS にはワンセグ (320x180 の H.264) も残っていて、 任せるとフィルタがそちらに当たることがあった
  • 窓は録画全体に散らす。 CM (実写の 60i) に乗った窓は釣り上がるが、CM はエンコードしながら探すので測る時点では分からない。 散らせば CM に乗るのは5窓のうち多くて2つで、中央値は動かない (本番の録画 117 本の CM 位置で数えると、乗るのは 0〜2 窓で 3窓以上は無かった)。最初の本編区間に5窓を固めていた頃は、アバン+OP の激しい動き (57〜74%) に窓が乗り、 30コマの本編 (20〜32%) を 60コマと誤判定していた
  • 迷ったら 60コマ。 本物の 60i を 30 に落とすと動きが崩れるが、逆は無駄が出るだけ

設定の「コマ数を映像から決める」を外すと実測せず全部 60コマでエンコードします (以前はジャンルで決めていた)。 ライブは映像が来る前に決めるので、常に 60コマです。

どちらに転んだかは recordings.fps に書き、番組詳細に「30コマ/秒」「60コマ/秒」のラベルで 出します。未エンコードの録画と、記録を始める前にエンコードしたものには付きません。

H.264 は crf 23 (AV1 と同じ画質)

画質は AV1 の既定に揃えます。 同じ番組をどちらでエンコードしても見た目が 変わらないようにするためで、大きさは揃えません。

以前の crf 24 は実写100秒1本の SSIM だけで決めた値で、揃え方を間違えていました。 本番の録画4か所を可逆で切り出し、同じコマを両方でエンコードして XPSNR で測り直したもの:

素材 AV1 crf35 h264 crf24 h264 crf23 h264 crf22
実写60コマ・忙しい所 35.28 dB 34.34 (−0.94) 34.93 (−0.35) 35.53 (+0.25)
実写60コマ・静かな所 35.36 dB 35.23 (−0.14) 35.64 (+0.28) 36.05 (+0.69)
アニメ30コマ 34.86 dB 35.10 (+0.24) 35.52 (+0.66) 35.97 (+1.11)
アニメ30コマ その2 38.52 dB 37.80 (−0.72) 38.16 (−0.36) 38.50 (−0.02)
平均 −0.39 平均 +0.06 平均 +0.51

crf 23 が AV1 の真上に乗ります (平均 +0.06dB、いちばん外して −0.36dB)。 crf 24 は平均で 0.4dB 低く、実写の忙しいところでは 0.94dB 低い。 crf 22 では AV1 を追い越します (平均 +0.51dB)。

Note

「60コマだと H.264 のほうが小さい」の正体はこれでした。 60コマ判定 ≒ 実写の動きの多い映像で、 crf24 がいちばん外していたところです。一段低い画質でエンコードしていただけでした。

SSIM で当てると 22 寄り、XPSNR で当てると 23 寄りです (一致点は SSIM で 21.3〜23.7、XPSNR で 21.9〜24.6)。23 は両者の間で、AV1 を下回りにくいほう。物差し1本・素材1本で決めないのが教訓です。

大きさは crf24 のときより2割ほど増えます (実写の忙しい所で 14.4→17.1MB、 アニメで 5.2→6.2MB)。画質を揃えて AV1 と比べると:

素材 AV1 crf35 h264 crf23 H.264 は
実写60コマ・忙しい所 20.7 MB 17.1 MB 0.8倍 (ここだけ H.264 が小さい)
実写60コマ・静かな所 1.6 MB 3.9 MB 2.4倍
アニメ30コマ 2.6 MB 6.1 MB 2.4倍
アニメ30コマ その2 0.8 MB 2.1 MB 2.7倍

忙しい実写では2つの効率はほぼ互角で、AV1 の強みは簡単な映像で出ます (そこで AV1 を crf38 に落とすと 18.7MB / 34.34dB となり、h264 crf24 とぴったり重なる)。

  • preset は medium。 slow にしても同じ画質で時間だけ 25% 増える (crf22 で slow 0.9757/76MB ↔ medium 0.9760/78MB)

preset は 9・crf 35 (値は書いておく)

書かずに既定へ任せません。 SVT-AV1 の既定は版で変わり (preset は 2.3.0 が 10、4.2.0 が 8)、 黙って入れ替わるとエンコードする速さも大きさも変わるためです。crf 35 は両方の版で既定のままです (ffmpeg の -crf が既定 0 と出るのは「未指定」の番兵で、渡さなければ SVT-AV1 内部の 35 が使われる)。

10 は使ってはいけない値でした

長らく -preset 10 (2.3.0 の既定の書き写し) でしたが、4.x ではこれが「人が見る映像のための段」から 外れています (Svt[warn]: Non-RTC M10+ are meant for automation tooling usage. Visual artifacts may occur otherwise.)。 上流が既定を 8 に下げたのはこのためです (こちらは時間と大きさの話だと思って留まっていた)。 症状はシーンの変わり目のブロックノイズで、カット直後の数コマだけ大きく崩れてすぐ戻ります。

実測は実素材 (地上波アニメ 90 秒、1440x1080 を denpa と同じ経路で 1920x1080 へ、 カット 22 箇所)。「カット直後」はカットのコマから 4 コマの PSNR です。

preset カット直後 最低 カット直後 平均 全体 所要 大きさ
10 29.74 dB 42.15 dB 46.27 dB 17秒 8.5 MB
9 37.77 dB 43.89 dB 46.70 dB 20秒 8.1 MB
8 37.69 dB 44.64 dB 47.50 dB 25秒 10.0 MB

9 は 10 の上位互換です。 崩れが 8dB 戻り、全体の画質も上がり、しかも 小さくなります。増えるのは時間だけで 18%。

8 までは落としません。カットの崩れは 9 で戻っており (差 0.08dB)、8 が勝つのは全体の平均だけで、 その代わり +18% 大きく +47% 遅い。

他のオプションは触りません

scd=1 (シーン変わり目でキーフレーム) は効きません。 4.2.0 は指定すると こう返して無視します。「SVT-AV1 has an integrated mode decision mechanism to handle scene changes and will not insert a key frame at scene changes」。 付けても付けなくてもキーフレームの位置は 0 と 161 のままでした。

tune (0/2)・enable-variance-boost・qp-scale-compress-strength・sharpness も同じ素材で測りましたが、大きさを揃えると差は 0.05dB 以内でした (固定 crf だと variance-boost がよく見えるのは、5割ビットを積んでいるだけ)。効くのは preset だけです。

10bit では出しません (実測して 8bit へ)

AV1 は 10bit で出していましたが、根拠がどこにも書いていなかったので測りました。 素材は実機の録画から100秒 (地上波の実写)、denpa と同じ手・ SVT-AV1 2.3.0 -preset 10 -crf 35。画質は同じフィルタを通した元との SSIM です。

所要 大きさ SSIM PSNR
10bit 39秒 21.71 MB 0.97582 44.65 dB
8bit 34秒 22.09 MB 0.97472 44.47 dB

差はあります。1.7% 小さく、SSIM +0.0011 (PSNR +0.18dB)。 8bit の素材を 10bit で エンコードすると丸め誤差が溜まりにくい、という教科書どおりの結果です。

元の放送が 8bit なので、8bit で出します。 AV1 10bit をハードウェアでデコードできる Android 機はまだ少なく、実機 (Android TV のプレイヤー) で 1080p59.94 の AV1 10bit が まともに再生できませんでした。2% は、観られなくなる代償に見合いません。

既にある 10bit のファイルは再エンコードしません (元のTSが無いものは再エンコードできず、あるものも 2% のためでは釣り合わない)。

SVT-AV1 は上流をソースから組みます

上流の 4.2.0 をソースから組んでイメージに入れています (Dockerfile の SVT_AV1_VERSION)。 Debian trixie のパッケージは 2.3.0 で止まっていて更新が来ません。libavcodec に静的に繋ぐので実行イメージは増えません。 上げるときの実測 (実機の録画から100秒、地上波の国内アニメ、denpa と同じフィルタ、当時は 10bit。画質は元との SSIM):

SVT-AV1 preset crf 所要 大きさ SSIM
2.3.0 10 (2.3.0 の既定) 35 28秒 5.25 MB 0.98900
4.2.0 10 35 26秒 5.65 MB 0.98821
4.2.0 10 36 — 5.27 MB 0.98770
2.3.0 8 35 45秒 5.87 MB 0.99133
4.2.0 8 (4.2.0 の既定) 35 37秒 6.69 MB 0.99090
4.2.0 8 37 — 5.77 MB 0.99034

同じ preset なら速くなり (preset 10 で 7%、8 で 18%)、大きさあたりの画質は変わりません (5.25 MB どうし・ 5.8 MB どうしで比べると、SSIM は 2.3.0 がわずかに上)。上げた理由は速さと上流の直しに乗り続けるためで、 仕上がりが黙って変わらないよう preset と crf は書いてあります (上の「preset は 9・crf 35」)。

中止は畳み終わってから返す

「エンコード中止」は、ffmpeg が死んで後始末が終わるのを待ってから返します (encoder.cancel)。画面は その返事で読み直すので、待たずに返すと押した直後は何も変わらず (同じ「エンコード中 60.9%」が出る。実機、2026-09-10)、 SSE の繋ぎが切れた端末ではリロードするまで変わりませんでした。

上限は5秒 (ffmpeg は SIGTERM から1秒ほどで終わる。実測 923ms / 1078ms)。間に合わなければ返して、通知 (SSE) に任せます。 待つ前に段階の名前を「中止しています」にし (CM検出のように数分かかる段階で効く)、以後は書き換えません。 畳む途中の段階が上書きして、押したのに「CMを探しています」に戻って見えていたためです。

ボタンは畳み終わるまで回したままにします。 上限で切り上げた返事で戻すと「回るのが止まったのにまだエンコード中」に なるので、行に isCanceling (DB には無く、動いている間だけ) を添え、立っている間は押せない回ったボタンを出します。

押された後の合図は聞き耳に届きません。 AbortSignal の abort は押された瞬間にしか鳴らないので、あとから 起こした道具は最後まで走っていました。エンコードする前の段階で押すと、当時の下ごしらえ (無音検出・字幕の画像化) まで 済ませてから畳み、何分も止まりませんでした。道具を起こす関数 (stream.run など) は起こす前に signal.aborted を見て降ります (stream.run は殺されたときと同じ 143 を返す)。合図を受け取らない下調べ (ffprobe) の前にも同じ判定があります。

行を先に canceled にはしません。 速く見えますが、ffmpeg が元のTSを読んでいる間に削除の API が 開きます (動いている間に出すのは中止だけ、が画面側の決まり)。

Note

試験で1度騙されました。 偽 ffmpeg は止められると即死するので、直っていなくても通っていました。 止められてから抜けるまでの間を本物に似せて (trap 'sleep 2') はじめて、直す前と同じように落ちます。

進み具合はエンコードできたコマ数から出す

画面に出す進み具合は、ffmpeg が何コマエンコードしたか (-progress の frame= ÷ エンコードするコマ数) から出します (encoder.encodeProgress)。残り時間も同じ出どころ (直近30秒で何コマエンコードできたか) です。出どころを混ぜると、 止まった数を動いている速度で割って「残り20時間」のような値が出ます。

エンコードするコマ数はエンコードする前に測った入力から出します (expectedFrames = 尺 × 入力の毎秒コマ数 × 60コマなら 2)。 インタレ解除は入力1コマからコマを作るので、入力が 29.97 (1080i) でも 59.94 (720p の局) でも同じ式で当たります。 CM を切るなら、切ったあとのTSを測ります。本番の録画を最後までエンコードして突き合わせたもの:

素材 見込み 実測
闇芝居 十七期 (5分26秒) 19553 コマ 19498 コマ 99.7%
さよならララ (30分25秒) 109412 コマ 109374 コマ 99.96%

見込みのほうが少しだけ多いので、バーは 99.7% あたりで頭打ちになります (終わるまで 99% で止めるので見えない)。 逆向きに外すと、終わる前に 100% と出て止まって見えます。

要点はエンコーダの位置を直に数えることで、以前の2つは別のものを見ていました (2026-09、BS-TBS の映画を libx264 で 240秒エンコードした時点で3つを同時に測ったもの):

物差し 実際には何か 言っている位置
frame=9576 (毎秒 39.89 コマ) エンコーダの位置 2分40秒ぶん (正しい。240秒 × 39.89)
out_time_us mux が最後に書いたパケットの時刻。疎な字幕 (.sup の copy) が先に mux されると先に飛び、壊れた副音声が混ざるとそこで止まる (Packet corrupt のあと 0% のまま) 13分10秒ぶん (speed=3.29x もこれ由来)
入力TSの読み位置 (/proc/<pid>/fdinfo) デマクサの位置。7.4GB の映画ではエンコードが半分進んだところで読み切り、99% に貼り付いた 12分30秒ぶん (RSS 1.07GB)

読み位置は控えとして残してあります (encodeProgress の第2引数)。尺が測れなかったとき (映像の無い録画、 ffprobe が使えない環境) はこちらで出します。ログには両方を並べて書くので、詰まったときに「デマクサだけ先に 走っているのか、本当に遅いのか」が分かります。