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.
This commit is contained in:
+100
-3
@@ -52,6 +52,10 @@ pub struct H264Parser {
|
|||||||
// the stale avcC copy after a mid-title redefinition.
|
// the stale avcC copy after a mid-title redefinition.
|
||||||
cur_sps: Option<Vec<u8>>,
|
cur_sps: Option<Vec<u8>>,
|
||||||
cur_pps: Option<Vec<u8>>,
|
cur_pps: Option<Vec<u8>>,
|
||||||
|
/// 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<super::reorder::SparsePtsReorder>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for H264Parser {
|
impl Default for H264Parser {
|
||||||
@@ -68,6 +72,25 @@ impl H264Parser {
|
|||||||
pps: None,
|
pps: None,
|
||||||
cur_sps: None,
|
cur_sps: None,
|
||||||
cur_pps: 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<i64>, frame: Frame) -> Vec<Frame> {
|
||||||
|
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 —
|
// decode order and the player reorders by timecode. Use PTS, not DTS —
|
||||||
// DTS presents B-frames in decode order (visible judder) and breaks
|
// DTS presents B-frames in decode order (visible judder) and breaks
|
||||||
// PTS-based seeking. Fall back to DTS only if PTS is absent.
|
// 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
|
// Single pass: detect IDR keyframes, seed/strip param sets, and convert
|
||||||
// Annex B (start-code prefixed) NALUs to length-prefixed NALUs (MKV with
|
// 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
|
// Coding-type only: H.264 field order is not decoded here, so
|
||||||
// `field_order()` stays `None` — honestly absent, never guessed.
|
// `field_order()` stays `None` — honestly absent, never guessed.
|
||||||
coding: coding_type.map(PictureInfo::coding_type_only),
|
coding: coding_type.map(PictureInfo::coding_type_only),
|
||||||
@@ -248,7 +272,15 @@ impl CodecParser for H264Parser {
|
|||||||
discontinuity: pes.discontinuity,
|
discontinuity: pes.discontinuity,
|
||||||
data: frame_data,
|
data: frame_data,
|
||||||
duration_ns: None,
|
duration_ns: None,
|
||||||
}]
|
};
|
||||||
|
self.finish(explicit_pts, frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> Vec<Frame> {
|
||||||
|
match self.reorder.as_mut() {
|
||||||
|
Some(r) => r.flush(),
|
||||||
|
None => Vec::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn codec_private(&self) -> Option<Vec<u8>> {
|
fn codec_private(&self) -> Option<Vec<u8>> {
|
||||||
@@ -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<i64>| {
|
||||||
|
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<super::super::Frame> {
|
||||||
|
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<i64> = 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),
|
/// 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
|
/// 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
|
/// revert to A == avcC must still be emitted in-band or the A-segment
|
||||||
|
|||||||
+35
-3
@@ -166,6 +166,10 @@ pub struct HevcParser {
|
|||||||
// colour-volume metadata is ever fabricated.
|
// colour-volume metadata is ever fabricated.
|
||||||
sei_mastering: Option<MasteringDisplay>,
|
sei_mastering: Option<MasteringDisplay>,
|
||||||
sei_content_light: Option<ContentLightLevel>,
|
sei_content_light: Option<ContentLightLevel>,
|
||||||
|
/// 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<super::reorder::SparsePtsReorder>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mastering Display Colour Volume payload (Rec. ITU-T H.265 D.2.28),
|
/// Mastering Display Colour Volume payload (Rec. ITU-T H.265 D.2.28),
|
||||||
@@ -234,6 +238,25 @@ impl HevcParser {
|
|||||||
pts_wrap_offset: 0,
|
pts_wrap_offset: 0,
|
||||||
sei_mastering: None,
|
sei_mastering: None,
|
||||||
sei_content_light: 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<i64>, frame: Frame) -> Vec<Frame> {
|
||||||
|
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
|
// block timecode monotonic in storage order, which presents B-frames in
|
||||||
// decode order (visible judder / wrong frames) and breaks PTS-based
|
// decode order (visible judder / wrong frames) and breaks PTS-based
|
||||||
// seeking. Fall back to DTS only if PTS is somehow absent.
|
// 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
|
// Auto-detect a non-seamless clip boundary from the bitstream. freemkv
|
||||||
// reads a BD title's clips as ONE concatenated sector stream and the
|
// 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`
|
// from the first coded picture before writing the track header). `None`
|
||||||
// until both SEI present → SDR / no-SEI tracks carry nothing.
|
// until both SEI present → SDR / no-SEI tracks carry nothing.
|
||||||
let hdr10 = self.hdr10();
|
let hdr10 = self.hdr10();
|
||||||
vec![Frame {
|
let frame = Frame {
|
||||||
// Coding-type only: HEVC field order (pic_struct, from a pic_timing
|
// Coding-type only: HEVC field order (pic_struct, from a pic_timing
|
||||||
// SEI) is not decoded here, so field_order() stays None — honestly
|
// SEI) is not decoded here, so field_order() stays None — honestly
|
||||||
// absent, never guessed. HDR10 metadata is attached when measured.
|
// absent, never guessed. HDR10 metadata is attached when measured.
|
||||||
@@ -666,7 +690,15 @@ impl CodecParser for HevcParser {
|
|||||||
discontinuity: pes.discontinuity,
|
discontinuity: pes.discontinuity,
|
||||||
data: frame_data,
|
data: frame_data,
|
||||||
duration_ns: None,
|
duration_ns: None,
|
||||||
}]
|
};
|
||||||
|
self.finish(explicit_pts, frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> Vec<Frame> {
|
||||||
|
match self.reorder.as_mut() {
|
||||||
|
Some(r) => r.flush(),
|
||||||
|
None => Vec::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn codec_private(&self) -> Option<Vec<u8>> {
|
fn codec_private(&self) -> Option<Vec<u8>> {
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ pub mod lpcm;
|
|||||||
pub mod mpeg2;
|
pub mod mpeg2;
|
||||||
/// HDMV PGS (Presentation Graphics Stream) subtitle parser.
|
/// HDMV PGS (Presentation Graphics Stream) subtitle parser.
|
||||||
pub mod pgs;
|
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.
|
/// Shared MPEG/Annex-B start-code scanning helpers.
|
||||||
pub(crate) mod startcode;
|
pub(crate) mod startcode;
|
||||||
/// Dolby TrueHD / Atmos elementary-stream parser.
|
/// Dolby TrueHD / Atmos elementary-stream parser.
|
||||||
@@ -166,10 +168,14 @@ pub fn parser_for_codec(
|
|||||||
is_dvd_ps: bool,
|
is_dvd_ps: bool,
|
||||||
) -> Box<dyn CodecParser> {
|
) -> Box<dyn CodecParser> {
|
||||||
match codec {
|
match codec {
|
||||||
Codec::H264 => Box::new(h264::H264Parser::new()),
|
// `is_dvd_ps` marks a program-stream source (DVD VOB / HD-DVD EVO), whose
|
||||||
Codec::Hevc => Box::new(hevc::HevcParser::new()),
|
// 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::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::Ac3 | Codec::Ac3Plus => Box::new(ac3::Ac3Parser::new()),
|
||||||
Codec::DtsHdMa | Codec::DtsHdHr | Codec::Dts => Box::new(dts::DtsParser::new()),
|
Codec::DtsHdMa | Codec::DtsHdHr | Codec::Dts => Box::new(dts::DtsParser::new()),
|
||||||
Codec::TrueHd => Box::new(truehd::TrueHdParser::new()),
|
Codec::TrueHd => Box::new(truehd::TrueHdParser::new()),
|
||||||
|
|||||||
@@ -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<i64>,
|
||||||
|
/// 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<Pending>,
|
||||||
|
/// Display index (0-based) of each `pend` entry, in `pend` (decode) order.
|
||||||
|
dispidx: Vec<i64>,
|
||||||
|
/// 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<Pending>,
|
||||||
|
/// 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<Gop>,
|
||||||
|
/// 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<i64>, frame: Frame) -> Vec<Frame> {
|
||||||
|
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<Frame> {
|
||||||
|
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<Frame> {
|
||||||
|
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<Frame> {
|
||||||
|
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<Item = CodingType>) -> Vec<i64> {
|
||||||
|
let types: Vec<CodingType> = types.collect();
|
||||||
|
let mut disp = vec![0i64; types.len()];
|
||||||
|
let mut held: Option<usize> = 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<i64> = 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<i64> = 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
-3
@@ -103,6 +103,10 @@ pub struct Vc1Parser {
|
|||||||
cur_entry_point: Option<Vec<u8>>,
|
cur_entry_point: Option<Vec<u8>>,
|
||||||
width: u32,
|
width: u32,
|
||||||
height: 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<super::reorder::SparsePtsReorder>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Vc1Parser {
|
impl Default for Vc1Parser {
|
||||||
@@ -120,6 +124,25 @@ impl Vc1Parser {
|
|||||||
cur_entry_point: None,
|
cur_entry_point: None,
|
||||||
width: 1920,
|
width: 1920,
|
||||||
height: 1080,
|
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<i64>, frame: Frame) -> Vec<Frame> {
|
||||||
|
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 —
|
// decode order and the player reorders by timecode. Use PTS, not DTS —
|
||||||
// DTS presents B-frames in decode order (visible judder) and breaks
|
// DTS presents B-frames in decode order (visible judder) and breaks
|
||||||
// PTS-based seeking. Fall back to DTS only if PTS is absent.
|
// 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_seq_header = false;
|
||||||
let mut has_entry_point = false;
|
let mut has_entry_point = false;
|
||||||
let mut frame_start: Option<usize> = None;
|
let mut frame_start: Option<usize> = None;
|
||||||
@@ -321,7 +345,7 @@ impl CodecParser for Vc1Parser {
|
|||||||
vc1_frame_coding_type(data.get(fs + 4..)?, self.cur_seq_header.as_deref())
|
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
|
// Coding-type only: VC-1 field order is not decoded here, so
|
||||||
// field_order() stays None — honestly absent, never guessed.
|
// field_order() stays None — honestly absent, never guessed.
|
||||||
coding: coding_type.map(PictureInfo::coding_type_only),
|
coding: coding_type.map(PictureInfo::coding_type_only),
|
||||||
@@ -333,7 +357,15 @@ impl CodecParser for Vc1Parser {
|
|||||||
discontinuity: pes.discontinuity,
|
discontinuity: pes.discontinuity,
|
||||||
data: frame_data,
|
data: frame_data,
|
||||||
duration_ns: None,
|
duration_ns: None,
|
||||||
}]
|
};
|
||||||
|
self.finish(explicit_pts, frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> Vec<Frame> {
|
||||||
|
match self.reorder.as_mut() {
|
||||||
|
Some(r) => r.flush(),
|
||||||
|
None => Vec::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn codec_private(&self) -> Option<Vec<u8>> {
|
fn codec_private(&self) -> Option<Vec<u8>> {
|
||||||
|
|||||||
Reference in New Issue
Block a user