保持区間 [t_in, t_out) に対して:
... I ....... I=========================I ....... I ...
^t_in ^k_first ^k_term ^t_out
|<-head->|<--------- body --------->|<-tail->|
再エンコード ストリームコピー 再エンコード
head / tail は GOP の途中から始まる/途中で終わるので、前のアクセス
ポイントから復号して作り直すしかない。その間の body は入力のバイト列を
そのまま出力する。アクセスポイント上でカットすれば再エンコードはゼロになる。
probe.py— ストリーム諸元、アクセスポイント索引、 leading picture の検出と参照判定planner.py— 区間 → セグメント列bitstream.py— Annex-B / MPEG-2 のアクセスユニット分割renderer.py— ffmpeg 実行と連結verify.py— 出力を復号してソースとフレーム単位で照合
「GOP 単位で切って繋ぐだけ」で済まない理由。どれも試作中に実際に踏んで、 テストで再現を固定してある。
再エンコード部の SPS を元ストリームとビット単位で一致させることは事実上
不可能(エンコーダが違えば VUI も VBV も変わる)。ところが MP4 の avcC
box も Matroska の CodecPrivate もパラメータセットを 1 組しか持てない。
素朴に連結すると、コピー部か再エンコード部のどちらかが誤った SPS で
復号され、映像が壊れる。
対策は 2 段構え:
- 各ピースを raw Annex-B エレメンタリストリームとして書き出し、 単純なバイト連結で繋ぐ。ES は IDR ごとに SPS/PPS をインバンドで持つので、 ピースごとに異なるパラメータセットを合法的に共存させられる。
- 最終 MP4 は
avc3/hev1sample entry で書く。これは ISO/IEC 14496-15 が定めるインバンド方式で、avcCに畳み込まれない。
MPEG-2 はシーケンスヘッダが元々インバンドなので、この問題自体が無い。
ffprobe -skip_frame nokey はオープン GOP で取りこぼす。参照が欠けた
I ピクチャをデコーダが出力できないため。試作の検証素材では、実際には 10 個
あるアクセスポイントのうち 3 個しか拾えていなかった。
パケットの K フラグを見れば復号せずに済み、速く、かつ正確。
I ピクチャの復号順では後ろ、表示順では前にあるピクチャ(leading picture)は前の GOP を参照している。そこからコピーを始めると、それらは 表示できない。
ここで分岐がある。leading picture 自身が参照ピクチャかどうかで 扱いが正反対になる:
- MPEG-2: B ピクチャは決して参照されない。→ leading picture を 捨ててよい。オープン GOP でもコピー開始点として使える。
- H.264 / HEVC(x264 の
open-gopなど): B ピラミッドにより leading picture が参照ピクチャになりうる。→ 捨てると、それを参照して いた後続フレームが GOP 丸ごと壊れる。
これは実測で確認した。x264 open-gop 素材で leading picture を落としたところ、 コピー区間の先頭 GOP 60 フレームがすべて不一致になった(落とさなければ一致)。
そこで、nal_ref_idc(H.264)/ NAL 種別(HEVC)/ picture_coding_type
(MPEG-2)をビットストリームから読んで参照されているかを判定し、
参照されているならその点をコピー開始点として使わない。コピーの終了点
としてはオープン GOP でも問題なく使える(表示範囲が lead_start まで
になるだけ)。
leading picture の除去はコンテナでは表現できない(エディットリストが要る)
ため、Annex-B のアクセスユニット境界を自前で解析して切り出している
(bitstream.py)。H.264 は「先頭スライス=
ピクチャ開始」だが MPEG-2 は「picture start code のあとヘッダが数個続いて
からスライス」なので、AU の切り方が異なる点に注意。
秒数で -t を渡すと二重に狂う。
-c copyの-tは DTS で判定される。DTS は表示時刻をリオーダ深度 ぶん前倒ししたものなので、次の GOP の I/P が余分に紛れ込む (180 フレームのはずが 182 になった)。- 分数フレームレート(30000/1001)では端数の丸めで ±1 フレームずれる。
結局、表示フレーム数と、コピーすべきパケット数を整数で数えて渡すのが
唯一頑健だった(-frames:v N)。パケット数はアクセスポイント索引の
復号順インデックスの差から厳密に求まる。
TS のタイムスタンプは 0 から始まらない(検証素材では 1.423s)。-ss は
ファイル先頭基準なのに、ffmpeg は出力を -ss の値だけで再基準化するため、
start_time が残差オフセットとして出力タイムラインに残る。秒数ベースの
-t はこれをまともに被る。アクセスポイント時刻は start_time を引いて
正規化し、区間長はフレーム数で渡すことで両方回避している。
オープン GOP のソースでは、-ss で目的位置に直接シークすると ffmpeg は
復号できなかった GOP を丸ごと捨て、出力が最大 1 GOP 遅れて始まる
(実測 0.2s)。数 GOP 手前のアクセスポイントから復号を始め、出力側 -ss
で前方を切り捨てる。
音声は映像セグメントではなく保持区間単位で切る。
--audio-mode copy(既定): ソースのフレームをそのまま使う。区間境界は 最寄りの音声フレームにスナップされる(AAC で最大 ~24ms)。--audio-mode reencode:atrim+concatフィルタで 1 パス処理。 サンプル単位で正確だが再エンコードになる。
AAC の encoder delay / priming を edit list で厳密に処理する実装は未着手。
--verify の参照側を -ss で作ってはいけない。オープン GOP では
(#6 と同じ理由で)参照側自身がずれ、正しいカットを不正と誤判定する。
先頭から全復号してフレーム番号でスライスする。