Skip to content

Latest commit

 

History

History
131 lines (94 loc) · 7.19 KB

File metadata and controls

131 lines (94 loc) · 7.19 KB

アルゴリズムと実装上の難所

← ドキュメント一覧← smartcut

アルゴリズム

保持区間 [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 単位で切って繋ぐだけ」で済まない理由。どれも試作中に実際に踏んで、 テストで再現を固定してある。

1. パラメータセット(SPS/PPS)が一致しない

再エンコード部の SPS を元ストリームとビット単位で一致させることは事実上 不可能(エンコーダが違えば VUI も VBV も変わる)。ところが MP4 の avcC box も Matroska の CodecPrivate もパラメータセットを 1 組しか持てない。 素朴に連結すると、コピー部か再エンコード部のどちらかが誤った SPS で 復号され、映像が壊れる。

対策は 2 段構え:

  • 各ピースを raw Annex-B エレメンタリストリームとして書き出し、 単純なバイト連結で繋ぐ。ES は IDR ごとに SPS/PPS をインバンドで持つので、 ピースごとに異なるパラメータセットを合法的に共存させられる。
  • 最終 MP4 は avc3 / hev1 sample entry で書く。これは ISO/IEC 14496-15 が定めるインバンド方式で、avcC に畳み込まれない。

MPEG-2 はシーケンスヘッダが元々インバンドなので、この問題自体が無い。

2. アクセスポイント索引は「パケット」を走査する

ffprobe -skip_frame nokeyオープン GOP で取りこぼす。参照が欠けた I ピクチャをデコーダが出力できないため。試作の検証素材では、実際には 10 個 あるアクセスポイントのうち 3 個しか拾えていなかった。

パケットの K フラグを見れば復号せずに済み、速く、かつ正確。

3. leading picture — オープン GOP の本丸

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 の切り方が異なる点に注意。

4. 区間指定は秒ではなくフレーム数・パケット数で

秒数で -t を渡すと二重に狂う。

  • -c copy-tDTS で判定される。DTS は表示時刻をリオーダ深度 ぶん前倒ししたものなので、次の GOP の I/P が余分に紛れ込む (180 フレームのはずが 182 になった)。
  • 分数フレームレート(30000/1001)では端数の丸めで ±1 フレームずれる。

結局、表示フレーム数と、コピーすべきパケット数を整数で数えて渡すのが 唯一頑健だった(-frames:v N)。パケット数はアクセスポイント索引の 復号順インデックスの差から厳密に求まる。

5. コンテナの start_time(MPEG-TS)

TS のタイムスタンプは 0 から始まらない(検証素材では 1.423s)。-ss は ファイル先頭基準なのに、ffmpeg は出力を -ss の値だけで再基準化するため、 start_time が残差オフセットとして出力タイムラインに残る。秒数ベースの -t はこれをまともに被る。アクセスポイント時刻は start_time を引いて 正規化し、区間長はフレーム数で渡すことで両方回避している。

6. 再エンコード区間は「手前から」復号する

オープン GOP のソースでは、-ss で目的位置に直接シークすると ffmpeg は 復号できなかった GOP を丸ごと捨て、出力が最大 1 GOP 遅れて始まる (実測 0.2s)。数 GOP 手前のアクセスポイントから復号を始め、出力側 -ss で前方を切り捨てる。

7. 音声は GOP 構造を持たない

音声は映像セグメントではなく保持区間単位で切る。

  • --audio-mode copy(既定): ソースのフレームをそのまま使う。区間境界は 最寄りの音声フレームにスナップされる(AAC で最大 ~24ms)。
  • --audio-mode reencode: atrim + concat フィルタで 1 パス処理。 サンプル単位で正確だが再エンコードになる。

AAC の encoder delay / priming を edit list で厳密に処理する実装は未着手。

8. 検証の参照はファイル先頭から全復号する

--verify の参照側を -ss で作ってはいけない。オープン GOP では (#6 と同じ理由で)参照側自身がずれ、正しいカットを不正と誤判定する。 先頭から全復号してフレーム番号でスライスする。