From 835e97ce71a5aa1b6cdbc48d51ae97c6baa448b2 Mon Sep 17 00:00:00 2001
From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com>
Date: Sat, 8 Aug 2026 00:48:10 -0700
Subject: [PATCH] Give a frame that outruns its epoch's video a provisional
offset
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The demuxer interleaves, so at a boundary the streams do not reset on the
same frame: audio for the next segment can reach the muxer before the video
frame that opens the epoch it belongs to. Only the primary video track may
open one, so those frames rode the just-ended offset and landed a whole
segment in the past. Downstream the strictly-monotonic block nudge then
crushed the entire run onto one instant a tick apart, which is audible.
Such a frame is recognised on its OWN raw PTS, which within an epoch only
advances, so a large backward step is unambiguous — a different signal from
the shared frontier, which is what the old false-positive ratchet keyed on.
It then rides a provisional offset private to its track, computed the same
way the video path computes a real one, and drops it the moment the video
retires an epoch, so the run rejoins with no seam.
The offset is deliberately private: it never writes offset_ns, never
advances the frontier and never retires an epoch, so it cannot move the
video timeline. Letting a passive track open a REAL epoch was tried first
and inflated a 476.776 s title to 656.216 s, because every track meets a
boundary at its own pace and the video path rebased again on top.
Measured on a real DVD title with 8 cell boundaries: audio frames stamped
inside one cadence 120 -> 0, outsized gaps 7 -> 0, subtitle 1 -> 0, and the
video span byte-identical at 476.484. On the HD-DVD title the remaining
boundary gap on its first audio track falls from 0.999 s to 0.275 s.
Acceptance: 76 pass, 0 fail, 0 skip.
---
src/mux/demux_sink.rs | 10 +--
src/mux/timeline.rs | 157 ++++++++++++++++++++++++++++++++++++++++--
2 files changed, 157 insertions(+), 10 deletions(-)
diff --git a/src/mux/demux_sink.rs b/src/mux/demux_sink.rs
index 8426421..9f39f32 100644
--- a/src/mux/demux_sink.rs
+++ b/src/mux/demux_sink.rs
@@ -1543,14 +1543,14 @@ mod tests {
fn timeline_track0_drives_epoch_others_ride() {
let mut tl = TimelineContinuity::new();
// Clip 1: video 0..10s (track 0 drives the epoch).
- assert_eq!(tl.adjust(0, true), 0);
- assert_eq!(tl.adjust(0, false), 0); // audio rides the same offset
- assert_eq!(tl.adjust(10_000_000_000, true), 10_000_000_000);
+ assert_eq!(tl.adjust(0, true, 0), 0);
+ assert_eq!(tl.adjust(0, false, 1), 0); // audio rides the same offset
+ assert_eq!(tl.adjust(10_000_000_000, true, 0), 10_000_000_000);
// Clip 2 seam: video PTS jumps back to ~0 (> 3s back) → new epoch.
- let out = tl.adjust(0, true);
+ let out = tl.adjust(0, true, 0);
assert!(out >= 10_000_000_000, "epoch must advance past prev high");
// Audio in clip 2 (non-epoch) gets the SAME offset (A/V sync preserved).
- let a = tl.adjust(0, false);
+ let a = tl.adjust(0, false, 1);
assert_eq!(a, out);
}
diff --git a/src/mux/timeline.rs b/src/mux/timeline.rs
index 9d899c7..caf3972 100644
--- a/src/mux/timeline.rs
+++ b/src/mux/timeline.rs
@@ -658,6 +658,24 @@ pub(crate) struct TimelineContinuity {
/// frontier, which by then may be a whole title away. Bounded: a source that
/// rebases forever must not grow this.
epoch_offsets: Vec<(i64, i64)>,
+ /// Last raw PTS seen per track, for spotting a track's OWN discontinuity.
+ /// Within an epoch a passive track's PTS only advances (audio and subtitles
+ /// do not reorder, and a passive video track's B-frame dip is far under the
+ /// backstep), so a large BACKWARD step is unambiguous. This is a different
+ /// signal from the shared frontier, which is what the old false-positive
+ /// ratchet keyed on.
+ last_raw_ns: Vec