コンテンツにスキップ

エンコード

録ったあとの話。 何をどうエンコードするか、字幕をどう入れるか。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)

Section titled “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 段の説明にあります。

使えるものは自動で使います。設定の「録画」の「エンコード」(/settings/recording#encoding) では、映像コーデックごとに横の欄でエンコーダ (ソフトウェア・GPU の名前・GPU を順番に使う) を選べます。欄が出るのは、そのコーデックを GPU でエンコードできるときだけです。 GPU が2枚あればコーデックごとに割り振れ、「GPU を順番に使う」ではジョブごとに先頭を入れ替えます。 設定はデバイス × 方式 × コーデックで持っていて (hwAllow)、画面で選び直しても使えない方式・いま見えていない デバイスの分は変えないので、GPU を挿し替えればそのまま効きます (hw.chooseHw)。挿し直した・権限を直したときは コーデックの下の「確かめ直す」で、再起動せずに見直せます。

試す順は (デバイスごとに) 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) でも試し、通ればそちらでエンコードします。どちらで 通ったかはデバイス・コーデックごとに覚えていて、設定画面のコーデックの下の「詳しく」には「VA-API: H.264 (CQP)」のように出ます。 CQP は場面に応じて量を変えないので、ICQ より動きの多い場面で大きく、静かな場面で小さくなるはずです (実測はまだ)。

ライブも同じエンコーダで GPU に載る

Section titled “ライブも同じエンコーダで 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 のコメント。ホストに 無いと起動できないので既定では書いていません)。

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

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

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

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

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

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

Section titled “字幕は放送の 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)。

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

Section titled “トラックの名前は放送のものを入れる”

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

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

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

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

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

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

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

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)。

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

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

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

ffmpeg 9.0 / 9.0.1 では 60コマの長尺で音声が途中で終わっていました (9.0.2 で直った。1時間超を通しで 確かめるのはまだ。patches/README.md)。

