//! MPEG-2 Video elementary stream parser. //! //! Reassembles coded pictures (access units) from the demuxed PES stream and //! extracts sequence headers for MKV codecPrivate. //! //! **One PES is NOT one frame.** On a DVD the video elementary stream is sliced //! into ~2 KB Program-Stream PES packets (one per 2048-byte pack), so a single //! coded picture (~10-100 KB) spans many PES packets and only the first carries //! a PTS. Emitting one MKV block per PES would write frame *fragments* — the //! decoder then sees truncated pictures (`ac-tex damaged`) and picture-coding //! extensions detached from their picture header (`ignoring pic cod ext`). So //! this parser buffers ES bytes across PES packets and emits exactly one Frame //! per coded picture. (Blu-ray aligns one access unit per PES and would not need //! this, but DVD MPEG-2 PS does.) //! //! Access-unit model (ISO/IEC 13818-2): an AU is an optional sequence header + //! optional GOP header + one picture header + its coding extension + slices. A //! new AU begins at the next picture / sequence / GOP start code *once the //! current AU already contains a picture* — leading sequence/GOP headers attach //! to the picture that follows them. //! //! Start codes: //! - Picture header: 00 00 01 00 //! - Slice: 00 00 01 01 .. AF //! - Sequence header: 00 00 01 B3 //! - Extension (seq/pic):00 00 01 B5 //! - GOP header: 00 00 01 B8 use std::collections::VecDeque; use super::startcode::find_start_code; use super::{CodecParser, Frame, pts_to_ns}; use crate::mux::ts::PesPacket; /// Sequence header start code suffix. const SEQ_HEADER_CODE: u8 = 0xB3; /// Sequence / picture extension start code suffix. const SEQ_EXT_CODE: u8 = 0xB5; /// Group-of-pictures header start code suffix. const GOP_CODE: u8 = 0xB8; /// Picture start code suffix. const PICTURE_CODE: u8 = 0x00; /// Picture coding type: I-frame. const PICTURE_TYPE_I: u8 = 1; /// Hard cap on the access-unit reassembly buffer. A real MPEG-2 frame is well /// under 1 MiB (DVD I-frames ~100 KB); past this cap a corrupt stream that /// never produces a second access-unit boundary is force-flushed as a single /// frame rather than driving unbounded allocation. const MAX_AU_BUFFER: usize = 8 * 1024 * 1024; /// Cap on frames held awaiting the first PES PTS anchor. A DVD stamps a PTS in /// the first VOBU (~0.5 s ≈ 15 frames); this leaves generous slack. If no PTS /// ever arrives within the cap, buffered frames are released on a 0 base. const MAX_PENDING_FRAMES: usize = 600; /// Frame rate table (index from sequence header frame_rate_code). const FRAME_RATES: [(u32, u32); 9] = [ (0, 1), // 0: forbidden (24000, 1001), // 1: 23.976 (24, 1), // 2: 24 (25, 1), // 3: 25 (30000, 1001), // 4: 29.97 (30, 1), // 5: 30 (50, 1), // 6: 50 (60000, 1001), // 7: 59.94 (60, 1), // 8: 60 ]; /// Aspect ratio table (index from sequence header aspect_ratio_information). const ASPECT_RATIOS: [(u8, u8); 5] = [ (0, 0), // 0: forbidden (1, 1), // 1: square pixels (1:1 SAR) (4, 3), // 2: 4:3 display (16, 9), // 3: 16:9 display (221, 100), // 4: 2.21:1 display ]; /// MPEG-2 Video elementary stream parser / access-unit reassembler. pub struct Mpeg2Parser { /// Raw bytes of the last seen sequence header (+ sequence extension if /// present), captured for MKV codecPrivate. seq_header: Option>, /// Unemitted elementary-stream bytes: the in-progress access unit plus any /// lookahead needed to detect the next AU boundary. buf: Vec, /// Absolute ES byte offset of `buf[0]`. Used to associate PES PTS marks /// (recorded by absolute offset) with the access units they belong to. base_offset: u64, /// `(absolute ES offset of a PES's first byte, PTS in ns)` for every PES /// that carried a timestamp, in ascending offset order. pts_marks: VecDeque<(u64, i64)>, /// Per-frame presentation interval (ns), derived from the sequence header /// frame rate. DVD stamps a PTS only ~once per VOBU (every ~0.5 s), so /// frames between marks must be timed by `temporal_reference` × this /// interval. 0 until a sequence header with a valid frame rate is seen. frame_duration_ns: i64, /// Cumulative count of coded pictures emitted in all GOPs before the /// current one. `temporal_reference` is GOP-relative (display order within /// the GOP); adding this base makes a whole-stream display index. gop_base: u64, /// Coded pictures emitted in the current GOP so far (folded into /// `gop_base` at the next GOP boundary). gop_count: u64, /// Display index of the last frame that carried an explicit PES PTS, used /// to anchor interpolated timestamps to the real disc timeline (so video /// stays in sync with the PES-timestamped audio tracks). anchor_index: Option, /// PTS (ns) of the anchor frame. anchor_pts: i64, /// Frames emitted before the first PES PTS anchor is known, held with their /// display index. A DVD title can open with a still-frame/first-play /// sequence whose PTS lands a few frames in; buffering until the anchor lets /// those leading frames take the disc's real timeline instead of a 0 base. pending: Vec<(u64, Frame)>, } impl Default for Mpeg2Parser { fn default() -> Self { Self::new() } } impl Mpeg2Parser { /// Create a new MPEG-2 parser with no captured sequence-header state. pub fn new() -> Self { Self { seq_header: None, buf: Vec::with_capacity(128 * 1024), base_offset: 0, pts_marks: VecDeque::new(), frame_duration_ns: 0, gop_base: 0, gop_count: 0, anchor_index: None, anchor_pts: 0, pending: Vec::new(), } } /// Extract resolution from a captured sequence header. /// Returns (width, height) or None if the header is too short. pub fn resolution(&self) -> Option<(u16, u16)> { let hdr = self.seq_header.as_ref()?; parse_resolution(hdr) } /// Extract frame rate from a captured sequence header. /// Returns (numerator, denominator) or None. pub fn frame_rate(&self) -> Option<(u32, u32)> { let hdr = self.seq_header.as_ref()?; parse_frame_rate(hdr) } /// Extract aspect ratio from a captured sequence header. /// Returns (width, height) for display aspect ratio, or None. pub fn aspect_ratio(&self) -> Option<(u8, u8)> { let hdr = self.seq_header.as_ref()?; parse_aspect_ratio(hdr) } /// The PTS (ns) to assign to an access unit whose first relevant byte is at /// absolute ES offset `target`: the most recent PES timestamp at or before /// that offset (the PES that contains the access unit's start). Falls back /// to 0 when no timestamp has been seen yet. fn pts_for(&self, target: u64) -> i64 { let mut best = 0; for &(off, pts) in &self.pts_marks { if off <= target { best = pts; } else { break; } } best } /// Drain every complete access unit from `buf`, returning one Frame each. /// When `force` is true (EOF flush, or buffer-cap backstop) the trailing /// in-progress access unit is emitted even without a following boundary. fn drain_complete_aus(&mut self, force: bool) -> Vec { let mut out = Vec::new(); loop { // An access unit must contain a coded picture; without one there is // nothing to emit yet (leading sequence/GOP headers wait for it). let Some(pic) = find_code(&self.buf, 0, PICTURE_CODE) else { // No coded picture in an over-cap buffer means we are // accumulating unparseable data (a stream with no picture // start codes). Drop all but a 3-byte tail — enough to catch a // start-code prefix straddling the boundary — and advance the // absolute offset so the PES-mark invariant holds. Mirrors the // post-picture buffer backstop in the AU-boundary search below. if self.buf.len() > MAX_AU_BUFFER { let drop = self.buf.len() - 3; self.base_offset += drop as u64; self.buf.drain(..drop); let cutoff = self.base_offset; while let Some(&(off, _)) = self.pts_marks.front() { if off < cutoff { self.pts_marks.pop_front(); } else { break; } } } break; }; // The current AU ends where the next one begins: the first // picture / sequence / GOP start code after this picture. let end = match find_au_start(&self.buf, pic + 4) { Some(b) => b, None if force => self.buf.len(), None if self.buf.len() > MAX_AU_BUFFER => self.buf.len(), None => break, // AU not yet complete — await the next boundary }; if end == 0 { break; } // Phase 1 — read everything from `buf` before any mutation of self // (the slice borrow must end before we touch self fields). let hdr = extract_seq_header(&self.buf[..end]); // A GOP header (0xB8) or a fresh sequence header (0xB3) starts a new // GOP, resetting temporal_reference to 0. let gop_boundary = find_code(&self.buf[..end], 0, GOP_CODE).is_some() || find_code(&self.buf[..end], 0, SEQ_HEADER_CODE).is_some(); let keyframe = pic + 5 < end && ((self.buf[pic + 5] >> 3) & 0x07) == PICTURE_TYPE_I; // temporal_reference: the 10 bits immediately after the picture // start code = display order within the GOP. let tr = if pic + 5 < end { (((self.buf[pic + 4] as u64) << 2) | ((self.buf[pic + 5] as u64) >> 6)) & 0x3FF } else { 0 }; let pic_abs = self.base_offset + pic as u64; let end_abs = self.base_offset + end as u64; let data = self.buf[..end].to_vec(); // Phase 2 — mutate self. if let Some(h) = hdr { self.seq_header = Some(h); if let Some((num, den)) = self.frame_rate() { if num > 0 { self.frame_duration_ns = 1_000_000_000i64 * den as i64 / num as i64; } } } if gop_boundary && self.gop_count > 0 { self.gop_base += self.gop_count; self.gop_count = 0; } let display_index = self.gop_base + tr; // An explicit PES PTS for this access unit, if any. By the mark-drain // invariant the front mark's offset is >= this AU's start, so a front // mark inside [start, end) is this AU's own timestamp. let explicit = self .pts_marks .front() .filter(|&&(off, _)| off < end_abs) .map(|&(_, p)| p); let duration_ns = (self.frame_duration_ns > 0).then_some(self.frame_duration_ns as u64); let mut frame = Frame { pts_ns: 0, keyframe, data, duration_ns, }; if self.frame_duration_ns > 0 { // Reconstruct from display order; anchor to the real PES PTS so // video stays in sync with the PES-timestamped audio. match explicit { Some(p) => { self.anchor_index = Some(display_index); self.anchor_pts = p; // Backfill any leading frames held before the anchor was // known (still-frame / first-play opening): give each the // disc's real timeline relative to this anchor. for (di, mut held) in self.pending.drain(..) { held.pts_ns = p + (di as i64 - display_index as i64) * self.frame_duration_ns; out.push(held); } frame.pts_ns = p; out.push(frame); } None => match self.anchor_index { Some(ai) => { frame.pts_ns = self.anchor_pts + (display_index as i64 - ai as i64) * self.frame_duration_ns; out.push(frame); } None if self.pending.len() < MAX_PENDING_FRAMES => { // No anchor yet — hold so leading frames get the // disc's real timeline once the first PTS arrives, // not a 0 base. self.pending.push((display_index, frame)); } None => { frame.pts_ns = display_index as i64 * self.frame_duration_ns; out.push(frame); } }, } } else { // No frame rate yet (no sequence header) — fall back to the // nearest preceding PES timestamp. frame.pts_ns = self.pts_for(pic_abs); out.push(frame); } self.gop_count += 1; self.buf.drain(..end); self.base_offset = end_abs; // Drop PTS marks fully consumed by the emitted AU; keep the mark at // the boundary (it belongs to the next AU). while let Some(&(off, _)) = self.pts_marks.front() { if off < end_abs { self.pts_marks.pop_front(); } else { break; } } } out } } impl CodecParser for Mpeg2Parser { fn parse(&mut self, pes: &PesPacket) -> Vec { if pes.data.is_empty() { return Vec::new(); } // Record this PES's timestamp against the absolute offset of its first // ES byte, BEFORE appending. MKV block timecodes are presentation // timestamps; prefer PTS (DTS shows B-frames in decode order — judder // and broken seeking), falling back to DTS only when PTS is absent. let off = self.base_offset + self.buf.len() as u64; if let Some(ts) = pes.pts.or(pes.dts) { self.pts_marks.push_back((off, pts_to_ns(ts))); } self.buf.extend_from_slice(&pes.data); self.drain_complete_aus(false) } fn flush(&mut self) -> Vec { let mut out = self.drain_complete_aus(true); // EOF: if no PES ever supplied a PTS/DTS, `self.pending` still holds the // frames buffered while waiting for an anchor (the opening keyframe + // first ~20s). Without this they'd be silently dropped — a 100%-recovery // violation. Emit each with the same 0-base fallback the no-anchor // overflow arm uses (`display_index * frame_duration_ns`), ordered by // display_index so presentation order is preserved. if !self.pending.is_empty() { let mut held: Vec<(u64, Frame)> = self.pending.drain(..).collect(); held.sort_by_key(|(di, _)| *di); for (di, mut frame) in held { frame.pts_ns = di as i64 * self.frame_duration_ns; out.push(frame); } } out } fn codec_private(&self) -> Option> { self.seq_header.clone() } } /// Extract the sequence header (+ any B5 extensions / user-data, up to the /// first GOP or picture start code) from a fully-assembled access unit — exactly /// the extradata an MPEG-2 decoder expects as codecPrivate. Returns None if the /// access unit carries no sequence header. A NEW header replaces the stored one /// (title boundary / channel change), so its extension is always re-captured. fn extract_seq_header(au: &[u8]) -> Option> { let b3 = find_code(au, 0, SEQ_HEADER_CODE)?; let mut end = au.len(); let mut p = b3 + 4; while let Some(sc) = find_start_code(au, p) { if sc + 3 >= au.len() { break; } let c = au[sc + 3]; if c == PICTURE_CODE || c == GOP_CODE { end = sc; break; } p = sc + 4; } Some(au[b3..end].to_vec()) } /// Find the next start code at or after `from` whose code byte equals `want`. fn find_code(data: &[u8], from: usize, want: u8) -> Option { let mut pos = from; while let Some(sc) = find_start_code(data, pos) { if sc + 3 >= data.len() { return None; } if data[sc + 3] == want { return Some(sc); } pos = sc + 4; } None } /// Find the next access-unit boundary at or after `from`: the position of a /// picture (0x00), sequence header (0xB3), or GOP (0xB8) start code. Extension /// (0xB5), slice (0x01..=0xAF), user-data (0xB2) and sequence-end (0xB7) codes /// belong to the current access unit and are NOT boundaries. fn find_au_start(data: &[u8], from: usize) -> Option { let mut pos = from; while let Some(sc) = find_start_code(data, pos) { if sc + 3 >= data.len() { return None; } let code = data[sc + 3]; if code == PICTURE_CODE || code == SEQ_HEADER_CODE || code == GOP_CODE { return Some(sc); } pos = sc + 4; } None } /// Parse horizontal and vertical resolution from sequence header bytes. /// The sequence header must start with 00 00 01 B3. fn parse_resolution(hdr: &[u8]) -> Option<(u16, u16)> { // Need at least start code (4) + 4 bytes of header data = 8 bytes. if hdr.len() < 8 { return None; } // Bytes 4-5: horizontal_size_value (12 bits) | vertical_size_value top 4 bits // Bytes 5-6: vertical_size_value bottom 8 bits (12 bits total) let h = ((hdr[4] as u16) << 4) | ((hdr[5] as u16) >> 4); let v = (((hdr[5] & 0x0F) as u16) << 8) | hdr[6] as u16; Some((h, v)) } /// Parse frame rate code from sequence header. fn parse_frame_rate(hdr: &[u8]) -> Option<(u32, u32)> { if hdr.len() < 8 { return None; } let frame_rate_code = (hdr[7] & 0x0F) as usize; if frame_rate_code == 0 || frame_rate_code >= FRAME_RATES.len() { return None; } Some(FRAME_RATES[frame_rate_code]) } /// Parse aspect ratio information from sequence header. fn parse_aspect_ratio(hdr: &[u8]) -> Option<(u8, u8)> { if hdr.len() < 8 { return None; } let ar_code = ((hdr[7] >> 4) & 0x0F) as usize; if ar_code == 0 || ar_code >= ASPECT_RATIOS.len() { return None; } Some(ASPECT_RATIOS[ar_code]) } #[cfg(test)] mod tests { use super::*; use crate::mux::ts::PesPacket; fn make_pes(data: Vec, pts: Option) -> PesPacket { PesPacket { pid: 0x1011, pts, dts: None, data, } } /// Build a minimal MPEG-2 sequence header. /// 00 00 01 B3 [h_size:12][v_size:12] [aspect:4][frame_rate:4] ... fn make_seq_header(width: u16, height: u16, aspect: u8, frame_rate: u8) -> Vec { let mut hdr = vec![0x00, 0x00, 0x01, SEQ_HEADER_CODE]; hdr.push((width >> 4) as u8); hdr.push(((width & 0x0F) as u8) << 4 | ((height >> 8) & 0x0F) as u8); hdr.push((height & 0xFF) as u8); hdr.push((aspect << 4) | (frame_rate & 0x0F)); // Bit rate (18 bits) + marker + VBV buffer size (10 bits) etc — pad minimally. hdr.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0x00]); hdr } /// Build a picture header with the given coding type. fn make_picture_header(coding_type: u8) -> Vec { // 00 00 01 00 [temporal_ref:10][picture_coding_type:3][...] let byte5 = (coding_type & 0x07) << 3; vec![0x00, 0x00, 0x01, PICTURE_CODE, 0x00, byte5, 0x00, 0x00] } /// A GOP header start code (used as a clean access-unit delimiter in tests). fn gop() -> Vec { vec![0x00, 0x00, 0x01, GOP_CODE, 0x00, 0x00, 0x00, 0x00] } /// Picture header carrying an explicit 10-bit temporal_reference. fn make_picture_header_tr(coding_type: u8, tr: u16) -> Vec { let b4 = ((tr >> 2) & 0xFF) as u8; let b5 = (((tr & 0x03) as u8) << 6) | ((coding_type & 0x07) << 3); vec![0x00, 0x00, 0x01, PICTURE_CODE, b4, b5, 0x00, 0x00] } /// Collect every frame from a single PES followed by an EOF flush — the /// common single-picture test shape (the final AU emits on flush()). fn parse_then_flush(parser: &mut Mpeg2Parser, pes: &PesPacket) -> Vec { let mut frames = parser.parse(pes); frames.extend(parser.flush()); frames } // --- Sequence header parsing --- #[test] fn parse_sequence_header_resolution() { assert_eq!( parse_resolution(&make_seq_header(720, 480, 2, 4)), Some((720, 480)) ); } #[test] fn parse_sequence_header_1920x1080() { assert_eq!( parse_resolution(&make_seq_header(1920, 1080, 3, 4)), Some((1920, 1080)) ); } #[test] fn parse_sequence_header_frame_rate() { let hdr = make_seq_header(720, 480, 2, 4); // frame_rate_code 4 = 29.97 assert_eq!(parse_frame_rate(&hdr), Some((30000, 1001))); } #[test] fn parse_sequence_header_aspect_ratio() { let hdr = make_seq_header(720, 480, 3, 4); // aspect code 3 = 16:9 assert_eq!(parse_aspect_ratio(&hdr), Some((16, 9))); } #[test] fn parse_sequence_header_too_short() { let hdr = vec![0x00, 0x00, 0x01, SEQ_HEADER_CODE]; assert!(parse_resolution(&hdr).is_none()); assert!(parse_frame_rate(&hdr).is_none()); assert!(parse_aspect_ratio(&hdr).is_none()); } // --- I-frame detection --- #[test] fn detect_i_frame() { let mut parser = Mpeg2Parser::new(); let mut data = make_picture_header(PICTURE_TYPE_I); data.extend_from_slice(&[0xFF; 16]); let frames = parse_then_flush(&mut parser, &make_pes(data, Some(90000))); assert_eq!(frames.len(), 1); assert!(frames[0].keyframe, "I-frame should be detected as keyframe"); } #[test] fn detect_p_frame_not_keyframe() { let mut parser = Mpeg2Parser::new(); let mut data = make_picture_header(2); // P-frame data.extend_from_slice(&[0xFF; 16]); let frames = parse_then_flush(&mut parser, &make_pes(data, Some(90000))); assert_eq!(frames.len(), 1); assert!(!frames[0].keyframe, "P-frame should not be keyframe"); } #[test] fn detect_b_frame_not_keyframe() { let mut parser = Mpeg2Parser::new(); let mut data = make_picture_header(3); // B-frame data.extend_from_slice(&[0xFF; 16]); let frames = parse_then_flush(&mut parser, &make_pes(data, Some(90000))); assert_eq!(frames.len(), 1); assert!(!frames[0].keyframe, "B-frame should not be keyframe"); } // --- The core fix: a picture split across many PES packets is ONE frame --- #[test] fn picture_fragmented_across_pes_is_reassembled_into_one_frame() { // A DVD coded picture spans multiple ~2 KB PES packets; only the first // carries a PTS. The parser must concatenate them into ONE access unit, // not emit one fragment per PES. let mut parser = Mpeg2Parser::new(); let mut au = make_seq_header(720, 480, 3, 4); au.extend_from_slice(&make_picture_header(PICTURE_TYPE_I)); au.extend_from_slice(&vec![0xAA; 5000]); // slice data (no start codes) // Split the AU into 2 KB fragments across separate PES packets. let mut frames = Vec::new(); for (i, chunk) in au.chunks(2000).enumerate() { let pts = if i == 0 { Some(90000) } else { None }; frames.extend(parser.parse(&make_pes(chunk.to_vec(), pts))); } // No boundary yet → nothing emitted during parse(). assert!(frames.is_empty(), "incomplete AU must not emit fragments"); // Flush completes the trailing AU. frames.extend(parser.flush()); assert_eq!(frames.len(), 1, "fragments reassembled into ONE frame"); assert_eq!(frames[0].data, au, "frame is the whole picture, byte-exact"); assert!(frames[0].keyframe); assert_eq!( frames[0].pts_ns, 1_000_000_000, "PTS from the first fragment" ); } #[test] fn two_pictures_emit_two_frames_at_the_boundary() { // pic1's frame is emitted as soon as pic2's start code is seen; pic2 on // flush. Each frame contains exactly its own picture. let mut parser = Mpeg2Parser::new(); let mut pic1 = make_picture_header(PICTURE_TYPE_I); pic1.extend_from_slice(&vec![0x11; 100]); let mut pic2 = make_picture_header(2); // P pic2.extend_from_slice(&vec![0x22; 100]); let mut stream = pic1.clone(); stream.extend_from_slice(&pic2); let mut frames = parser.parse(&make_pes(stream, Some(0))); assert_eq!( frames.len(), 1, "first picture emitted at second's boundary" ); assert_eq!(frames[0].data, pic1); assert!(frames[0].keyframe); frames.extend(parser.flush()); assert_eq!(frames.len(), 2); assert_eq!(frames[1].data, pic2); assert!(!frames[1].keyframe); } #[test] fn picture_coding_extension_stays_with_its_picture() { // Regression for `ignoring pic cod ext after 0`: the picture coding // extension (00 00 01 B5) must remain in the SAME access unit as its // picture header, never split into the next block. let mut parser = Mpeg2Parser::new(); let mut au = make_picture_header(PICTURE_TYPE_I); au.extend_from_slice(&[0x00, 0x00, 0x01, SEQ_EXT_CODE, 0x88, 0x00]); // pic coding ext au.extend_from_slice(&[0x00, 0x00, 0x01, 0x01]); // slice au.extend_from_slice(&vec![0x77; 50]); let frames = parse_then_flush(&mut parser, &make_pes(au.clone(), Some(0))); assert_eq!(frames.len(), 1); assert_eq!( frames[0].data, au, "picture + coding extension + slice = one AU" ); } // --- PTS association across fragments --- #[test] fn each_picture_gets_the_pts_of_the_pes_that_began_it() { let mut parser = Mpeg2Parser::new(); // PES 1: pic1 (PTS 90000) + start of pic2's bytes carried later. let mut pic1 = make_picture_header(PICTURE_TYPE_I); pic1.extend_from_slice(&vec![0x11; 50]); let frames1 = parser.parse(&make_pes(pic1, Some(90000))); assert!(frames1.is_empty(), "pic1 awaits pic2's boundary"); // PES 2: pic2 (PTS 180000). let mut pic2 = make_picture_header(2); pic2.extend_from_slice(&vec![0x22; 50]); let mut frames = parser.parse(&make_pes(pic2, Some(180000))); assert_eq!(frames.len(), 1, "pic1 emitted when pic2 starts"); assert_eq!(frames[0].pts_ns, 1_000_000_000, "pic1 → PTS 90000"); frames.extend(parser.flush()); assert_eq!(frames.len(), 2); assert_eq!(frames[1].pts_ns, 2_000_000_000, "pic2 → PTS 180000"); } // --- sparse PTS reconstructed from temporal_reference + frame rate --- #[test] fn sparse_pts_interpolated_by_temporal_reference() { // DVD stamps a PTS only ~once per VOBU; frames between marks must be // timed by temporal_reference × frame interval, anchored to the real // PES PTS so audio stays in sync. Frame rate code 3 = 25 fps = 40 ms. let mut p = Mpeg2Parser::new(); // GOP 1: seq + gop + I(TR0) carrying PES PTS 0 (the anchor). let mut a = make_seq_header(720, 480, 3, 3); a.extend_from_slice(&gop()); a.extend_from_slice(&make_picture_header_tr(1, 0)); a.extend_from_slice(&[0xAA; 20]); let mut frames = p.parse(&make_pes(a, Some(0))); assert!( frames.is_empty(), "first AU waits for the next picture boundary" ); // TR1, no PES PTS → interpolate. let mut b1 = make_picture_header_tr(3, 1); b1.extend_from_slice(&[0xBB; 20]); frames.extend(p.parse(&make_pes(b1, None))); // TR2, no PES PTS → interpolate. let mut b2 = make_picture_header_tr(3, 2); b2.extend_from_slice(&[0xCC; 20]); frames.extend(p.parse(&make_pes(b2, None))); frames.extend(p.flush()); assert_eq!(frames.len(), 3); assert_eq!(frames[0].pts_ns, 0, "anchor frame uses its real PES PTS"); assert_eq!(frames[1].pts_ns, 40_000_000, "TR1 → +1 frame interval"); assert_eq!(frames[2].pts_ns, 80_000_000, "TR2 → +2 frame intervals"); assert_eq!(frames[0].duration_ns, Some(40_000_000)); } #[test] fn temporal_reference_resets_each_gop_via_gop_base() { // Across a GOP boundary, temporal_reference restarts at 0 but the // whole-stream display index must keep climbing (gop_base folds the // previous GOP's frame count). 25 fps = 40 ms. let mut p = Mpeg2Parser::new(); // GOP 1: two pictures TR0 (anchor PTS 0), TR1. let mut g1 = make_seq_header(720, 480, 3, 3); g1.extend_from_slice(&gop()); g1.extend_from_slice(&make_picture_header_tr(1, 0)); g1.extend_from_slice(&[0xAA; 10]); g1.extend_from_slice(&make_picture_header_tr(2, 1)); g1.extend_from_slice(&[0xBB; 10]); let mut frames = p.parse(&make_pes(g1, Some(0))); // GOP 2: new GOP header, picture TR0 again (no PES PTS). let mut g2 = gop(); g2.extend_from_slice(&make_picture_header_tr(1, 0)); g2.extend_from_slice(&[0xCC; 10]); frames.extend(p.parse(&make_pes(g2, None))); frames.extend(p.flush()); assert_eq!(frames.len(), 3); assert_eq!(frames[0].pts_ns, 0); // GOP1 TR0 assert_eq!(frames[1].pts_ns, 40_000_000); // GOP1 TR1 // GOP2 TR0 → display index 2 (gop_base 2 + TR 0), NOT a reset to 0. assert_eq!( frames[2].pts_ns, 80_000_000, "gop_base keeps the clock climbing" ); } #[test] fn leading_frames_buffered_until_first_pts_anchor() { // A DVD title can open with a still-frame/first-play sequence whose PTS // lands a few frames in (the disc stamps the opening I-frames at one real // PES PTS, not 0). Leading frames must be held and then anchored to that // real timeline — never zero-stamped. 25 fps = 40 ms. PTS (2 s) arrives // only on the THIRD picture. let mut p = Mpeg2Parser::new(); let mut a = make_seq_header(720, 480, 3, 3); a.extend_from_slice(&gop()); a.extend_from_slice(&make_picture_header_tr(1, 0)); a.extend_from_slice(&[0xAA; 20]); let mut f = p.parse(&make_pes(a, None)); // no PTS → buffered let mut b1 = make_picture_header_tr(3, 1); b1.extend_from_slice(&[0xBB; 20]); f.extend(p.parse(&make_pes(b1, None))); // no PTS → buffered let mut b2 = make_picture_header_tr(3, 2); b2.extend_from_slice(&[0xCC; 20]); f.extend(p.parse(&make_pes(b2, Some(180000)))); // PTS 2 s → anchor + backfill f.extend(p.flush()); assert_eq!(f.len(), 3); // Anchored to the real disc timeline, NOT a 0 base. assert_eq!( f[0].pts_ns, 2_000_000_000 - 80_000_000, "leading frame back-anchored" ); assert_eq!(f[1].pts_ns, 2_000_000_000 - 40_000_000); assert_eq!( f[2].pts_ns, 2_000_000_000, "anchor frame = its real PES PTS" ); // Decode order preserved. assert!(f[0].keyframe); } // --- Sequence header → codec_private --- #[test] fn codec_private_from_sequence_header() { let mut parser = Mpeg2Parser::new(); let mut data = make_seq_header(720, 480, 3, 4); data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I)); data.extend_from_slice(&[0xFF; 8]); let _ = parse_then_flush(&mut parser, &make_pes(data, Some(0))); let cp = parser .codec_private() .expect("codec_private after seq header"); assert_eq!(&cp[..4], &[0x00, 0x00, 0x01, SEQ_HEADER_CODE]); } #[test] fn codec_private_none_initially() { assert!(Mpeg2Parser::new().codec_private().is_none()); } #[test] fn codec_private_includes_extension_but_not_picture() { let mut parser = Mpeg2Parser::new(); let mut data = make_seq_header(1920, 1080, 3, 4); // Sequence extension: 00 00 01 B5 [ext data] data.extend_from_slice(&[0x00, 0x00, 0x01, SEQ_EXT_CODE, 0x14, 0x8A, 0x00, 0x01]); data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I)); data.extend_from_slice(&[0xFF; 4]); let _ = parse_then_flush(&mut parser, &make_pes(data, Some(0))); let cp = parser.codec_private().unwrap(); assert!( cp.windows(4).any(|w| w == [0x00, 0x00, 0x01, SEQ_EXT_CODE]), "codec_private should include the sequence extension" ); // It must stop before the picture header — extradata is seq header only. assert!( !cp.windows(4).any(|w| w == [0x00, 0x00, 0x01, PICTURE_CODE]), "codec_private must NOT include the picture start code" ); } // --- seq-header keyframe flag must not leak into a P/B-frame --- #[test] fn seq_header_then_p_frame_is_not_keyframe() { // A PES carrying a sequence header followed by a P-frame must NOT be a // keyframe — keyframe-ness belongs to the coded picture. let mut parser = Mpeg2Parser::new(); let mut data = make_seq_header(720, 480, 3, 4); data.extend_from_slice(&make_picture_header(2)); // P-frame data.extend_from_slice(&[0xFF; 16]); let frames = parse_then_flush(&mut parser, &make_pes(data, Some(0))); assert_eq!(frames.len(), 1); assert!( !frames[0].keyframe, "seq-header + P-frame must not be a keyframe" ); assert!(parser.codec_private().is_some()); } #[test] fn sequence_header_with_picture_is_keyframe() { let mut parser = Mpeg2Parser::new(); let mut data = make_seq_header(720, 480, 3, 4); data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I)); data.extend_from_slice(&[0xFF; 16]); let frames = parse_then_flush(&mut parser, &make_pes(data, Some(0))); assert_eq!(frames.len(), 1); assert!(frames[0].keyframe); assert!(parser.codec_private().is_some()); } // --- a SECOND sequence header re-captures (title boundary) --- #[test] fn new_sequence_header_replaces_codec_private() { let mut parser = Mpeg2Parser::new(); // AU A: 1920x1080 seq header + I picture, delimited by a following GOP. let mut a = make_seq_header(1920, 1080, 3, 4); a.extend_from_slice(&make_picture_header(PICTURE_TYPE_I)); a.extend_from_slice(&[0xAA; 20]); a.extend_from_slice(&gop()); // boundary → AU A emits let fa = parser.parse(&make_pes(a, Some(0))); assert_eq!(fa.len(), 1); assert_eq!(parser.resolution(), Some((1920, 1080))); // AU B: a NEW 720x480 seq header + I picture. Its extension/header must // replace the stored one rather than keeping stale 1920x1080. let mut b = make_seq_header(720, 480, 2, 4); b.extend_from_slice(&make_picture_header(PICTURE_TYPE_I)); b.extend_from_slice(&[0xBB; 20]); let _ = parse_then_flush(&mut parser, &make_pes(b, Some(3600))); assert_eq!( parser.resolution(), Some((720, 480)), "codec_private updated to header B" ); } // --- PTS conversion --- #[test] fn pts_conversion_to_nanoseconds() { let mut parser = Mpeg2Parser::new(); let mut data = make_picture_header(PICTURE_TYPE_I); data.extend_from_slice(&[0xFF; 4]); let frames = parse_then_flush(&mut parser, &make_pes(data, Some(90000))); assert_eq!(frames.len(), 1); assert_eq!(frames[0].pts_ns, 1_000_000_000); } #[test] fn mpeg2_dts_fallback_and_zero() { let mut parser = Mpeg2Parser::new(); let mut data = make_picture_header(PICTURE_TYPE_I); data.extend_from_slice(&[0xFF; 4]); let pes = PesPacket { pid: 0x1011, pts: None, dts: Some(90000), data, }; let f = parse_then_flush(&mut parser, &pes); assert_eq!(f[0].pts_ns, 1_000_000_000, "DTS fallback"); let mut parser2 = Mpeg2Parser::new(); let mut data2 = make_picture_header(PICTURE_TYPE_I); data2.extend_from_slice(&[0xFF; 4]); let pes2 = PesPacket { pid: 0x1011, pts: None, dts: None, data: data2, }; let f2 = parse_then_flush(&mut parser2, &pes2); assert_eq!(f2[0].pts_ns, 0, "no PTS/DTS → 0"); } // --- Empty PES --- #[test] fn empty_pes_no_frames() { let mut parser = Mpeg2Parser::new(); assert!(parser.parse(&make_pes(Vec::new(), Some(0))).is_empty()); } // --- parameter-set-only stream: seq header, no picture → no frame --- #[test] fn sequence_header_only_emits_no_frame_but_captures_codec_private() { let mut parser = Mpeg2Parser::new(); let mut data = make_seq_header(1920, 1080, 3, 4); data.extend_from_slice(&[0x00, 0x00, 0x01, SEQ_EXT_CODE, 0x14, 0x8A]); // No picture start code at all. let frames = parse_then_flush(&mut parser, &make_pes(data, Some(0))); assert!(frames.is_empty(), "no coded picture → no frame"); // codec_private only captured when an AU is emitted; a header-only // stream emits nothing, so nothing is captured — and there is no frame // to need it. (Real streams always follow the header with a picture.) } // --- buffer cap: corrupt stream with no second boundary is force-flushed --- #[test] fn oversized_au_without_boundary_is_force_flushed() { let mut parser = Mpeg2Parser::new(); let mut data = make_picture_header(PICTURE_TYPE_I); // > MAX_AU_BUFFER of slice bytes with no following picture/seq/GOP. data.extend(std::iter::repeat_n(0xAA, MAX_AU_BUFFER + 1024)); let frames = parser.parse(&make_pes(data, Some(0))); assert_eq!( frames.len(), 1, "over-cap AU force-flushed rather than buffered" ); assert!(frames[0].keyframe); } // --- parse_resolution: 12-bit field packing (ISO 13818-2 §6.2.2.1) --- #[test] fn resolution_packs_split_nibble_correctly() { let hdr = make_seq_header(0xABC, 0xDEF, 1, 1); assert_eq!(parse_resolution(&hdr), Some((0xABC, 0xDEF))); } #[test] fn resolution_max_12bit() { let hdr = make_seq_header(4095, 4095, 1, 1); assert_eq!(parse_resolution(&hdr), Some((4095, 4095))); } #[test] fn resolution_too_short_none() { assert_eq!(parse_resolution(&[0x00, 0x00, 0x01, 0xB3, 0x07]), None); } // --- parse_frame_rate: full table + reserved codes --- #[test] fn frame_rate_all_valid_codes() { let expect = [ (24000u32, 1001u32), (24, 1), (25, 1), (30000, 1001), (30, 1), (50, 1), (60000, 1001), (60, 1), ]; for (i, &want) in expect.iter().enumerate() { let code = (i + 1) as u8; let hdr = make_seq_header(720, 480, 1, code); assert_eq!(parse_frame_rate(&hdr), Some(want), "frame_rate_code {code}"); } } #[test] fn frame_rate_code_zero_forbidden_none() { assert_eq!(parse_frame_rate(&make_seq_header(720, 480, 1, 0)), None); } #[test] fn frame_rate_code_out_of_range_none() { assert_eq!(parse_frame_rate(&make_seq_header(720, 480, 1, 0x0F)), None); } // --- parse_aspect_ratio: table + reserved codes --- #[test] fn aspect_ratio_all_valid_codes() { let expect = [(1u8, 1u8), (4, 3), (16, 9), (221, 100)]; for (i, &want) in expect.iter().enumerate() { let code = (i + 1) as u8; let hdr = make_seq_header(720, 480, code, 4); assert_eq!(parse_aspect_ratio(&hdr), Some(want), "aspect code {code}"); } } #[test] fn aspect_ratio_code_zero_none() { assert_eq!(parse_aspect_ratio(&make_seq_header(720, 480, 0, 4)), None); } #[test] fn aspect_ratio_code_out_of_range_none() { assert_eq!( parse_aspect_ratio(&make_seq_header(720, 480, 0x0F, 4)), None ); } // --- picture_coding_type: byte position + bit field --- #[test] fn picture_coding_type_bits_5_3() { for (ct, is_kf) in [(1u8, true), (2, false), (3, false), (4, false)] { let mut parser = Mpeg2Parser::new(); let mut data = make_picture_header(ct); data.extend_from_slice(&[0xFF; 8]); let f = parse_then_flush(&mut parser, &make_pes(data, Some(0))); assert_eq!(f.len(), 1); assert_eq!(f[0].keyframe, is_kf, "picture_coding_type {ct}"); } } #[test] fn parser_resolution_method() { let mut parser = Mpeg2Parser::new(); let mut data = make_seq_header(720, 576, 2, 3); data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I)); data.extend_from_slice(&[0xFF; 4]); let _ = parse_then_flush(&mut parser, &make_pes(data, Some(0))); assert_eq!(parser.resolution(), Some((720, 576))); assert_eq!(parser.frame_rate(), Some((25, 1))); // frame_rate_code 3 = 25fps assert_eq!(parser.aspect_ratio(), Some((4, 3))); // aspect code 2 = 4:3 } }