wip: top gun EL/decrypt follow-up (in progress, rc3)
This commit is contained in:
+81
-36
@@ -391,44 +391,48 @@ impl TimelineContinuity {
|
|||||||
|
|
||||||
/// Map a raw PES PTS (ns) onto the continuous output timeline.
|
/// Map a raw PES PTS (ns) onto the continuous output timeline.
|
||||||
///
|
///
|
||||||
/// `is_video` gates EVERY epoch decision. Only video may advance the frontier
|
/// `drives_epoch` gates EVERY epoch decision. It is `true` for the PRIMARY
|
||||||
/// or open a new epoch; non-video tracks are passive riders.
|
/// video track (base layer, track 0) ONLY. Every other track — audio, PGS
|
||||||
|
/// subtitle, and a second video track such as a Dolby Vision enhancement
|
||||||
|
/// layer — passes `false` and is a passive rider. (The DV EL is video but
|
||||||
|
/// runs its own PTS timeline interleaved with the base layer's; letting it
|
||||||
|
/// drive epochs would false-trigger a reset on every GOP.)
|
||||||
///
|
///
|
||||||
/// **Non-video tracks** (`is_video == false`) — audio, PGS subtitle. Always
|
/// **Passive tracks** (`drives_epoch == false`). Always remapped under the
|
||||||
/// remapped under the CURRENT offset. They never advance `high_ns`, never
|
/// CURRENT offset. They never advance `high_ns`, never trigger a clip-boundary
|
||||||
/// trigger a clip-boundary reset, and never bump `offset_ns`. This is what
|
/// reset, and never bump `offset_ns`. This is what kills the single-clip
|
||||||
/// kills the single-clip ratchet: a sparse, lagging subtitle/audio PTS can
|
/// ratchet: a sparse/lagging subtitle/audio PTS, or an interleaved EL frame,
|
||||||
/// no longer push the frontier up and make the next video frame look like a
|
/// can no longer push the frontier up and make the next base-video frame look
|
||||||
/// boundary. A/V sync is preserved because the offset they ride is the same
|
/// like a boundary. A/V sync is preserved because the offset they ride is the
|
||||||
/// one video established for the epoch.
|
/// same one the base video established for the epoch.
|
||||||
///
|
///
|
||||||
/// **Video track** (`is_video == true`):
|
/// **Primary video** (`drives_epoch == true`):
|
||||||
/// - **Backward jump > `DISCONTINUITY_BACKSTEP_NS`** vs the frontier =
|
/// - **Backward jump > `DISCONTINUITY_BACKSTEP_NS`** vs the frontier =
|
||||||
/// clip-boundary reset: open a new epoch (bump the offset so this frame
|
/// clip-boundary reset: open a new epoch (bump the offset so this frame
|
||||||
/// continues just after the frontier). This is the genuine multi-clip
|
/// continues just after the frontier). This is the genuine multi-clip
|
||||||
/// seamless rebasing, now driven only by real video back-jumps.
|
/// seamless rebasing, now driven only by real base-video back-jumps.
|
||||||
/// - **Everything else** (normal progression + sub-threshold B-frame reorder
|
/// - **Everything else** (normal progression + sub-threshold B-frame reorder
|
||||||
/// dips) passes through with the current offset and advances the frontier,
|
/// dips) passes through with the current offset and advances the frontier,
|
||||||
/// preserving PTS.
|
/// preserving PTS.
|
||||||
fn adjust(&mut self, raw_pts_ns: i64, is_video: bool) -> i64 {
|
fn adjust(&mut self, raw_pts_ns: i64, drives_epoch: bool) -> i64 {
|
||||||
// Non-video: ride the current epoch's offset. Never advance the frontier
|
// Passive track: ride the current epoch's offset. Never advance the
|
||||||
// and never open an epoch — these tracks are too sparse/laggy to make a
|
// frontier and never open an epoch — these tracks each run on their own
|
||||||
// reliable boundary signal and would false-trigger the ratchet.
|
// (sparse/laggy/independent) timeline and would false-trigger the ratchet.
|
||||||
if !is_video {
|
if !drives_epoch {
|
||||||
let mapped = raw_pts_ns.saturating_add(self.offset_ns);
|
let mapped = raw_pts_ns.saturating_add(self.offset_ns);
|
||||||
// Tail-straggler remap: at a REAL (video-driven) multi-clip boundary
|
// Tail-straggler remap: at a REAL (base-video-driven) multi-clip
|
||||||
// the offset has just jumped forward by ~a whole clip, but a lagging
|
// boundary the offset has just jumped forward by ~a whole clip, but a
|
||||||
// tail frame from the just-ended clip still carries an OLD-epoch raw
|
// lagging tail frame from the just-ended clip still carries an
|
||||||
// PTS. Adding the NEW offset flings it ~a clip past the frontier and
|
// OLD-epoch raw PTS. Adding the NEW offset flings it ~a clip past the
|
||||||
// would force a forward-dated split cluster (breaking cluster
|
// frontier and would force a forward-dated split cluster (breaking
|
||||||
// monotonicity). Such a straggler is recognised precisely: its
|
// cluster monotonicity). Such a straggler is recognised precisely: its
|
||||||
// current-offset mapping lands more than a backstep PAST the frontier
|
// current-offset mapping lands more than a backstep PAST the frontier
|
||||||
// AND its PREVIOUS-offset mapping lands at/below the frontier (i.e. it
|
// AND its PREVIOUS-offset mapping lands at/below the frontier (i.e. it
|
||||||
// belongs to the prior epoch). Remap it with the previous offset so
|
// belongs to the prior epoch). Remap it with the previous offset so
|
||||||
// it lands at its true seam position. This is what distinguishes a
|
// it lands at its true seam position. This is what distinguishes a
|
||||||
// tail straggler from a frame that legitimately runs ahead of the
|
// tail straggler from a frame that legitimately runs ahead of the
|
||||||
// (video-only) frontier — a long audio-only tail, or a sparse
|
// (base-video-only) frontier — a long audio-only tail, a sparse
|
||||||
// subtitle — which is left on the current offset.
|
// subtitle, or an EL frame — which is left on the current offset.
|
||||||
if let Some(high) = self.high_ns {
|
if let Some(high) = self.high_ns {
|
||||||
if mapped > high + DISCONTINUITY_BACKSTEP_NS {
|
if mapped > high + DISCONTINUITY_BACKSTEP_NS {
|
||||||
let prev_mapped = raw_pts_ns.saturating_add(self.prev_offset_ns);
|
let prev_mapped = raw_pts_ns.saturating_add(self.prev_offset_ns);
|
||||||
@@ -767,22 +771,29 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
|||||||
data: &[u8],
|
data: &[u8],
|
||||||
duration_ns: Option<u64>,
|
duration_ns: Option<u64>,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
// Is this a video track? Needed both for the continuity epoch decision
|
// Is this a video track? Used for the monotonic block-timestamp nudge
|
||||||
// (only video may open/advance an epoch — non-video tracks are too
|
// below, which must exempt EVERY video track (incl. a Dolby Vision EL).
|
||||||
// sparse/laggy to be a reliable clip-boundary signal and would otherwise
|
|
||||||
// false-trigger the rebase on single-clip titles) and for the monotonic
|
|
||||||
// block-timestamp nudge below.
|
|
||||||
let is_video = self.track_is_video.get(track_idx).copied().unwrap_or(false);
|
let is_video = self.track_is_video.get(track_idx).copied().unwrap_or(false);
|
||||||
|
|
||||||
|
// The clip-boundary epoch decision is driven by the PRIMARY video track
|
||||||
|
// ONLY (track 0). A title can carry a SECOND video track — a Dolby Vision
|
||||||
|
// enhancement layer — whose PTS runs on its OWN timeline, interleaved
|
||||||
|
// with the base layer's. The two video PTS sequences overlap, so the EL's
|
||||||
|
// frames look like multi-second backward jumps against the base layer's
|
||||||
|
// frontier and would false-trigger an epoch reset on every GOP (the exact
|
||||||
|
// ratchet that inflated Top Gun's 1-clip timeline to ~7 h). Only the base
|
||||||
|
// video layer establishes/advances the frontier and opens epochs; the EL
|
||||||
|
// — like audio and subtitles — rides the current offset.
|
||||||
|
let drives_epoch = track_idx == 0;
|
||||||
|
|
||||||
// Map the raw PES PTS onto the continuous output timeline FIRST, before
|
// Map the raw PES PTS onto the continuous output timeline FIRST, before
|
||||||
// any base/cluster math: freemkv concatenates a title's BD clips as one
|
// any base/cluster math: at a non-seamless clip / layer-break boundary
|
||||||
// sector stream, so a non-seamless clip / layer-break boundary arrives
|
// the source PES PTS jumps backward. Rebasing it (a global offset across
|
||||||
// here as a large backward PTS jump. Rebasing it (a global offset across
|
|
||||||
// all tracks, A/V-sync-preserving) keeps the boundary from becoming a
|
// all tracks, A/V-sync-preserving) keeps the boundary from becoming a
|
||||||
// band of non-monotonic block timestamps. Only VIDEO drives the
|
// band of non-monotonic block timestamps. Only the PRIMARY video track
|
||||||
// boundary decision; non-video tracks ride the current offset. No-op for
|
// drives the boundary decision; every other track (audio, subtitle, DV
|
||||||
// single-clip titles.
|
// EL) rides the current offset. No-op for single-clip titles.
|
||||||
let pts_ns = self.continuity.adjust(pts_ns, is_video);
|
let pts_ns = self.continuity.adjust(pts_ns, drives_epoch);
|
||||||
let raw_ticks = pts_ns / TIMESTAMP_SCALE_NS;
|
let raw_ticks = pts_ns / TIMESTAMP_SCALE_NS;
|
||||||
|
|
||||||
// Cluster boundaries normally coincide with a video keyframe so every
|
// Cluster boundaries normally coincide with a video keyframe so every
|
||||||
@@ -1450,6 +1461,40 @@ mod tests {
|
|||||||
assert!(max_out <= 60 * S, "no timeline inflation, max={max_out}");
|
assert!(max_out <= 60 * S, "no timeline inflation, max={max_out}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// PRIMARY rc3 regression (Dolby Vision dual-layer): a SECOND video track —
|
||||||
|
/// the DV enhancement layer — runs its OWN PTS timeline interleaved with the
|
||||||
|
/// base layer's, so the two video PTS sequences OVERLAP. The EL must be a
|
||||||
|
/// PASSIVE rider (drives_epoch == false): if it drove epochs, every EL GOP
|
||||||
|
/// would look like a multi-second backward jump against the base-layer
|
||||||
|
/// frontier and false-trigger a clip-boundary reset — the exact ratchet that
|
||||||
|
/// inflated Top Gun's 1-clip 1h49m timeline to ~7 h. Here the base layer
|
||||||
|
/// advances 0..60s while the EL re-emits the SAME 0..60s interleaved; the
|
||||||
|
/// timeline must stay at 60s with offset 0.
|
||||||
|
#[test]
|
||||||
|
fn dv_enhancement_layer_does_not_drive_epochs() {
|
||||||
|
let mut tc = TimelineContinuity::new();
|
||||||
|
let mut max_out = i64::MIN;
|
||||||
|
for sec in 0..=60 {
|
||||||
|
// Base layer (track 0) drives the epoch.
|
||||||
|
let bl = adj_video(&mut tc, sec * S);
|
||||||
|
// EL (track 1) re-emits the same time — a passive rider. Its raw PTS
|
||||||
|
// equals the base layer's, but it arrives just AFTER the base frame
|
||||||
|
// for the NEXT second sometimes; simulate the overlap by feeding the
|
||||||
|
// PREVIOUS second's time, which is a backward swing vs the frontier.
|
||||||
|
let el_raw = if sec > 0 { (sec - 1) * S } else { 0 };
|
||||||
|
let el = adj_other(&mut tc, el_raw);
|
||||||
|
assert_eq!(el, el_raw, "EL rides current offset, true PTS preserved");
|
||||||
|
max_out = max_out.max(bl).max(el);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
tc.offset_ns, 0,
|
||||||
|
"DV EL interleave must not ratchet offset (was {})",
|
||||||
|
tc.offset_ns
|
||||||
|
);
|
||||||
|
assert_eq!(tc.high_ns, Some(60 * S), "frontier tracks base video only");
|
||||||
|
assert!(max_out <= 60 * S, "no timeline inflation, max={max_out}");
|
||||||
|
}
|
||||||
|
|
||||||
/// Companion: a non-video frame must never ADVANCE the frontier. Even a
|
/// Companion: a non-video frame must never ADVANCE the frontier. Even a
|
||||||
/// non-video PTS far ABOVE the current video frontier (a subtitle/audio
|
/// non-video PTS far ABOVE the current video frontier (a subtitle/audio
|
||||||
/// timestamp that leads the video momentarily) leaves `high_ns` untouched,
|
/// timestamp that leads the video momentarily) leaves `high_ns` untouched,
|
||||||
|
|||||||
Reference in New Issue
Block a user