Test-hardening release, no runtime changes. Adds spec-grounded unit tests across the silent-corruption surfaces — UDF/MPLS/CLPI/IFO parsing, BD/DVD title + extent assembly, AACS/CSS key handling, TS/PS demux + codec parsers, MKV/EBML container output, the mux pipeline, sector prefetch + decrypt decorator, drive/SCSI sense decoding, label extraction, and core I/O. Each test is grounded in the format spec or real on-disc behavior and verified to fail under a targeted source mutation. No behavior changed.
837 lines
32 KiB
Rust
837 lines
32 KiB
Rust
//! MPEG-2 Video elementary stream parser.
|
|
//!
|
|
//! Extracts sequence headers for MKV codecPrivate.
|
|
//! Detects keyframes (I-frames from picture headers).
|
|
//! Each PES packet = one access unit = one frame.
|
|
//!
|
|
//! Start codes:
|
|
//! - Sequence header: 00 00 01 B3
|
|
//! - Sequence extension: 00 00 01 B5
|
|
//! - Picture header: 00 00 01 00
|
|
|
|
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 extension start code suffix.
|
|
const SEQ_EXT_CODE: u8 = 0xB5;
|
|
|
|
/// Picture start code suffix.
|
|
const PICTURE_CODE: u8 = 0x00;
|
|
|
|
/// Picture coding type: I-frame.
|
|
const PICTURE_TYPE_I: u8 = 1;
|
|
|
|
/// 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.
|
|
pub struct Mpeg2Parser {
|
|
/// Raw bytes of the last seen sequence header (+ sequence extension if found).
|
|
seq_header: Option<Vec<u8>>,
|
|
/// Whether we've captured the sequence extension (B5) already.
|
|
has_extension: bool,
|
|
}
|
|
|
|
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,
|
|
has_extension: false,
|
|
}
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
}
|
|
|
|
impl CodecParser for Mpeg2Parser {
|
|
fn parse(&mut self, pes: &PesPacket) -> Vec<Frame> {
|
|
if pes.data.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
|
|
// MKV block timecodes are PRESENTATION timestamps; frames are stored in
|
|
// 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 data = &pes.data;
|
|
// Keyframe-ness is a property of the coded PICTURE, not of a sequence
|
|
// header. A PES may carry a sequence header followed by a P/B-frame
|
|
// (open-GOP / re-encoded MPEG-2); the picture, not the seq header,
|
|
// decides the cue point. Set this only from the PICTURE_CODE arm.
|
|
let mut picture_is_keyframe = false;
|
|
let mut has_picture = false;
|
|
let mut saw_seq_header = false;
|
|
|
|
// Scan for start codes in the elementary stream data.
|
|
let mut pos = 0;
|
|
while let Some(sc) = find_start_code(data, pos) {
|
|
if sc + 3 >= data.len() {
|
|
break;
|
|
}
|
|
let code = data[sc + 3];
|
|
|
|
match code {
|
|
SEQ_HEADER_CODE => {
|
|
// MPEG-2 sequence header: 00 00 01 B3 + variable data.
|
|
// Base header: 8 bytes after start code = 12 bytes total.
|
|
// Then possibly 64 intra quantizer values (bit-packed from bit 63).
|
|
// Then possibly 64 non-intra quantizer values.
|
|
// Then extensions (00 00 01 B5).
|
|
//
|
|
// Capture to the next start code within this PES data.
|
|
// If no next start code exists (extension in next PES), capture
|
|
// just the sequence header without extensions.
|
|
let hdr_start = sc;
|
|
let next_sc = find_start_code(data, sc + 4);
|
|
let hdr_end = match next_sc {
|
|
Some(next) if next + 3 < data.len() => {
|
|
let mut end = next;
|
|
// Include B5 extensions
|
|
while end + 3 < data.len() && data[end + 3] == SEQ_EXT_CODE {
|
|
end = find_start_code(data, end + 4).unwrap_or(data.len());
|
|
}
|
|
end
|
|
}
|
|
_ => {
|
|
// No next start code in this PES — calculate exact header size.
|
|
// Bit 62: load_intra_quantiser_matrix
|
|
// Bit 62+1+512: load_non_intra_quantiser_matrix (if intra present)
|
|
// Bit 62+1: load_non_intra_quantiser_matrix (if intra absent)
|
|
if sc + 12 > data.len() {
|
|
data.len()
|
|
} else {
|
|
let mut bits = 63u32; // bits consumed so far
|
|
let intra = (data[sc + 11] & 0x02) != 0;
|
|
if intra {
|
|
bits += 64 * 8;
|
|
}
|
|
// Non-intra flag is at current bit position
|
|
let byte_pos = (bits / 8) as usize;
|
|
let bit_pos = 7 - (bits % 8) as u8;
|
|
if sc + 4 + byte_pos < data.len() {
|
|
let non_intra = (data[sc + 4 + byte_pos] >> bit_pos) & 1 != 0;
|
|
bits += 1;
|
|
if non_intra {
|
|
bits += 64 * 8;
|
|
}
|
|
}
|
|
let total_bytes = 4 + bits.div_ceil(8) as usize;
|
|
(sc + total_bytes).min(data.len())
|
|
}
|
|
}
|
|
};
|
|
|
|
self.seq_header = Some(data[hdr_start..hdr_end].to_vec());
|
|
// A NEW sequence header replaces the stored one, so its B5
|
|
// sequence extension must be re-captured. Reset the flag the
|
|
// SEQ_EXT_CODE arm guards on; otherwise, once the first
|
|
// header's B3+B5 pair was seen, every later header (channel
|
|
// change, title boundary, parser reuse) would be stored
|
|
// without its extension bytes — corrupting codecPrivate
|
|
// (interlace, chroma format, progressive-sequence flags).
|
|
self.has_extension = false;
|
|
// NOTE: a sequence header does NOT make the access unit a
|
|
// keyframe — that is decided solely by the PICTURE_CODE arm
|
|
// (picture_is_keyframe). Setting it here would mis-cue a
|
|
// seq-header-followed-by-P/B-frame PES.
|
|
saw_seq_header = true;
|
|
pos = if next_sc.is_some() { hdr_end } else { sc + 4 };
|
|
}
|
|
SEQ_EXT_CODE if self.seq_header.is_some() && !self.has_extension => {
|
|
// Sequence extension appears after seq header (may be in next PES).
|
|
// Append it to the stored seq_header.
|
|
let ext_end = find_start_code(data, sc + 4).unwrap_or(data.len());
|
|
if let Some(ref mut hdr) = self.seq_header {
|
|
hdr.extend_from_slice(&data[sc..ext_end]);
|
|
}
|
|
self.has_extension = true;
|
|
pos = ext_end;
|
|
}
|
|
PICTURE_CODE => {
|
|
// Picture header: bytes after start code contain temporal_reference
|
|
// (10 bits) + picture_coding_type (3 bits).
|
|
has_picture = true;
|
|
if sc + 5 < data.len() {
|
|
let picture_coding_type = (data[sc + 5] >> 3) & 0x07;
|
|
if picture_coding_type == PICTURE_TYPE_I {
|
|
picture_is_keyframe = true;
|
|
}
|
|
}
|
|
pos = sc + 4;
|
|
}
|
|
_ => {
|
|
pos = sc + 4;
|
|
}
|
|
}
|
|
}
|
|
|
|
// A PES that carried a sequence header but no picture start code is a
|
|
// parameter-set-only access unit: it has no coded picture to emit.
|
|
// Emitting it as a standalone keyframe would put bare sequence-header
|
|
// bytes into frame data with no picture. The sequence header is
|
|
// captured into codec_private above and is re-emitted in-band on the
|
|
// next real picture's PES, so dropping the empty access unit loses
|
|
// nothing. Mirrors how the H.264/HEVC parsers skip parameter-set-only
|
|
// access units.
|
|
//
|
|
// Conservative: only drop when this PES actually contained a sequence
|
|
// header and no picture. A PES with neither (e.g. a slice
|
|
// continuation) still passes through unchanged, preserving real
|
|
// keyframe detection.
|
|
// `saw_seq_header` is set by the scan loop's SEQ_HEADER_CODE arm above,
|
|
// so this reuses that single pass instead of re-scanning the PES bytes.
|
|
if !has_picture && saw_seq_header {
|
|
return Vec::new();
|
|
}
|
|
|
|
vec![Frame {
|
|
pts_ns,
|
|
keyframe: picture_is_keyframe,
|
|
data: pes.data.clone(),
|
|
duration_ns: None,
|
|
}]
|
|
}
|
|
|
|
fn codec_private(&self) -> Option<Vec<u8>> {
|
|
self.seq_header.clone()
|
|
}
|
|
}
|
|
|
|
/// 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<u8>, pts: Option<i64>) -> 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<u8> {
|
|
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<u8> {
|
|
// 00 00 01 00 [temporal_ref:10][picture_coding_type:3][...]
|
|
// temporal_reference = 0 for simplicity
|
|
// byte4 = temporal_ref[9:2] = 0x00
|
|
// byte5 = temporal_ref[1:0] | picture_coding_type[2:0] << 3 | ...
|
|
let byte5 = (coding_type & 0x07) << 3;
|
|
vec![0x00, 0x00, 0x01, PICTURE_CODE, 0x00, byte5, 0x00, 0x00]
|
|
}
|
|
|
|
// --- Sequence header parsing ---
|
|
|
|
#[test]
|
|
fn parse_sequence_header_resolution() {
|
|
let hdr = make_seq_header(720, 480, 2, 4);
|
|
let res = parse_resolution(&hdr);
|
|
assert_eq!(res, Some((720, 480)));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_sequence_header_1920x1080() {
|
|
let hdr = make_seq_header(1920, 1080, 3, 4);
|
|
let res = parse_resolution(&hdr);
|
|
assert_eq!(res, 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
|
|
let fr = parse_frame_rate(&hdr);
|
|
assert_eq!(fr, Some((30000, 1001)));
|
|
}
|
|
|
|
#[test]
|
|
fn parse_sequence_header_aspect_ratio() {
|
|
let hdr = make_seq_header(720, 480, 3, 4); // aspect code 3 = 16:9
|
|
let ar = parse_aspect_ratio(&hdr);
|
|
assert_eq!(ar, 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 = Vec::new();
|
|
data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I));
|
|
// Some payload data after the picture header.
|
|
data.extend_from_slice(&[0xFF; 16]);
|
|
|
|
let pes = make_pes(data, Some(90000));
|
|
let frames = parser.parse(&pes);
|
|
|
|
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 = Vec::new();
|
|
data.extend_from_slice(&make_picture_header(2)); // P-frame
|
|
data.extend_from_slice(&[0xFF; 16]);
|
|
|
|
let pes = make_pes(data, Some(90000));
|
|
let frames = parser.parse(&pes);
|
|
|
|
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 = Vec::new();
|
|
data.extend_from_slice(&make_picture_header(3)); // B-frame
|
|
data.extend_from_slice(&[0xFF; 16]);
|
|
|
|
let pes = make_pes(data, Some(90000));
|
|
let frames = parser.parse(&pes);
|
|
|
|
assert_eq!(frames.len(), 1);
|
|
assert!(!frames[0].keyframe, "B-frame should not be keyframe");
|
|
}
|
|
|
|
// --- Sequence header → codec_private ---
|
|
|
|
#[test]
|
|
fn codec_private_from_sequence_header() {
|
|
let mut parser = Mpeg2Parser::new();
|
|
|
|
let mut data = Vec::new();
|
|
let seq = make_seq_header(720, 480, 3, 4);
|
|
data.extend_from_slice(&seq);
|
|
// Follow with a picture header (I-frame).
|
|
data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I));
|
|
data.extend_from_slice(&[0xFF; 8]);
|
|
|
|
let pes = make_pes(data, Some(0));
|
|
let _frames = parser.parse(&pes);
|
|
|
|
let cp = parser.codec_private();
|
|
assert!(
|
|
cp.is_some(),
|
|
"codec_private should be available after sequence header"
|
|
);
|
|
let cp = cp.unwrap();
|
|
// Should start with the sequence header start code.
|
|
assert_eq!(&cp[..4], &[0x00, 0x00, 0x01, SEQ_HEADER_CODE]);
|
|
}
|
|
|
|
#[test]
|
|
fn codec_private_none_initially() {
|
|
let parser = Mpeg2Parser::new();
|
|
assert!(parser.codec_private().is_none());
|
|
}
|
|
|
|
// --- Sequence header with extension ---
|
|
|
|
#[test]
|
|
fn codec_private_includes_extension() {
|
|
let mut parser = Mpeg2Parser::new();
|
|
|
|
let mut data = Vec::new();
|
|
let seq = make_seq_header(1920, 1080, 3, 4);
|
|
data.extend_from_slice(&seq);
|
|
// Sequence extension: 00 00 01 B5 [ext data]
|
|
data.extend_from_slice(&[0x00, 0x00, 0x01, SEQ_EXT_CODE]);
|
|
data.extend_from_slice(&[0x14, 0x8A, 0x00, 0x01, 0x00, 0x00]); // ext payload
|
|
// Picture header follows.
|
|
data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I));
|
|
data.extend_from_slice(&[0xFF; 4]);
|
|
|
|
let pes = make_pes(data, Some(0));
|
|
let _frames = parser.parse(&pes);
|
|
|
|
let cp = parser.codec_private().unwrap();
|
|
// Should contain both sequence header and sequence extension start codes.
|
|
let has_ext = cp.windows(4).any(|w| w == [0x00, 0x00, 0x01, SEQ_EXT_CODE]);
|
|
assert!(has_ext, "codec_private should include sequence extension");
|
|
}
|
|
|
|
// --- sequence header + picture = keyframe ---
|
|
|
|
#[test]
|
|
fn sequence_header_with_picture_is_keyframe() {
|
|
let mut parser = Mpeg2Parser::new();
|
|
|
|
let mut data = Vec::new();
|
|
data.extend_from_slice(&make_seq_header(720, 480, 3, 4));
|
|
data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I));
|
|
data.extend_from_slice(&[0xFF; 16]);
|
|
|
|
let pes = make_pes(data, Some(0));
|
|
let frames = parser.parse(&pes);
|
|
|
|
assert_eq!(frames.len(), 1);
|
|
assert!(frames[0].keyframe);
|
|
// codecPrivate is still captured.
|
|
assert!(parser.codec_private().is_some());
|
|
}
|
|
|
|
// --- 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 (open-GOP /
|
|
// re-encoded MPEG-2) must NOT be flagged a keyframe — the keyframe-ness
|
|
// belongs to the coded picture, not the sequence header. A spurious
|
|
// keyframe here produces a bad MKV cue point.
|
|
let mut parser = Mpeg2Parser::new();
|
|
|
|
let mut data = Vec::new();
|
|
data.extend_from_slice(&make_seq_header(720, 480, 3, 4));
|
|
data.extend_from_slice(&make_picture_header(2)); // P-frame
|
|
data.extend_from_slice(&[0xFF; 16]);
|
|
|
|
let pes = make_pes(data, Some(0));
|
|
let frames = parser.parse(&pes);
|
|
|
|
assert_eq!(frames.len(), 1);
|
|
assert!(
|
|
!frames[0].keyframe,
|
|
"seq-header + P-frame must not be a keyframe"
|
|
);
|
|
// The sequence header is still captured for codecPrivate.
|
|
assert!(parser.codec_private().is_some());
|
|
}
|
|
|
|
// --- parameter-set-only PES (seq header, no picture) emits no frame ---
|
|
|
|
#[test]
|
|
fn sequence_header_only_pes_emits_no_frame() {
|
|
let mut parser = Mpeg2Parser::new();
|
|
|
|
// A PES carrying only a sequence header (+ extension), no picture.
|
|
let mut data = Vec::new();
|
|
data.extend_from_slice(&make_seq_header(1920, 1080, 3, 4));
|
|
data.extend_from_slice(&[0x00, 0x00, 0x01, SEQ_EXT_CODE]);
|
|
data.extend_from_slice(&[0x14, 0x8A, 0x00, 0x01, 0x00, 0x00]);
|
|
|
|
let pes = make_pes(data, Some(0));
|
|
let frames = parser.parse(&pes);
|
|
|
|
// No coded picture → no frame emitted, but the sequence header is
|
|
// still captured for codecPrivate.
|
|
assert!(
|
|
frames.is_empty(),
|
|
"parameter-set-only PES should not emit a frame"
|
|
);
|
|
assert!(
|
|
parser.codec_private().is_some(),
|
|
"sequence header should still be captured into codec_private"
|
|
);
|
|
|
|
// A following picture-bearing PES emits the real keyframe.
|
|
let mut data2 = Vec::new();
|
|
data2.extend_from_slice(&make_picture_header(PICTURE_TYPE_I));
|
|
data2.extend_from_slice(&[0xFF; 16]);
|
|
let frames2 = parser.parse(&make_pes(data2, Some(3600)));
|
|
assert_eq!(frames2.len(), 1);
|
|
assert!(frames2[0].keyframe);
|
|
}
|
|
|
|
// --- a SECOND sequence header re-captures its extension ---
|
|
|
|
#[test]
|
|
fn new_sequence_header_recaptures_extension() {
|
|
// Regression: has_extension was never reset when a new sequence header
|
|
// replaced the stored one, so a second header (channel change / title
|
|
// boundary) was stored WITHOUT its B5 sequence extension. To exercise
|
|
// the SEQ_EXT_CODE arm (which the has_extension flag guards), each
|
|
// header and its extension arrive in SEPARATE PES packets.
|
|
let mut parser = Mpeg2Parser::new();
|
|
|
|
// Header A (no trailing start code → captured alone), then its B5
|
|
// extension in the next PES.
|
|
let _ = parser.parse(&make_pes(make_seq_header(1920, 1080, 3, 4), Some(0)));
|
|
let mut ext_a = vec![0x00, 0x00, 0x01, SEQ_EXT_CODE];
|
|
ext_a.extend_from_slice(&[0x11, 0x22, 0x33, 0x44, 0x55, 0x66]);
|
|
let _ = parser.parse(&make_pes(ext_a, Some(0)));
|
|
assert!(
|
|
parser
|
|
.codec_private()
|
|
.unwrap()
|
|
.windows(6)
|
|
.any(|w| w == [0x11, 0x22, 0x33, 0x44, 0x55, 0x66]),
|
|
"first header's extension captured (has_extension now true)"
|
|
);
|
|
|
|
// A NEW header B, then ITS extension in a separate PES. With the bug,
|
|
// has_extension stayed true and this extension would be dropped.
|
|
let _ = parser.parse(&make_pes(make_seq_header(720, 480, 2, 4), Some(3600)));
|
|
let mut ext_b = vec![0x00, 0x00, 0x01, SEQ_EXT_CODE];
|
|
ext_b.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
|
|
let _ = parser.parse(&make_pes(ext_b, Some(3600)));
|
|
|
|
let cp2 = parser.codec_private().unwrap();
|
|
assert!(
|
|
cp2.windows(6)
|
|
.any(|w| w == [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]),
|
|
"second header's extension must be re-captured, not dropped"
|
|
);
|
|
// It is header B (720x480), not stale header A.
|
|
assert_eq!(parser.resolution(), Some((720, 480)));
|
|
}
|
|
|
|
// --- PTS conversion ---
|
|
|
|
#[test]
|
|
fn pts_conversion_to_nanoseconds() {
|
|
let mut parser = Mpeg2Parser::new();
|
|
|
|
let mut data = Vec::new();
|
|
data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I));
|
|
data.extend_from_slice(&[0xFF; 4]);
|
|
|
|
// 90000 ticks = 1 second = 1_000_000_000 ns
|
|
let pes = make_pes(data, Some(90000));
|
|
let frames = parser.parse(&pes);
|
|
|
|
assert_eq!(frames.len(), 1);
|
|
assert_eq!(frames[0].pts_ns, 1_000_000_000);
|
|
}
|
|
|
|
// --- Empty PES ---
|
|
|
|
#[test]
|
|
fn empty_pes_no_frames() {
|
|
let mut parser = Mpeg2Parser::new();
|
|
let pes = make_pes(Vec::new(), Some(0));
|
|
let frames = parser.parse(&pes);
|
|
assert!(frames.is_empty());
|
|
}
|
|
|
|
// --- Resolution helper methods ---
|
|
|
|
// --- parse_resolution: 12-bit field packing (ISO 13818-2 §6.2.2.1) ---
|
|
|
|
#[test]
|
|
fn resolution_packs_split_nibble_correctly() {
|
|
// h_size is bytes4-5[7:4] (12 bits), v_size is byte5[3:0]+byte6 (12 bits).
|
|
// Use a width/height whose nibbles differ so a swap would be caught:
|
|
// 0xABC x 0xDEF. byte4=0xAB, byte5=0xCD, byte6=0xEF.
|
|
let hdr = make_seq_header(0xABC, 0xDEF, 1, 1);
|
|
assert_eq!(parse_resolution(&hdr), Some((0xABC, 0xDEF)));
|
|
}
|
|
|
|
#[test]
|
|
fn resolution_max_12bit() {
|
|
// Max 12-bit dimension = 4095 (0xFFF) each.
|
|
let hdr = make_seq_header(4095, 4095, 1, 1);
|
|
assert_eq!(parse_resolution(&hdr), Some((4095, 4095)));
|
|
}
|
|
|
|
#[test]
|
|
fn resolution_too_short_none() {
|
|
// < 8 bytes → None, no panic.
|
|
assert_eq!(parse_resolution(&[0x00, 0x00, 0x01, 0xB3, 0x07]), None);
|
|
}
|
|
|
|
// --- parse_frame_rate: full table + reserved codes ---
|
|
|
|
#[test]
|
|
fn frame_rate_all_valid_codes() {
|
|
// ISO 13818-2 Table 6-4 frame_rate_code 1..=8.
|
|
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() {
|
|
// Code 0 is forbidden → None.
|
|
let hdr = make_seq_header(720, 480, 1, 0);
|
|
assert_eq!(parse_frame_rate(&hdr), None);
|
|
}
|
|
|
|
#[test]
|
|
fn frame_rate_code_out_of_range_none() {
|
|
// Codes 9..=15 are reserved (table has 9 entries, index 9..). 0x0F → None.
|
|
let hdr = make_seq_header(720, 480, 1, 0x0F);
|
|
assert_eq!(parse_frame_rate(&hdr), None);
|
|
}
|
|
|
|
// --- parse_aspect_ratio: table + reserved codes ---
|
|
|
|
#[test]
|
|
fn aspect_ratio_all_valid_codes() {
|
|
// ISO 13818-2 Table 6-3 aspect_ratio_information 1..=4.
|
|
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() {
|
|
let hdr = make_seq_header(720, 480, 0, 4);
|
|
assert_eq!(parse_aspect_ratio(&hdr), None);
|
|
}
|
|
|
|
#[test]
|
|
fn aspect_ratio_code_out_of_range_none() {
|
|
// Codes 5..=15 reserved. 0x0F → None.
|
|
let hdr = make_seq_header(720, 480, 0x0F, 4);
|
|
assert_eq!(parse_aspect_ratio(&hdr), None);
|
|
}
|
|
|
|
// --- picture_coding_type: byte position + bit field ---
|
|
|
|
#[test]
|
|
fn picture_coding_type_bits_5_3() {
|
|
// picture_coding_type is byte5 bits 5-3 (>> 3 & 0x07). I=1 (keyframe),
|
|
// P=2, B=3, all others (D=4, reserved) not keyframes.
|
|
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 = parser.parse(&make_pes(data, Some(0)));
|
|
assert_eq!(f.len(), 1);
|
|
assert_eq!(
|
|
f[0].keyframe, is_kf,
|
|
"picture_coding_type {ct}: keyframe={is_kf}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn picture_header_too_short_not_keyframe() {
|
|
// A picture start code with too few following bytes to read byte5 must
|
|
// NOT panic and must NOT be flagged a keyframe (the `sc + 5 < len` guard
|
|
// is false). 00 00 01 00 + only 1 byte.
|
|
let mut parser = Mpeg2Parser::new();
|
|
let data = vec![0x00, 0x00, 0x01, PICTURE_CODE, 0x00];
|
|
let f = parser.parse(&make_pes(data, Some(0)));
|
|
assert_eq!(f.len(), 1, "picture present but header truncated");
|
|
assert!(!f[0].keyframe, "truncated picture header → not keyframe");
|
|
}
|
|
|
|
// --- seq-header exact-size calc when no following start code (quantizers) ---
|
|
|
|
#[test]
|
|
fn seq_header_without_following_sc_captures_base_when_no_quantizers() {
|
|
// When a sequence header has no following start code in the PES, the
|
|
// parser computes its exact byte length. With load_intra_quantiser_matrix
|
|
// = 0 and load_non_intra = 0 (byte11 bit1 clear), the header is the base
|
|
// size (no 64-byte quantizer blocks appended). make_seq_header sets
|
|
// byte11 (index sc+11) — our 8-byte tail's last byte is 0x00 → both flags
|
|
// clear. The captured codecPrivate must be the base header only.
|
|
let mut parser = Mpeg2Parser::new();
|
|
let seq = make_seq_header(1920, 1080, 3, 4);
|
|
let base_len = seq.len();
|
|
// Sequence header alone in the PES (no picture, no next SC). It is a
|
|
// parameter-set-only AU → no frame, but codecPrivate is captured.
|
|
let f = parser.parse(&make_pes(seq, Some(0)));
|
|
assert!(f.is_empty(), "seq-header-only PES emits no frame");
|
|
let cp = parser.codec_private().expect("seq header captured");
|
|
// The capture must not run past the buffer; length <= what we provided.
|
|
assert!(
|
|
cp.len() <= base_len,
|
|
"captured header bounded by provided bytes"
|
|
);
|
|
assert_eq!(&cp[..4], &[0x00, 0x00, 0x01, SEQ_HEADER_CODE]);
|
|
}
|
|
|
|
#[test]
|
|
fn picture_without_start_code_passes_through_keyframe_false() {
|
|
// A PES with neither a sequence header nor a picture start code (a slice
|
|
// continuation) passes through unchanged and is not a keyframe (the
|
|
// `!has_picture && saw_seq_header` drop only fires when a seq header was
|
|
// seen).
|
|
let mut parser = Mpeg2Parser::new();
|
|
// 00 00 01 01 is a slice start code (0x01), not picture/seq/ext.
|
|
let data = vec![0x00, 0x00, 0x01, 0x01, 0xAA, 0xBB, 0xCC];
|
|
let f = parser.parse(&make_pes(data.clone(), Some(0)));
|
|
assert_eq!(f.len(), 1, "slice continuation passes through");
|
|
assert!(!f[0].keyframe);
|
|
assert_eq!(f[0].data, data, "data passed through verbatim");
|
|
}
|
|
|
|
#[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: data.clone(),
|
|
};
|
|
let f = parser.parse(&pes);
|
|
assert_eq!(f[0].pts_ns, 1_000_000_000, "DTS fallback");
|
|
|
|
let mut parser2 = Mpeg2Parser::new();
|
|
let pes2 = PesPacket {
|
|
pid: 0x1011,
|
|
pts: None,
|
|
dts: None,
|
|
data,
|
|
};
|
|
let f2 = parser2.parse(&pes2);
|
|
assert_eq!(f2[0].pts_ns, 0, "no PTS/DTS → 0");
|
|
}
|
|
|
|
#[test]
|
|
fn frame_data_is_whole_pes_not_just_picture() {
|
|
// The emitted frame data is the ENTIRE PES payload (pes.data.clone()),
|
|
// not just the picture NAL — MPEG-2 ES is muxed as-is. Confirm a seq
|
|
// header + picture PES emits the whole buffer.
|
|
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(&[0x12, 0x34]);
|
|
let f = parser.parse(&make_pes(data.clone(), Some(0)));
|
|
assert_eq!(f.len(), 1);
|
|
assert_eq!(f[0].data, data, "frame data = whole PES payload");
|
|
}
|
|
|
|
#[test]
|
|
fn parser_resolution_method() {
|
|
let mut parser = Mpeg2Parser::new();
|
|
|
|
let mut data = Vec::new();
|
|
data.extend_from_slice(&make_seq_header(720, 576, 2, 3));
|
|
data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I));
|
|
data.extend_from_slice(&[0xFF; 4]);
|
|
|
|
let pes = make_pes(data, Some(0));
|
|
let _ = parser.parse(&pes);
|
|
|
|
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
|
|
}
|
|
}
|