mux/dts: re-base PTS per PES instead of a running clock (fix long-title drift)
The first cut used a global running clock (max(next, own-PES PTS) + advance), which fixed the same-PES collision but DRIFTED: once accumulated frame durations exceeded the PES-timestamp spacing, it never re-based, so a feature-long DVD DTS track ran minutes past its real length (2h44 for a 2h03 film) while AC-3 from the same source stayed exact. Match the AC-3 path: re-base to each PES's own container timestamp, and advance by one frame duration ONLY within a run of AUs sharing one PES. Fixes the DVD multi-frame-per-PES collision without drift; the UHD DTS-HD MA per-PES attribution (da85f56) is preserved (each AU still takes its own core PES's PTS). Adds new_pes_rebases_to_its_own_pts_no_drift; full mux suite green (905).
This commit is contained in:
+68
-15
@@ -38,15 +38,16 @@ pub struct DtsParser {
|
|||||||
/// current `buf` start and rebased whenever bytes are drained from the
|
/// current `buf` start and rebased whenever bytes are drained from the
|
||||||
/// front.
|
/// front.
|
||||||
pts_marks: Vec<(usize, i64)>,
|
pts_marks: Vec<(usize, i64)>,
|
||||||
/// Running monotonic timestamp: the PTS the NEXT emitted access unit must
|
/// The `front_pts` of the PREVIOUS emitted access unit. When the current
|
||||||
/// not fall below. Each AU is stamped `max(next_pts_ns, its own core PES
|
/// AU's `front_pts` differs, it began a new PES → re-base to it. When it is
|
||||||
/// PTS)` and then `next_pts_ns` advances by the AU's decoded duration. This
|
/// unchanged, this AU shares the previous AU's PES → advance one frame
|
||||||
/// is what makes the DVD case — several DTS core frames packed in ONE PES,
|
/// duration. This per-PES re-base (rather than a global running clock) is
|
||||||
/// all reporting that single PES PTS — advance frame-by-frame instead of
|
/// what keeps a feature-long DVD DTS track from drifting past its real
|
||||||
/// colliding on one timestamp (the "non monotonically increasing dts" mux
|
/// length. `PTS_UNSET` = no AU emitted yet.
|
||||||
/// error). The UHD/DTS-HD MA case (one AU per PES with its own later PTS) is
|
last_front_pts: i64,
|
||||||
/// unaffected: that PTS is `>= next_pts_ns`, so the AU keeps its own PES's
|
/// The PTS for the NEXT AU *if it shares the current PES* (the within-PES
|
||||||
/// timestamp exactly as before. `PTS_UNSET` = no base captured yet.
|
/// running cursor: previous emit + its duration). Only consulted when
|
||||||
|
/// `front_pts` is unchanged from `last_front_pts`. `PTS_UNSET` = no base yet.
|
||||||
next_pts_ns: i64,
|
next_pts_ns: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,24 +63,37 @@ impl DtsParser {
|
|||||||
buf: Vec::with_capacity(32768),
|
buf: Vec::with_capacity(32768),
|
||||||
pending_pts: 0,
|
pending_pts: 0,
|
||||||
pts_marks: Vec::new(),
|
pts_marks: Vec::new(),
|
||||||
|
last_front_pts: PTS_UNSET,
|
||||||
next_pts_ns: PTS_UNSET,
|
next_pts_ns: PTS_UNSET,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stamp an access unit with a monotonic PTS and advance the running clock.
|
/// Stamp an access unit's PTS. `front` is the AU's own core-PES PTS (from
|
||||||
/// `front` is the AU's own core-PES PTS (from [`front_pts`]); `dur_ns` its
|
/// [`front_pts`]); `dur_ns` its decoded duration.
|
||||||
/// decoded duration. Honors a forward jump to a new PES PTS (UHD multi-PES
|
///
|
||||||
/// AUs) but never emits below the running clock (DVD multi-AU-per-PES).
|
/// The model matches the (correct) AC-3 path: **re-base to each PES's own
|
||||||
|
/// container timestamp, and advance by one frame duration ONLY within a run
|
||||||
|
/// of AUs that share the same PES.** A new PES (its `front` differs from the
|
||||||
|
/// previous AU's) trusts its own timestamp — so the emitted timeline tracks
|
||||||
|
/// the container and never drifts. Advancing within a PES is what fixes the
|
||||||
|
/// DVD case (several DTS core frames packed in one PES, which otherwise all
|
||||||
|
/// collided on that single PES timestamp → "non monotonically increasing
|
||||||
|
/// dts"). A global running clock was WRONG here: once accumulated frame
|
||||||
|
/// durations exceeded the PES spacing it never re-based, drifting the track
|
||||||
|
/// minutes past the real length over a feature-long title.
|
||||||
fn stamp_pts(&mut self, front: i64, dur_ns: i64) -> i64 {
|
fn stamp_pts(&mut self, front: i64, dur_ns: i64) -> i64 {
|
||||||
let base = if front != PTS_UNSET && front > self.next_pts_ns {
|
let base = if front != PTS_UNSET && front != self.last_front_pts {
|
||||||
|
// New PES (or the first AU): trust its own timestamp — no drift.
|
||||||
front
|
front
|
||||||
} else if self.next_pts_ns != PTS_UNSET {
|
} else if self.next_pts_ns != PTS_UNSET {
|
||||||
|
// Same PES as the previous AU (front unchanged) → advance one frame.
|
||||||
self.next_pts_ns
|
self.next_pts_ns
|
||||||
} else if front != PTS_UNSET {
|
} else if front != PTS_UNSET {
|
||||||
front
|
front
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
self.last_front_pts = front;
|
||||||
self.next_pts_ns = base + dur_ns;
|
self.next_pts_ns = base + dur_ns;
|
||||||
base
|
base
|
||||||
}
|
}
|
||||||
@@ -167,8 +181,9 @@ impl CodecParser for DtsParser {
|
|||||||
self.pts_marks.clear();
|
self.pts_marks.clear();
|
||||||
self.pending_pts = PTS_UNSET;
|
self.pending_pts = PTS_UNSET;
|
||||||
// A concealed gap is a timeline discontinuity: let the post-gap AU
|
// A concealed gap is a timeline discontinuity: let the post-gap AU
|
||||||
// re-base to its own (later) PES PTS rather than the pre-gap clock.
|
// re-base to its own PES PTS rather than the pre-gap cursor.
|
||||||
self.next_pts_ns = PTS_UNSET;
|
self.next_pts_ns = PTS_UNSET;
|
||||||
|
self.last_front_pts = PTS_UNSET;
|
||||||
}
|
}
|
||||||
if pes.data.is_empty() {
|
if pes.data.is_empty() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
@@ -900,6 +915,44 @@ mod tests {
|
|||||||
assert_eq!(dts_core_sample_rate(&core), 48_000);
|
assert_eq!(dts_core_sample_rate(&core), 48_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn new_pes_rebases_to_its_own_pts_no_drift() {
|
||||||
|
// Regression for the drift bug: a global running clock overshot a
|
||||||
|
// feature-long DTS track by minutes (2h44 for a 2h03 film). When a NEW
|
||||||
|
// PES arrives whose PTS is BEHIND where accumulated frame durations
|
||||||
|
// would put a running clock, the AU must re-base to that PES's OWN
|
||||||
|
// timestamp — tracking the container, not drifting ahead of it.
|
||||||
|
let mut parser = DtsParser::new();
|
||||||
|
// PES A: core1 + core2 (2 frames), pts 90000.
|
||||||
|
let mut pes_a = make_dts_core(512);
|
||||||
|
pes_a.extend_from_slice(&make_dts_core(600));
|
||||||
|
let f = parser.parse(&make_pes(pes_a, Some(90000)));
|
||||||
|
assert_eq!(f.len(), 1, "AU1 (core1) emits on the core2 boundary");
|
||||||
|
assert_eq!(f[0].pts_ns, pts_to_ns(90000), "AU1 = PES A base");
|
||||||
|
// PES B: core3, pts only 500 ticks after PES A — LESS than one frame
|
||||||
|
// (960 ticks @ 48 kHz). AU2 (core2, still PES A) advances within PES A;
|
||||||
|
// AU3 (core3, PES B) must RE-BASE to PES B's own PTS.
|
||||||
|
let f2 = parser.parse(&make_pes(make_dts_core(640), Some(90500)));
|
||||||
|
assert_eq!(f2.len(), 1, "AU2 (core2) closes on core3");
|
||||||
|
assert_eq!(
|
||||||
|
f2[0].pts_ns,
|
||||||
|
pts_to_ns(90000) + DTS_CORE_DUR_NS,
|
||||||
|
"AU2 = 2nd frame of PES A → advances one frame within PES A"
|
||||||
|
);
|
||||||
|
let tail = parser.flush();
|
||||||
|
assert_eq!(tail.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
tail[0].pts_ns,
|
||||||
|
pts_to_ns(90500),
|
||||||
|
"AU3 = core3 in PES B → re-bases to PES B's PTS"
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
tail[0].pts_ns,
|
||||||
|
pts_to_ns(90000) + 2 * DTS_CORE_DUR_NS,
|
||||||
|
"must NOT carry the accumulated running clock across a PES (drift)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Build a minimal DTS-HD extension substream of `size` bytes (just the
|
/// Build a minimal DTS-HD extension substream of `size` bytes (just the
|
||||||
/// sync + zero-padding). The parser delimits extensions by the next CORE
|
/// sync + zero-padding). The parser delimits extensions by the next CORE
|
||||||
/// sync, not by the extension's own size header, so a valid header isn't
|
/// sync, not by the extension's own size header, so a valid header isn't
|
||||||
|
|||||||
Reference in New Issue
Block a user