use std::collections::VecDeque;
use super::startcode::find_start_code;
use super::{CodecParser, Frame, pts_to_ns};
use crate::mux::ts::PesPacket;
const SEQ_HEADER_CODE: u8 = 0xB3;
const SEQ_EXT_CODE: u8 = 0xB5;
const GOP_CODE: u8 = 0xB8;
const PICTURE_CODE: u8 = 0x00;
const PICTURE_TYPE_I: u8 = 1;
const MAX_AU_BUFFER: usize = 8 * 1024 * 1024;
const MAX_PENDING_FRAMES: usize = 600;
const MAX_PENDING_BYTES: usize = 8 * 1024 * 1024;
const FRAME_RATES: [(u32, u32); 9] = [
(0, 1), (24000, 1001), (24, 1), (25, 1), (30000, 1001), (30, 1), (50, 1), (60000, 1001), (60, 1), ];
const ASPECT_RATIOS: [(u8, u8); 5] = [
(0, 0), (1, 1), (4, 3), (16, 9), (221, 100), ];
pub struct Mpeg2Parser {
seq_header: Option<Vec<u8>>,
buf: Vec<u8>,
base_offset: u64,
pts_marks: VecDeque<(u64, i64)>,
frame_duration_ns: i64,
gop_base: u64,
gop_count: u64,
anchor_index: Option<u64>,
anchor_pts: i64,
pending: Vec<(u64, Frame)>,
pending_bytes: usize,
}
impl Default for Mpeg2Parser {
fn default() -> Self {
Self::new()
}
}
impl Mpeg2Parser {
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(),
pending_bytes: 0,
}
}
pub fn resolution(&self) -> Option<(u16, u16)> {
let hdr = self.seq_header.as_ref()?;
parse_resolution(hdr)
}
pub fn frame_rate(&self) -> Option<(u32, u32)> {
let hdr = self.seq_header.as_ref()?;
parse_frame_rate(hdr)
}
pub fn aspect_ratio(&self) -> Option<(u8, u8)> {
let hdr = self.seq_header.as_ref()?;
parse_aspect_ratio(hdr)
}
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
}
fn drain_complete_aus(&mut self, force: bool) -> Vec<Frame> {
let mut out = Vec::new();
loop {
let Some(pic) = find_code(&self.buf, 0, PICTURE_CODE) else {
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;
};
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, };
if end == 0 {
break;
}
let hdr = extract_seq_header(&self.buf[..end]);
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;
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();
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;
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 {
match explicit {
Some(p) => {
self.anchor_index = Some(display_index);
self.anchor_pts = p;
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);
}
self.pending_bytes = 0;
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
&& self.pending_bytes < MAX_PENDING_BYTES =>
{
self.pending_bytes += frame.data.len();
self.pending.push((display_index, frame));
}
None => {
for (di, mut held) in self.pending.drain(..) {
held.pts_ns = di as i64 * self.frame_duration_ns;
out.push(held);
}
self.pending_bytes = 0;
frame.pts_ns = display_index as i64 * self.frame_duration_ns;
out.push(frame);
}
},
}
} else {
frame.pts_ns = self.pts_for(pic_abs);
out.push(frame);
}
self.gop_count += 1;
self.buf.drain(..end);
self.base_offset = end_abs;
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<Frame> {
if pes.data.is_empty() {
return Vec::new();
}
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<Frame> {
let mut out = self.drain_complete_aus(true);
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);
}
self.pending_bytes = 0;
}
out
}
fn codec_private(&self) -> Option<Vec<u8>> {
self.seq_header.clone()
}
}
fn extract_seq_header(au: &[u8]) -> Option<Vec<u8>> {
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())
}
fn find_code(data: &[u8], from: usize, want: u8) -> Option<usize> {
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
}
fn find_au_start(data: &[u8], from: usize) -> Option<usize> {
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
}
fn parse_resolution(hdr: &[u8]) -> Option<(u16, u16)> {
if hdr.len() < 8 {
return None;
}
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))
}
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])
}
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,
}
}
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));
hdr.extend_from_slice(&[0xFF, 0xFF, 0xFF, 0x00]);
hdr
}
fn make_picture_header(coding_type: u8) -> Vec<u8> {
let byte5 = (coding_type & 0x07) << 3;
vec![0x00, 0x00, 0x01, PICTURE_CODE, 0x00, byte5, 0x00, 0x00]
}
fn gop() -> Vec<u8> {
vec![0x00, 0x00, 0x01, GOP_CODE, 0x00, 0x00, 0x00, 0x00]
}
fn make_picture_header_tr(coding_type: u8, tr: u16) -> Vec<u8> {
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]
}
fn parse_then_flush(parser: &mut Mpeg2Parser, pes: &PesPacket) -> Vec<Frame> {
let mut frames = parser.parse(pes);
frames.extend(parser.flush());
frames
}
#[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); 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); 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());
}
#[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); 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); 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");
}
#[test]
fn picture_fragmented_across_pes_is_reassembled_into_one_frame() {
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]);
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)));
}
assert!(frames.is_empty(), "incomplete AU must not emit fragments");
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() {
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); 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() {
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]); au.extend_from_slice(&[0x00, 0x00, 0x01, 0x01]); 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"
);
}
#[test]
fn each_picture_gets_the_pts_of_the_pes_that_began_it() {
let mut parser = Mpeg2Parser::new();
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");
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");
}
#[test]
fn sparse_pts_interpolated_by_temporal_reference() {
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 frames = p.parse(&make_pes(a, Some(0)));
assert!(
frames.is_empty(),
"first AU waits for the next picture boundary"
);
let mut b1 = make_picture_header_tr(3, 1);
b1.extend_from_slice(&[0xBB; 20]);
frames.extend(p.parse(&make_pes(b1, None)));
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() {
let mut p = Mpeg2Parser::new();
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)));
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); assert_eq!(frames[1].pts_ns, 40_000_000); assert_eq!(
frames[2].pts_ns, 80_000_000,
"gop_base keeps the clock climbing"
);
}
#[test]
fn leading_frames_buffered_until_first_pts_anchor() {
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));
let mut b1 = make_picture_header_tr(3, 1);
b1.extend_from_slice(&[0xBB; 20]);
f.extend(p.parse(&make_pes(b1, None)));
let mut b2 = make_picture_header_tr(3, 2);
b2.extend_from_slice(&[0xCC; 20]);
f.extend(p.parse(&make_pes(b2, Some(180000)))); f.extend(p.flush());
assert_eq!(f.len(), 3);
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"
);
assert!(f[0].keyframe);
}
#[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);
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"
);
assert!(
!cp.windows(4).any(|w| w == [0x00, 0x00, 0x01, PICTURE_CODE]),
"codec_private must NOT include the picture start code"
);
}
#[test]
fn seq_header_then_p_frame_is_not_keyframe() {
let mut parser = Mpeg2Parser::new();
let mut data = make_seq_header(720, 480, 3, 4);
data.extend_from_slice(&make_picture_header(2)); 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());
}
#[test]
fn new_sequence_header_replaces_codec_private() {
let mut parser = Mpeg2Parser::new();
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()); let fa = parser.parse(&make_pes(a, Some(0)));
assert_eq!(fa.len(), 1);
assert_eq!(parser.resolution(), Some((1920, 1080)));
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"
);
}
#[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");
}
#[test]
fn empty_pes_no_frames() {
let mut parser = Mpeg2Parser::new();
assert!(parser.parse(&make_pes(Vec::new(), Some(0))).is_empty());
}
#[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]);
let frames = parse_then_flush(&mut parser, &make_pes(data, Some(0)));
assert!(frames.is_empty(), "no coded picture → no frame");
}
#[test]
fn oversized_au_without_boundary_is_force_flushed() {
let mut parser = Mpeg2Parser::new();
let mut data = make_picture_header(PICTURE_TYPE_I);
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);
}
#[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);
}
#[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);
}
#[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
);
}
#[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))); assert_eq!(parser.aspect_ratio(), Some((4, 3))); }
}