The video codec parsers (HEVC, H.264, VC-1, MPEG-2) used pes.dts.or(pes.pts) as each frame's timestamp. MKV block timecodes are presentation timestamps; frames are stored in decode order and the player reorders for display by timecode. Using DTS makes the timecode monotonic in storage order, presenting B-frames in decode order — visible motion judder / wrong frames on playback, and PTS-based seeking lands on the wrong frame. The compressed video was always byte-correct (verified by NAL-level diff against a known-good demux); this was purely a timestamp defect affecting every B-frame title. Fix: prefer PTS (pes.pts.or(pes.dts)). Verified on a real UHD iso->mkv: emitted PTS now reorders for B-frames identically to a reference muxer. Update the two tests that asserted the old DTS-preferred behavior and add an HEVC regression test pinning PTS as the block timecode.
572 lines
20 KiB
Rust
572 lines
20 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::{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 {
|
|
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;
|
|
let mut keyframe = false;
|
|
let mut has_picture = 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());
|
|
keyframe = 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 {
|
|
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.
|
|
if !has_picture && contains_seq_header(data) {
|
|
return Vec::new();
|
|
}
|
|
|
|
vec![Frame {
|
|
pts_ns,
|
|
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])
|
|
}
|
|
|
|
/// Returns true if `data` contains a sequence-header start code (00 00 01 B3).
|
|
fn contains_seq_header(data: &[u8]) -> bool {
|
|
let mut pos = 0;
|
|
while let Some(sc) = find_start_code(data, pos) {
|
|
if sc + 3 >= data.len() {
|
|
break;
|
|
}
|
|
if data[sc + 3] == SEQ_HEADER_CODE {
|
|
return true;
|
|
}
|
|
pos = sc + 4;
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Find the position of the next start code (00 00 01) at or after `from`.
|
|
fn find_start_code(data: &[u8], from: usize) -> Option<usize> {
|
|
if data.len() < from + 3 {
|
|
return None;
|
|
}
|
|
(from..data.len() - 2).find(|&i| data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x01)
|
|
}
|
|
|
|
#[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());
|
|
}
|
|
|
|
// --- 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);
|
|
}
|
|
|
|
// --- 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 ---
|
|
|
|
#[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
|
|
}
|
|
}
|