実測 (地上波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% (パン・合成・ノイズで縞が出る)

そこでフィールドそのものを比べて素材を見分けます (src/lib/server/fps.ts)。本編から30秒の窓を5つ取り、 窓ごとに2つの値を出します。

  • 繰り返し (rep) … 上フィールドだけを並べ (field=type=top)、前の上フィールドとの差 (signalstats の YDIF) を 1秒ずつ位相 mod 5 で平均し、いちばん小さい位相 ÷ 残り4つの平均。3:2 のテレシネは5回に1回同じフィールドを 繰り返すので 0 に近く、60i と 30p は 1 に近い
  • 上下の差 (wr) … separatefields で並べたフィールドの差を「同じコマの上と下」と「コマをまたぐ」に分け、 小さいほう ÷ 大きいほう。30p は上下が同じ時刻の映像なので低く、60i とテレシネは 1 に近い

どちらも動いている秒 (差の平均 0.5 以上) だけで数え、中央値を採ります。窓は次のように分けます。

判定 条件 出すコマ数
測れない 動いている秒が 5 未満 (静止・黒)、ffmpeg の失敗 数えない
24p 系 (アニメ・フィルム) rep < 0.45 30
30p rep ≥ 0.45 かつ wr < 0.5 30
60i (実写・生放送) rep ≥ 0.45 かつ wr ≥ 0.5 60

5窓の多数決で、60i が 24p 系 + 30p と同数以上なら 60コマ、測れた窓が無いときも 60コマです。 窓ごとの rep・wr・判定はログ ([fps]) に出るので、閾値を詰め直すときはそれを見ます。

実測 (窓ごとの値):

素材 窓 rep wr
アニメ本編 (本番の録画5本) 27 0.04〜0.42 0.74〜0.98
CM のフィルム撮り 13 0.04〜0.20 0.29〜0.92
CM の 30p 4 0.52〜0.89 0.24〜0.40
実写の 60i (スポーツ・バラエティ、2局) 4 以上 0.83〜0.93 0.87〜0.97
合成: テレシネ (ノイズ・60i のテロップつきも) 4 0.08〜0.26 0.88〜0.99
合成: 60i 2 0.97〜0.99 0.90〜0.98
合成: 30p 2 0.96〜0.99 0.29〜0.39
  • 60コマに起こして同じフレームを数える方法 (mpdecimate) は使えません。 bwdif が同じ映像の上下フィールドから 少しずつ違う2枚を作るので、動きの激しいアニメが重複に数えられず 60コマと誤判定します
  • CM は実写の 60i とは限りません。 本番の CM の窓 18 個のうち、13 個がフィルム撮り (24p のテレシネ)、 3 個が 30p で、60i らしいのは 2 個だけでした
  • 30p も見分けます。 出すのは 30 か 60 だけですが、wr が無いと 30p (rep は 1 に近い) が 60i に見えます
  • [0:v:0] で主映像に固定。 地上波の TS にはワンセグ (320x180 の H.264) も残っていて、 任せるとフィルタがそちらに当たることがあった
  • 窓は録画全体に散らす。 CM はエンコードしながら探すので、測る時点では分かりません。散らせば CM に乗るのは 5窓のうち多くて2つで、多数決は動きません (本番の録画 117 本の CM 位置で数えると、乗るのは 0〜2 窓で 3窓以上は無かった)。最初の本編区間に5窓を固めると、アバン+OP に引きずられます
  • 確かめ方。 判定の計算は単体テスト (fps.test.ts) で、本物の ffmpeg での値はイメージの確認 (.github/image-smoke.sh → scripts/fps-smoke.ts) で、testsrc2 から作ったテレシネ・60i・30p・テロップ帯つきの TS を測って見ます。image-smoke は Dockerfile などを触った PR と fps.ts を触った PR で走ります
  • 迷ったら 60コマ。 本物の 60i を 30 に落とすと動きが崩れるが、逆は無駄が出るだけ

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

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

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

本番の録画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 を追い越します。 SSIM で当てると 22 寄りで (一致点は SSIM で 21.3〜23.7、XPSNR で 21.9〜24.6)、23 は両者の間で AV1 を下回りにくいほうです。

画質を揃えて 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 (値は書いておく)

Section titled “preset は 9・crf 35 (値は書いておく)”

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

4.x の -preset 10 は「人が見る映像のための段」から外れています (Svt[warn]: Non-RTC M10+ are meant for automation tooling usage. Visual artifacts may occur otherwise.)。 症状はシーンの変わり目のブロックノイズで、カット直後の数コマだけ大きく崩れてすぐ戻ります。

実測は実素材 (地上波アニメ 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 へ)

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

素材は実機の録画から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

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

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

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

Section titled “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」)。

中断は後始末が終わってから返す

Section titled “中断は後始末が終わってから返す”

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

上限は5秒 (ffmpeg は SIGTERM から1秒ほどで終わる)。間に合わなければ返して、通知 (SSE) に任せます。 待つ前に段階の名前を「中止しています」にし、以後は書き換えません (後始末の途中の段階が上書きすると、 押したのに「CMを探しています」に戻って見える)。

ボタンは後始末が終わるまで回したままにします。 行に isCanceling (DB には無く、動いている間だけ) を添え、 立っている間は押せない回ったボタンを出します。

AbortSignal の abort は押された瞬間にしか鳴りません。 あとから起こした処理は最後まで走るので、 処理を起こす関数 (stream.run など) は起こす前に signal.aborted を見て降ります (stream.run は 殺されたときと同じ 143 を返す)。signal を受け取らない ffprobe の前にも同じ判定があります。

行を先に canceled にはしません。 速く見えますが、ffmpeg が元のTSを読んでいる間に削除の API が 開きます (動いている間に出すのは中断だけ、が画面側の決まり)。 試験の偽 ffmpeg は、止められてから抜けるまでの間を本物に似せてあります (trap 'sleep 2'。即死すると直っていなくても通る)。

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

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

画面に出す進み具合は、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 が使えない環境) はこちらで出します。ログには両方を並べて書くので、詰まったときに「デマクサだけ先に 走っているのか、本当に遅いのか」が分かります。