From 5fdff5664f2e073c45d299ef6c6bddd9310bb1f7 Mon Sep 17 00:00:00 2001 From: Matthew Jackson <1085847+MattJackson@users.noreply.github.com> Date: Wed, 8 Jul 2026 21:54:09 -0700 Subject: [PATCH] mux: reconstruct display-order PTS for sparse-PTS program streams HD-DVD EVO (and DVD VOB) program streams timestamp video at GOP granularity: only one access unit per GOP carries a PES PTS. The H.264 / HEVC / VC-1 parsers collapsed a missing PTS to 0, so on such a source every non-anchor frame landed on the same block timestamp and a decoder reported "non monotonically increasing dts". Add a shared SparsePtsReorder that rebuilds a display-order PTS per frame from the coded picture type (I/P/B) plus the sparse anchor PTS, with a per-frame duration self-calibrated from the spacing between consecutive GOP anchors (no external frame-rate needed). Display order is derived via the classic single-anchor-delay rule (an anchor displays only after the previously-held anchor; a B displays immediately), exact for the non-hierarchical GOP structures HD-DVD H.264/VC-1 use. It mirrors the MPEG-2 parser's GOP-buffered origin-locking. Gated to the program-stream path only: the three parsers enable it via with_ps_reorder(is_dvd_ps), so the BD/UHD transport path (per-frame PTS) is byte-identical and untouched. --- src/mux/codec/h264.rs | 103 ++++++++++++- src/mux/codec/hevc.rs | 38 ++++- src/mux/codec/mod.rs | 12 +- src/mux/codec/reorder.rs | 318 +++++++++++++++++++++++++++++++++++++++ src/mux/codec/vc1.rs | 38 ++++- 5 files changed, 497 insertions(+), 12 deletions(-) create mode 100644 src/mux/codec/reorder.rs diff --git a/src/mux/codec/h264.rs b/src/mux/codec/h264.rs index 32976ea..f32bbea 100644 --- a/src/mux/codec/h264.rs +++ b/src/mux/codec/h264.rs @@ -52,6 +52,10 @@ pub struct H264Parser { // the stale avcC copy after a mid-title redefinition. cur_sps: Option>, cur_pps: Option>, + /// Display-order PTS reconstruction, enabled only on the program-stream + /// (HD-DVD EVO) path where the source stamps a PTS once per GOP. `None` on + /// the BD/UHD transport path, which carries a per-frame PTS. + reorder: Option, } impl Default for H264Parser { @@ -68,6 +72,25 @@ impl H264Parser { pps: None, cur_sps: None, cur_pps: None, + reorder: None, + } + } + + /// Enable display-order PTS reconstruction for a program-stream source. + /// No-op (leaves timestamps as parsed) for a transport-stream source. + pub(crate) fn with_ps_reorder(mut self, enabled: bool) -> Self { + if enabled { + self.reorder = Some(super::reorder::SparsePtsReorder::new()); + } + self + } + + /// Route a finished frame through the PTS reorderer when enabled, else emit + /// it directly (unchanged transport-stream behaviour). + fn finish(&mut self, explicit: Option, frame: Frame) -> Vec { + match self.reorder.as_mut() { + Some(r) => r.push(explicit, frame), + None => vec![frame], } } } @@ -152,7 +175,8 @@ impl CodecParser for H264Parser { // decode order and the player reorders by timecode. Use PTS, not DTS — // DTS presents B-frames in decode order (visible judder) and breaks // PTS-based seeking. Fall back to DTS only if PTS is absent. - let pts_ns = pes.pts.or(pes.dts).map(pts_to_ns).unwrap_or(0); + let explicit_pts = pes.pts.or(pes.dts).map(pts_to_ns); + let pts_ns = explicit_pts.unwrap_or(0); // Single pass: detect IDR keyframes, seed/strip param sets, and convert // Annex B (start-code prefixed) NALUs to length-prefixed NALUs (MKV with @@ -236,7 +260,7 @@ impl CodecParser for H264Parser { } } - vec![Frame { + let frame = Frame { // Coding-type only: H.264 field order is not decoded here, so // `field_order()` stays `None` — honestly absent, never guessed. coding: coding_type.map(PictureInfo::coding_type_only), @@ -248,7 +272,15 @@ impl CodecParser for H264Parser { discontinuity: pes.discontinuity, data: frame_data, duration_ns: None, - }] + }; + self.finish(explicit_pts, frame) + } + + fn flush(&mut self) -> Vec { + match self.reorder.as_mut() { + Some(r) => r.flush(), + None => Vec::new(), + } } fn codec_private(&self) -> Option> { @@ -667,6 +699,71 @@ mod tests { ); } + /// End-to-end sparse-PTS reconstruction through the REAL parser + reorder: + /// a program-stream source (`with_ps_reorder(true)`) that stamps a PTS only + /// on each GOP's I-frame must yield distinct, display-ordered PTS for every + /// frame — the property the mkv muxer needs so a decoder derives monotonic + /// DTS. Without the reorder the non-anchor frames all collapse to one PTS. + #[test] + fn h264_ps_reorder_reconstructs_distinct_display_pts() { + use super::super::coding::CodingType; + // slice bodies: 0x88 → I (IDR), 0x98 → P, 0x9C → B (non-IDR). + // Decode order of a classic single-B GOP: I P B P B. + let gop = |anchor_pts: Option| { + vec![ + (NAL_SLICE_IDR, 0x88u8, anchor_pts), + (NAL_SLICE_NON_IDR, 0x98, None), + (NAL_SLICE_NON_IDR, 0x9C, None), + (NAL_SLICE_NON_IDR, 0x98, None), + (NAL_SLICE_NON_IDR, 0x9C, None), + ] + }; + + let feed = |reorder: bool| -> Vec { + let mut p = H264Parser::new().with_ps_reorder(reorder); + let mut out = Vec::new(); + // Two GOPs; the second I carries an anchor 5 frames later (90 kHz: + // 5 * 3750 = 18750 ticks) so the reorder can calibrate a duration. + for (nal, body, pts) in gop(Some(0)).into_iter().chain(gop(Some(18750))) { + out.extend(p.parse(&make_pes(h264_nal(nal, &[body]), pts))); + } + out.extend(p.flush()); + out + }; + + // With reorder ON: all 10 frames emitted, every PTS distinct. + let recon = feed(true); + assert_eq!(recon.len(), 10, "no frame dropped"); + let mut pts: Vec = recon.iter().map(|f| f.pts_ns).collect(); + let n = pts.len(); + pts.sort_unstable(); + pts.dedup(); + assert_eq!( + pts.len(), + n, + "reconstructed PTS are all distinct (no DTS collision)" + ); + + // The GOP's first-displayed frame is the I; the B in decode position 2 + // must display BEFORE the P in decode position 1 (classic reorder). + let g1 = &recon[0..5]; + assert_eq!(g1[0].coding.unwrap().coding_type(), CodingType::I); + assert!( + g1[2].pts_ns < g1[1].pts_ns, + "B (decode idx 2) displays before its forward-anchor P (decode idx 1)" + ); + assert_eq!(g1[0].pts_ns, 0, "GOP anchor locks the I to its true PTS"); + + // With reorder OFF (transport-stream behaviour): the non-anchor frames + // collapse to a single colliding PTS — the bug this fix removes. + let raw = feed(false); + let collisions = raw.iter().filter(|f| f.pts_ns == 0).count(); + assert!( + collisions >= 8, + "without reorder the sparse-PTS frames collide on 0 (got {collisions})" + ); + } + /// Regression (Fight Club bug, H.264 variant): PPS id 0 = body A (→ avcC), /// redefined to B, then switched BACK to A. A streaming decoder is on B; the /// revert to A == avcC must still be emitted in-band or the A-segment diff --git a/src/mux/codec/hevc.rs b/src/mux/codec/hevc.rs index da59e15..a5378ec 100644 --- a/src/mux/codec/hevc.rs +++ b/src/mux/codec/hevc.rs @@ -166,6 +166,10 @@ pub struct HevcParser { // colour-volume metadata is ever fabricated. sei_mastering: Option, sei_content_light: Option, + /// Display-order PTS reconstruction, enabled only on the program-stream + /// path where the source stamps a PTS once per GOP. `None` on the BD/UHD + /// transport path (the common HEVC case), which carries a per-frame PTS. + reorder: Option, } /// Mastering Display Colour Volume payload (Rec. ITU-T H.265 D.2.28), @@ -234,6 +238,25 @@ impl HevcParser { pts_wrap_offset: 0, sei_mastering: None, sei_content_light: None, + reorder: None, + } + } + + /// Enable display-order PTS reconstruction for a program-stream source. + /// No-op (leaves timestamps as parsed) for a transport-stream source. + pub(crate) fn with_ps_reorder(mut self, enabled: bool) -> Self { + if enabled { + self.reorder = Some(super::reorder::SparsePtsReorder::new()); + } + self + } + + /// Route a finished frame through the PTS reorderer when enabled, else emit + /// it directly (unchanged transport-stream behaviour). + fn finish(&mut self, explicit: Option, frame: Frame) -> Vec { + match self.reorder.as_mut() { + Some(r) => r.push(explicit, frame), + None => vec![frame], } } @@ -443,7 +466,8 @@ impl CodecParser for HevcParser { // block timecode monotonic in storage order, which presents B-frames in // decode order (visible judder / wrong frames) and breaks PTS-based // seeking. Fall back to DTS only if PTS is somehow absent. - let pts_ns = pes.pts.or(pes.dts).map(pts_to_ns).unwrap_or(0); + let explicit_pts = pes.pts.or(pes.dts).map(pts_to_ns); + let pts_ns = explicit_pts.unwrap_or(0); // Auto-detect a non-seamless clip boundary from the bitstream. freemkv // reads a BD title's clips as ONE concatenated sector stream and the @@ -651,7 +675,7 @@ impl CodecParser for HevcParser { // from the first coded picture before writing the track header). `None` // until both SEI present → SDR / no-SEI tracks carry nothing. let hdr10 = self.hdr10(); - vec![Frame { + let frame = Frame { // Coding-type only: HEVC field order (pic_struct, from a pic_timing // SEI) is not decoded here, so field_order() stays None — honestly // absent, never guessed. HDR10 metadata is attached when measured. @@ -666,7 +690,15 @@ impl CodecParser for HevcParser { discontinuity: pes.discontinuity, data: frame_data, duration_ns: None, - }] + }; + self.finish(explicit_pts, frame) + } + + fn flush(&mut self) -> Vec { + match self.reorder.as_mut() { + Some(r) => r.flush(), + None => Vec::new(), + } } fn codec_private(&self) -> Option> { diff --git a/src/mux/codec/mod.rs b/src/mux/codec/mod.rs index 54895ea..3902015 100644 --- a/src/mux/codec/mod.rs +++ b/src/mux/codec/mod.rs @@ -25,6 +25,8 @@ pub mod lpcm; pub mod mpeg2; /// HDMV PGS (Presentation Graphics Stream) subtitle parser. pub mod pgs; +/// Display-order PTS reconstruction for sparse-PTS program-stream video. +pub(crate) mod reorder; /// Shared MPEG/Annex-B start-code scanning helpers. pub(crate) mod startcode; /// Dolby TrueHD / Atmos elementary-stream parser. @@ -166,10 +168,14 @@ pub fn parser_for_codec( is_dvd_ps: bool, ) -> Box { match codec { - Codec::H264 => Box::new(h264::H264Parser::new()), - Codec::Hevc => Box::new(hevc::HevcParser::new()), + // `is_dvd_ps` marks a program-stream source (DVD VOB / HD-DVD EVO), whose + // video is timestamped only at GOP granularity. On that path the H.264 / + // HEVC / VC-1 parsers reconstruct a display-order PTS per frame; on the + // BD/UHD transport path (per-frame PTS) they leave timestamps untouched. + Codec::H264 => Box::new(h264::H264Parser::new().with_ps_reorder(is_dvd_ps)), + Codec::Hevc => Box::new(hevc::HevcParser::new().with_ps_reorder(is_dvd_ps)), Codec::Mpeg2 => Box::new(mpeg2::Mpeg2Parser::new()), - Codec::Vc1 => Box::new(vc1::Vc1Parser::new()), + Codec::Vc1 => Box::new(vc1::Vc1Parser::new().with_ps_reorder(is_dvd_ps)), Codec::Ac3 | Codec::Ac3Plus => Box::new(ac3::Ac3Parser::new()), Codec::DtsHdMa | Codec::DtsHdHr | Codec::Dts => Box::new(dts::DtsParser::new()), Codec::TrueHd => Box::new(truehd::TrueHdParser::new()), diff --git a/src/mux/codec/reorder.rs b/src/mux/codec/reorder.rs new file mode 100644 index 0000000..afcdd55 --- /dev/null +++ b/src/mux/codec/reorder.rs @@ -0,0 +1,318 @@ +//! Display-order PTS reconstruction for sparse-PTS program-stream video. +//! +//! MPEG program streams (DVD VOB, HD-DVD EVO) timestamp video at GOP +//! granularity: only one access unit per GOP carries a PES PTS, and the rest +//! arrive with none. The H.264 / HEVC / VC-1 parsers collapse a missing PTS to +//! `0` (`pes.pts.or(dts).unwrap_or(0)`), so on such a source every non-anchor +//! frame lands on the same block timestamp. A decoder then cannot order them and +//! reports "non monotonically increasing dts". (The MPEG-2 parser already avoids +//! this by reconstructing per-picture PTS from `temporal_reference`; these three +//! codecs carry no such field.) +//! +//! [`SparsePtsReorder`] reconstructs a display-order PTS for every frame from two +//! signals the parsers already provide — the coded picture type (I/P/B) and the +//! sparse anchor PTS — plus a per-frame duration self-calibrated from the spacing +//! between consecutive GOP anchors (no external frame-rate needed). It mirrors +//! the MPEG-2 parser's GOP-buffered origin-locking, but derives display order via +//! the classic single-anchor-delay rule instead of `temporal_reference`: +//! +//! - In DECODE order an anchor (I/P) is stored before the B-frames that +//! reference it forward, so decode `I P B P B` displays as `I B P B P`. +//! - The rule that produces that mapping: an anchor is displayed only after the +//! previously-held anchor; a B-frame displays immediately. This is exact for +//! the classic (non-hierarchical) GOP structures HD-DVD H.264/VC-1 use. +//! +//! This reconstruction is applied ONLY on the program-stream path +//! (`ContentFormat::MpegPs`). BD/UHD transport streams carry a per-frame PTS and +//! are never routed through it, so the primary decode path is untouched. + +use super::Frame; +use super::coding::CodingType; + +/// Fallback per-frame duration (ns) when the anchor spacing cannot calibrate one +/// (a stream with a single GOP, or no anchor PTS at all): 24000/1001 fps film, +/// the dominant HD-DVD cadence. Only affects intra-GOP spacing — each GOP's +/// origin is re-locked to its own anchor PTS, so a wrong fallback cannot drift +/// the timeline across GOPs. +const FALLBACK_FRAME_DUR_NS: i64 = 1_001_000_000 / 24; + +/// One buffered coded picture awaiting its GOP's completion. +struct Pending { + /// Explicit PES PTS (ns) for this AU, or `None` when the source omitted it. + explicit: Option, + /// Coded picture type; `P` (anchor) when the parser could not determine it, + /// so an unknown frame is never mis-placed as a bi-predicted B. + ctype: CodingType, + frame: Frame, +} + +/// A completed GOP, buffered until the NEXT GOP's anchor is known so a per-frame +/// duration can be calibrated from the two anchors before its frames are emitted. +struct Gop { + pend: Vec, + /// Display index (0-based) of each `pend` entry, in `pend` (decode) order. + dispidx: Vec, + /// Display-frame count (== `pend.len()`). + count: i64, + /// The anchor: `(explicit_pts, dispidx)` of the first buffered frame that + /// carried an explicit PTS, used to lock the display origin. `None` when the + /// GOP carried no PTS at all (origin then continues from the running base). + anchor: Option<(i64, i64)>, +} + +/// Reconstructs display-order PTS for a sparse-PTS video elementary stream. +pub(crate) struct SparsePtsReorder { + /// Frames of the GOP currently accumulating, in decode order. + cur: Vec, + /// The previously-completed GOP, held one step so its duration can be + /// calibrated from the next GOP's anchor before it is emitted. + held: Option, + /// Self-calibrated per-frame display duration (ns); 0 until two anchors seen. + dur_ns: i64, + /// Display time (ns) at which the next emitted GOP should begin, when its own + /// anchor is absent. Advanced by each emitted GOP. + next_start_ns: i64, +} + +impl SparsePtsReorder { + pub(crate) fn new() -> Self { + Self { + cur: Vec::new(), + held: None, + dur_ns: 0, + next_start_ns: 0, + } + } + + /// Feed one parsed frame with its explicit PES PTS (or `None`). Returns any + /// frames whose display PTS is now finalized (emitted in decode order). + pub(crate) fn push(&mut self, explicit: Option, frame: Frame) -> Vec { + let ctype = frame + .coding + .map(|c| c.coding_type()) + .unwrap_or(CodingType::P); + // A keyframe opens a new GOP: the picture already accumulated in `cur` is + // a complete GOP. Complete it (this frame belongs to the NEW GOP). + let mut out = Vec::new(); + if frame.keyframe && !self.cur.is_empty() { + out = self.complete_current_gop(); + } + self.cur.push(Pending { + explicit, + ctype, + frame, + }); + out + } + + /// Flush all buffered frames at end of stream. + pub(crate) fn flush(&mut self) -> Vec { + let mut out = self.complete_current_gop(); + if let Some(gop) = self.held.take() { + out.extend(self.emit_gop(gop)); + } + out + } + + /// Move `cur` into a completed [`Gop`]; if a GOP was already held, calibrate + /// the duration from the two anchors and emit the held one. + fn complete_current_gop(&mut self) -> Vec { + if self.cur.is_empty() { + return Vec::new(); + } + let pend = std::mem::take(&mut self.cur); + let dispidx = display_indices(pend.iter().map(|p| p.ctype)); + let count = pend.len() as i64; + let anchor = pend + .iter() + .zip(&dispidx) + .find_map(|(p, &d)| p.explicit.map(|pts| (pts, d))); + let gop = Gop { + pend, + dispidx, + count, + anchor, + }; + + let mut out = Vec::new(); + match self.held.take() { + Some(held) => { + // Calibrate a per-frame duration from the two anchors' spacing, + // spread across the held GOP's display-frame count. Approximate + // (assumes both anchors sit at a similar relative display slot), + // but each GOP re-locks its own origin, so the estimate only sets + // intra-GOP spacing. + if self.dur_ns == 0 { + if let (Some((p_held, _)), Some((p_next, _))) = (held.anchor, gop.anchor) { + let span = p_next - p_held; + if span > 0 && held.count > 0 { + self.dur_ns = (span / held.count).max(1); + } + } + } + out = self.emit_gop(held); + self.held = Some(gop); + } + None => self.held = Some(gop), + } + out + } + + /// Assign each frame in `gop` its display PTS and return them in decode order. + fn emit_gop(&mut self, gop: Gop) -> Vec { + let dur = if self.dur_ns > 0 { + self.dur_ns + } else { + FALLBACK_FRAME_DUR_NS + }; + // Lock the display origin: prefer the GOP's own anchor PTS (back out its + // display offset); otherwise continue from the running base. + let origin = match gop.anchor { + Some((pts, didx)) => pts - didx * dur, + None => self.next_start_ns, + }; + let Gop { + pend, + dispidx, + count, + .. + } = gop; + let mut out = Vec::with_capacity(pend.len()); + for (mut p, didx) in pend.into_iter().zip(dispidx) { + p.frame.pts_ns = origin + didx * dur; + out.push(p.frame); + } + // Next GOP with no anchor continues after this one's last display slot. + self.next_start_ns = origin + count * dur; + out + } +} + +/// Display index (0-based, decode order in → decode order out) for a GOP's coded +/// picture types via the classic single-anchor-delay reorder: an anchor (I/P) is +/// displayed only after the previously-held anchor; a B displays immediately. +/// Decode `I P B P B` → display indices `[0, 2, 1, 4, 3]` (display `I B P B P`). +fn display_indices(types: impl Iterator) -> Vec { + let types: Vec = types.collect(); + let mut disp = vec![0i64; types.len()]; + let mut held: Option = None; + let mut cursor = 0i64; + for (i, &c) in types.iter().enumerate() { + match c { + CodingType::I | CodingType::P => { + if let Some(h) = held { + disp[h] = cursor; + cursor += 1; + } + held = Some(i); + } + CodingType::B => { + disp[i] = cursor; + cursor += 1; + } + } + } + if let Some(h) = held { + disp[h] = cursor; + } + disp +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mux::codec::coding::PictureInfo; + + fn frame(ctype: CodingType, keyframe: bool) -> Frame { + Frame { + keyframe, + coding: Some(PictureInfo::coding_type_only(ctype)), + ..Default::default() + } + } + + #[test] + fn display_indices_map_classic_gop() { + use CodingType::*; + // decode I P B P B -> display I B P B P + let d = display_indices([I, P, B, P, B].into_iter()); + assert_eq!(d, vec![0, 2, 1, 4, 3]); + } + + #[test] + fn display_indices_all_anchors_are_identity() { + use CodingType::*; + let d = display_indices([I, P, P, P].into_iter()); + assert_eq!(d, vec![0, 1, 2, 3]); + } + + #[test] + fn reconstructs_monotonic_display_pts_from_one_anchor_per_gop() { + use CodingType::*; + // Two GOPs of 5 frames, decode order I P B P B, anchor PTS only on the + // GOP's I (0 ns, then ~5-frames-later). Frame duration should calibrate + // to the spacing/5 and every frame get a distinct increasing display PTS. + let dur = 41_708_333i64; + let mut r = SparsePtsReorder::new(); + let mut got: Vec = Vec::new(); + // GOP 1: anchor on the I at t=0. + for (k, (ct, pts)) in [(I, Some(0i64)), (P, None), (B, None), (P, None), (B, None)] + .into_iter() + .enumerate() + { + let out = r.push(pts, frame(ct, k == 0)); + got.extend(out.iter().map(|f| f.pts_ns)); + } + // GOP 2: anchor on the I at t = 5*dur (its true display time). + for (k, (ct, pts)) in [ + (I, Some(5 * dur)), + (P, None), + (B, None), + (P, None), + (B, None), + ] + .into_iter() + .enumerate() + { + let out = r.push(pts, frame(ct, k == 0)); + got.extend(out.iter().map(|f| f.pts_ns)); + } + got.extend(r.flush().iter().map(|f| f.pts_ns)); + + // Ten frames out, none dropped. + assert_eq!(got.len(), 10, "all frames emitted"); + // The calibrated duration is (5*dur)/5 = dur. + // GOP 1 decode order I P B P B -> display indices 0 2 1 4 3 -> PTS: + assert_eq!( + &got[0..5], + &[0, 2 * dur, 1 * dur, 4 * dur, 3 * dur], + "GOP1 display PTS in decode order" + ); + // GOP 2 re-locks origin to 5*dur. + assert_eq!( + &got[5..10], + &[5 * dur, 7 * dur, 6 * dur, 9 * dur, 8 * dur], + "GOP2 display PTS continue monotonically per display order" + ); + } + + #[test] + fn no_pts_collisions_within_a_gop() { + use CodingType::*; + // Every frame distinct in DISPLAY order — the property the mkv muxer + // needs so a decoder can derive monotonic DTS. + let mut r = SparsePtsReorder::new(); + let mut all: Vec = Vec::new(); + for gop in 0..3 { + for (k, ct) in [I, P, B, P, B].into_iter().enumerate() { + let pts = (k == 0).then_some(gop as i64 * 5 * 41_708_333); + all.extend(r.push(pts, frame(ct, k == 0)).iter().map(|f| f.pts_ns)); + } + } + all.extend(r.flush().iter().map(|f| f.pts_ns)); + let mut sorted = all.clone(); + sorted.sort_unstable(); + sorted.dedup(); + assert_eq!(sorted.len(), all.len(), "no two frames share a display PTS"); + } +} diff --git a/src/mux/codec/vc1.rs b/src/mux/codec/vc1.rs index 23ac4b4..8d77c90 100644 --- a/src/mux/codec/vc1.rs +++ b/src/mux/codec/vc1.rs @@ -103,6 +103,10 @@ pub struct Vc1Parser { cur_entry_point: Option>, width: u32, height: u32, + /// Display-order PTS reconstruction, enabled only on the program-stream + /// (HD-DVD EVO) path where the source stamps a PTS once per GOP. `None` on + /// the BD/UHD transport path, which carries a per-frame PTS. + reorder: Option, } impl Default for Vc1Parser { @@ -120,6 +124,25 @@ impl Vc1Parser { cur_entry_point: None, width: 1920, height: 1080, + reorder: None, + } + } + + /// Enable display-order PTS reconstruction for a program-stream source. + /// No-op (leaves timestamps as parsed) for a transport-stream source. + pub(crate) fn with_ps_reorder(mut self, enabled: bool) -> Self { + if enabled { + self.reorder = Some(super::reorder::SparsePtsReorder::new()); + } + self + } + + /// Route a finished frame through the PTS reorderer when enabled, else emit + /// it directly (unchanged transport-stream behaviour). + fn finish(&mut self, explicit: Option, frame: Frame) -> Vec { + match self.reorder.as_mut() { + Some(r) => r.push(explicit, frame), + None => vec![frame], } } } @@ -169,7 +192,8 @@ impl CodecParser for Vc1Parser { // decode order and the player reorders by timecode. Use PTS, not DTS — // DTS presents B-frames in decode order (visible judder) and breaks // PTS-based seeking. Fall back to DTS only if PTS is absent. - let ts_ns = pes.pts.or(pes.dts).map(pts_to_ns).unwrap_or(0); + let explicit_pts = pes.pts.or(pes.dts).map(pts_to_ns); + let ts_ns = explicit_pts.unwrap_or(0); let mut has_seq_header = false; let mut has_entry_point = false; let mut frame_start: Option = None; @@ -321,7 +345,7 @@ impl CodecParser for Vc1Parser { vc1_frame_coding_type(data.get(fs + 4..)?, self.cur_seq_header.as_deref()) }); - vec![Frame { + let frame = Frame { // Coding-type only: VC-1 field order is not decoded here, so // field_order() stays None — honestly absent, never guessed. coding: coding_type.map(PictureInfo::coding_type_only), @@ -333,7 +357,15 @@ impl CodecParser for Vc1Parser { discontinuity: pes.discontinuity, data: frame_data, duration_ns: None, - }] + }; + self.finish(explicit_pts, frame) + } + + fn flush(&mut self) -> Vec { + match self.reorder.as_mut() { + Some(r) => r.flush(), + None => Vec::new(), + } } fn codec_private(&self) -> Option> {