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.
|
||||
cur_sps: 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 {
|
||||
@@ -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<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 —
|
||||
// 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<Frame> {
|
||||
match self.reorder.as_mut() {
|
||||
Some(r) => r.flush(),
|
||||
None => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
/// 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
|
||||
|
||||
+35
-3
@@ -166,6 +166,10 @@ pub struct HevcParser {
|
||||
// colour-volume metadata is ever fabricated.
|
||||
sei_mastering: Option<MasteringDisplay>,
|
||||
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),
|
||||
@@ -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<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
|
||||
// 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<Frame> {
|
||||
match self.reorder.as_mut() {
|
||||
Some(r) => r.flush(),
|
||||
None => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn codec_private(&self) -> Option<Vec<u8>> {
|
||||
|
||||
@@ -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<dyn CodecParser> {
|
||||
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()),
|
||||
|
||||
@@ -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>>,
|
||||
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<super::reorder::SparsePtsReorder>,
|
||||
}
|
||||
|
||||
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<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 —
|
||||
// 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<usize> = 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<Frame> {
|
||||
match self.reorder.as_mut() {
|
||||
Some(r) => r.flush(),
|
||||
None => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn codec_private(&self) -> Option<Vec<u8>> {
|
||||
|
||||
Reference in New Issue
Block a user