use std::collections::VecDeque;
use super::coding::{CodingType, Mpeg2Coding, PictureInfo};
use super::startcode::find_start_code;
use super::{CodecParser, Frame, pts_to_ns};
use crate::mux::ts::PesPacket;
use crate::pes::SourcePos;
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)>,
source_marks: VecDeque<(u64, SourcePos)>,
frame_duration_ns: i64,
progressive_sequence: bool,
gop_buf: Vec<BufferedPicture>,
emitted_fields: u64,
origin_pts_ns: Option<i64>,
}
struct BufferedPicture {
tr: u64,
info: PictureInfo,
explicit_pts: Option<i64>,
frame: Frame,
}
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(),
source_marks: VecDeque::new(),
frame_duration_ns: 0,
progressive_sequence: false,
gop_buf: Vec::new(),
emitted_fields: 0,
origin_pts_ns: None,
}
}
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 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;
}
}
while let Some(&(off, _)) = self.source_marks.front() {
if off < cutoff {
self.source_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 raw_coding_type = if pic + 5 < end {
(self.buf[pic + 5] >> 3) & 0x07
} else {
0
};
let tr = if pic + 5 < end {
(((self.buf[pic + 4] as u64) << 2) | ((self.buf[pic + 5] as u64) >> 6)) & 0x3FF
} else {
0
};
let end_abs = self.base_offset + end as u64;
let data = self.buf[..end].to_vec();
if let Some(h) = hdr {
self.progressive_sequence = parse_progressive_sequence(&h);
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;
}
}
}
let (tff, rff, progressive_frame, frame_picture) = picture_coding_flags(&data);
let info = PictureInfo::mpeg2(
coding_type_from_raw(raw_coding_type),
Mpeg2Coding {
top_field_first: tff,
repeat_first_field: rff,
progressive_frame,
progressive_sequence: self.progressive_sequence,
frame_picture,
},
);
let keyframe = info.keyframe();
let explicit = self
.pts_marks
.front()
.filter(|&&(off, _)| off < end_abs)
.map(|&(_, p)| p);
let src = self
.source_marks
.front()
.filter(|&&(off, _)| off < end_abs)
.map(|&(_, s)| s);
if gop_boundary && !self.gop_buf.is_empty() {
self.flush_gop(&mut out);
}
self.gop_buf.push(BufferedPicture {
tr,
info,
explicit_pts: explicit,
frame: Frame {
pts_ns: 0,
keyframe,
data,
duration_ns: None,
coding: Some(info),
source: src,
},
});
if self.gop_buf.len() >= MAX_PENDING_FRAMES {
self.flush_gop(&mut out);
}
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;
}
}
while let Some(&(off, _)) = self.source_marks.front() {
if off < end_abs {
self.source_marks.pop_front();
} else {
break;
}
}
}
if force {
self.flush_gop(&mut out);
}
out
}
fn flush_gop(&mut self, out: &mut Vec<Frame>) {
let n = self.gop_buf.len();
if n == 0 {
return;
}
let field_period = self.frame_duration_ns / 2;
if field_period <= 0 {
for bp in self.gop_buf.drain(..) {
let mut f = bp.frame;
f.pts_ns = bp.explicit_pts.unwrap_or(0);
out.push(f);
}
return;
}
let mut order: Vec<usize> = (0..n).collect();
order.sort_by_key(|&i| self.gop_buf[i].tr);
let mut cum_before = vec![0u64; n];
let mut running = 0u64;
for &i in &order {
cum_before[i] = running;
running += self.gop_buf[i].info.nb_fields() as u64;
}
let gop_fields = running;
let base = self.emitted_fields;
for &i in &order {
if let Some(p) = self.gop_buf[i].explicit_pts {
self.origin_pts_ns = Some(p - field_period * (base + cum_before[i]) as i64);
break;
}
}
let origin = self.origin_pts_ns.unwrap_or(0);
for (i, mut bp) in self.gop_buf.drain(..).enumerate() {
bp.frame.pts_ns = origin + field_period * (base + cum_before[i]) as i64;
bp.frame.duration_ns = Some(bp.info.nb_fields() as u64 * field_period as u64);
out.push(bp.frame);
}
self.emitted_fields += gop_fields;
}
}
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)));
}
if let Some(src) = pes.source {
self.source_marks.push_back((off, src));
}
self.buf.extend_from_slice(&pes.data);
self.drain_complete_aus(false)
}
fn flush(&mut self) -> Vec<Frame> {
self.drain_complete_aus(true)
}
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])
}
fn picture_coding_flags(au: &[u8]) -> (bool, bool, bool, bool) {
let mut search = 0;
while let Some(q) = find_code(au, search, SEQ_EXT_CODE) {
search = q + 4;
if au.get(q + 4).map(|b| b >> 4) != Some(0b1000) {
continue;
}
let (Some(&e2), Some(&e3), Some(&e4)) = (au.get(q + 6), au.get(q + 7), au.get(q + 8))
else {
break;
};
let frame_picture = e2 & 0x03 == 0b11;
let tff = (e3 >> 7) & 1 == 1;
let rff = (e3 >> 1) & 1 == 1;
let progressive_frame = (e4 >> 7) & 1 == 1;
return (tff, rff, progressive_frame, frame_picture);
}
(false, false, true, true)
}
fn coding_type_from_raw(raw: u8) -> CodingType {
match raw {
1 => CodingType::I,
3 => CodingType::B,
_ => CodingType::P,
}
}
fn picture_nb_fields(au: &[u8], progressive_sequence: bool) -> u8 {
let mut search = 0;
while let Some(q) = find_code(au, search, SEQ_EXT_CODE) {
search = q + 4;
if au.get(q + 4).map(|b| b >> 4) != Some(0b1000) {
continue;
}
let (Some(&e2), Some(&e3), Some(&e4)) = (au.get(q + 6), au.get(q + 7), au.get(q + 8))
else {
break;
};
if e2 & 0x03 != 0b11 {
return 1;
}
let tff = (e3 >> 7) & 1;
let rff = (e3 >> 1) & 1;
let progressive_frame = (e4 >> 7) & 1;
let repeat_pict = if rff == 0 {
0
} else if progressive_sequence {
if tff == 1 { 4 } else { 2 }
} else if progressive_frame == 1 {
1
} else {
0
};
return repeat_pict + 2;
}
2
}
fn parse_progressive_sequence(hdr: &[u8]) -> bool {
let mut search = 0;
while let Some(q) = find_code(hdr, search, SEQ_EXT_CODE) {
search = q + 4;
if hdr.get(q + 4).map(|b| b >> 4) != Some(0b0001) {
continue;
}
return hdr.get(q + 5).map(|&b| (b >> 3) & 1 == 1).unwrap_or(false);
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mux::ts::PesPacket;
fn pic_coding_ext(tff: u8, rff: u8, progressive_frame: u8, frame_picture: bool) -> Vec<u8> {
let e0 = 0x80; let e1 = 0x00;
let e2 = if frame_picture { 0x03 } else { 0x01 }; let e3 = (tff << 7) | (rff << 1);
let e4 = progressive_frame << 7;
vec![0x00, 0x00, 0x01, SEQ_EXT_CODE, e0, e1, e2, e3, e4]
}
#[test]
fn nb_fields_normal_frame_is_two() {
assert_eq!(picture_nb_fields(&pic_coding_ext(0, 0, 0, true), false), 2);
}
#[test]
fn nb_fields_telecine_repeat_field_is_three() {
assert_eq!(picture_nb_fields(&pic_coding_ext(0, 1, 1, true), false), 3);
}
#[test]
fn nb_fields_field_picture_is_one() {
assert_eq!(picture_nb_fields(&pic_coding_ext(0, 0, 0, false), false), 1);
}
#[test]
fn nb_fields_progressive_seq_rff_tff_is_six() {
assert_eq!(picture_nb_fields(&pic_coding_ext(1, 1, 0, true), true), 6);
}
#[test]
fn nb_fields_progressive_seq_rff_no_tff_is_four() {
assert_eq!(picture_nb_fields(&pic_coding_ext(0, 1, 0, true), true), 4);
}
#[test]
fn nb_fields_no_picture_ext_defaults_two() {
assert_eq!(picture_nb_fields(&[0, 0, 1, 0x00, 0, 0], false), 2);
}
#[test]
fn parser_populates_full_pictureinfo_and_source() {
use crate::mux::codec::coding::FieldOrder;
let mk_pes = |data: Vec<u8>, byte: u64| PesPacket {
source: Some(crate::pes::SourcePos::at_byte(byte)),
pid: 0x1011,
pts: None,
dts: None,
data,
};
let mut p = Mpeg2Parser::new();
let mut frames = Vec::new();
let mut au = make_seq_header(720, 576, 3, 3); au.extend_from_slice(&make_picture_header(1));
au.extend_from_slice(&pic_coding_ext(1, 0, 0, true));
frames.extend(p.parse(&mk_pes(au, 0)));
let mut au = make_picture_header(2);
au.extend_from_slice(&pic_coding_ext(0, 0, 0, true));
frames.extend(p.parse(&mk_pes(au, 2048)));
let mut au = make_picture_header(3);
au.extend_from_slice(&pic_coding_ext(0, 1, 1, true));
frames.extend(p.parse(&mk_pes(au, 4096)));
frames.extend(p.flush());
assert_eq!(frames.len(), 3, "three pictures → three frames");
for f in &frames {
assert!(f.coding.is_some(), "every MPEG-2 frame carries PictureInfo");
assert!(
f.source.is_some(),
"every frame carries SourcePos provenance"
);
}
let frame = |t: CodingType| {
frames
.iter()
.find(|f| f.coding.unwrap().coding_type() == t)
.unwrap_or_else(|| panic!("no {t:?} frame"))
};
let i = frame(CodingType::I);
assert_eq!(
i.source.unwrap().byte,
0,
"I frame keeps its PES source @ 0"
);
let ic = i.coding.unwrap();
assert!(ic.keyframe(), "I picture is a keyframe");
assert_eq!(ic.field_order(), Some(FieldOrder::Tff), "tff=1 → TFF");
assert_eq!(ic.nb_fields(), 2, "normal interlaced frame = 2 fields");
assert_eq!(ic.progressive(), Some(false));
let pp = frame(CodingType::P);
assert_eq!(
pp.source.unwrap().byte,
2048,
"P frame keeps its PES source"
);
let pc = pp.coding.unwrap();
assert!(!pc.keyframe());
assert_eq!(
pc.field_order(),
Some(FieldOrder::Bff),
"tff=0 interlaced frame → BFF (the red-flag fix)"
);
assert_eq!(pc.nb_fields(), 2);
let b = frame(CodingType::B);
assert_eq!(b.source.unwrap().byte, 4096, "B frame keeps its PES source");
let bc = b.coding.unwrap();
assert!(!bc.keyframe());
assert_eq!(
bc.field_order(),
Some(FieldOrder::Progressive),
"progressive_frame → Progressive (no field order)"
);
assert_eq!(
bc.nb_fields(),
3,
"rff + progressive_frame in interlaced seq → 2:3 pulldown = 3 fields"
);
assert_eq!(bc.progressive(), Some(true));
}
#[test]
fn progressive_sequence_parsed_from_seq_ext() {
assert!(parse_progressive_sequence(&[
0,
0,
1,
SEQ_EXT_CODE,
0x10,
0x08
]));
assert!(!parse_progressive_sequence(&[
0,
0,
1,
SEQ_EXT_CODE,
0x10,
0x00
]));
assert!(!parse_progressive_sequence(&[
0,
0,
1,
SEQ_HEADER_CODE,
0,
0
]));
}
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
PesPacket {
source: None,
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_in_one_gop_emit_both_on_flush() {
let mut parser = Mpeg2Parser::new();
let mut pic1 = make_picture_header(PICTURE_TYPE_I);
pic1.extend_from_slice(&[0x11; 100]);
let mut pic2 = make_picture_header(2); pic2.extend_from_slice(&[0x22; 100]);
let mut stream = pic1.clone();
stream.extend_from_slice(&pic2);
let frames = parser.parse(&make_pes(stream, Some(0)));
assert!(frames.is_empty(), "same GOP — buffered until flush");
let frames = parser.flush();
assert_eq!(frames.len(), 2);
assert_eq!(frames[0].data, pic1);
assert!(frames[0].keyframe);
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(&[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(&[0x11; 50]);
let frames1 = parser.parse(&make_pes(pic1, Some(90000)));
assert!(frames1.is_empty(), "buffered until flush");
let mut pic2 = make_picture_header(2);
pic2.extend_from_slice(&[0x22; 50]);
let frames2 = parser.parse(&make_pes(pic2, Some(180000)));
assert!(frames2.is_empty(), "same GOP — still buffered");
let frames = parser.flush();
assert_eq!(frames.len(), 2);
assert_eq!(frames[0].pts_ns, 1_000_000_000, "pic1 → PTS 90000");
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));
}
fn make_pulldown_picture(coding_type: u8, tr: u16, rff: u8) -> Vec<u8> {
let mut au = make_picture_header_tr(coding_type, tr);
au.extend_from_slice(&[
0x00,
0x00,
0x01,
SEQ_EXT_CODE,
0x80,
0x00,
0x03,
rff << 1,
0x80,
]);
au.extend_from_slice(&[0xAA; 16]);
au
}
#[test]
fn telecine_pts_accumulates_by_field_durations_not_a_fixed_grid() {
let mut p = Mpeg2Parser::new();
let field = 1_000_000_000i64 * 1001 / 30000 / 2;
let mut a = make_seq_header(720, 480, 2, 4);
a.extend_from_slice(&gop());
a.extend(make_pulldown_picture(1, 0, 1)); a.extend(make_pulldown_picture(2, 1, 0)); let mut frames = p.parse(&make_pes(a, Some(0)));
frames.extend(p.flush());
assert_eq!(frames.len(), 2);
assert_eq!(frames[0].pts_ns, 0, "I anchored to PES PTS 0");
assert_eq!(
frames[0].duration_ns,
Some(3 * field as u64),
"I = 3 fields"
);
assert_eq!(
frames[1].pts_ns,
3 * field,
"P starts exactly at I-end (3 fields), not the 1/29.97 grid"
);
assert_eq!(
frames[1].duration_ns,
Some(2 * field as u64),
"P = 2 fields"
);
assert!(frames[1].pts_ns > frames[0].pts_ns, "strictly monotonic");
}
#[test]
fn b_frames_emit_in_decode_order_with_lower_display_pts() {
let mut p = Mpeg2Parser::new();
let field = 1_000_000_000i64 * 1001 / 30000 / 2;
let mut a = make_seq_header(720, 480, 2, 4);
a.extend_from_slice(&gop());
a.extend(make_pulldown_picture(1, 0, 0)); a.extend(make_pulldown_picture(2, 2, 0)); a.extend(make_pulldown_picture(3, 1, 0)); let mut frames = p.parse(&make_pes(a, Some(0)));
frames.extend(p.flush());
assert_eq!(frames.len(), 3);
assert!(frames[0].keyframe, "decode order preserved: I first");
assert_eq!(frames[0].pts_ns, 0, "I (tr0) displays 1st");
assert_eq!(frames[1].pts_ns, 4 * field, "P (tr2) displays 3rd");
assert_eq!(frames[2].pts_ns, 2 * field, "B (tr1) displays 2nd");
assert!(
frames[2].pts_ns < frames[1].pts_ns,
"B emitted AFTER P (decode order) but displays BEFORE it (lower PTS)"
);
}
#[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 opening_au_keeps_disc_pts_and_opening_seq_header_no_zero_floor() {
let mut p = Mpeg2Parser::new();
let mut a = make_seq_header(720, 576, 3, 3); a.extend_from_slice(&gop());
a.extend_from_slice(&make_picture_header_tr(PICTURE_TYPE_I, 0));
a.extend_from_slice(&[0xAA; 20]);
let mut frames = p.parse(&make_pes(a, Some(180_000))); assert!(frames.is_empty(), "first AU waits for the next boundary");
let mut b = make_picture_header_tr(3, 1);
b.extend_from_slice(&[0xBB; 20]);
frames.extend(p.parse(&make_pes(b, None)));
let cp = p
.codec_private()
.expect("opening seq header captured at headers-ready");
assert_eq!(
&cp[..4],
&[0x00, 0x00, 0x01, SEQ_HEADER_CODE],
"codec_private is the opening sequence header"
);
assert_eq!(p.resolution(), Some((720, 576)), "576i opening header");
assert_eq!(p.frame_rate(), Some((25, 1)), "25 fps opening header");
frames.extend(p.flush());
assert_eq!(frames.len(), 2);
assert!(frames[0].keyframe, "opening picture is the I-frame");
assert_eq!(
frames[0].pts_ns, 2_000_000_000,
"opening I-frame keeps the disc's real PTS (2 s), NOT floored to 0"
);
assert_eq!(
frames[1].pts_ns, 2_040_000_000,
"next frame is one 40 ms interval later on the real timeline"
);
}
#[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!(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 {
source: None,
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 {
source: None,
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!(
frames.is_empty(),
"over-cap AU is force-COMPLETED (bounded) but buffered in its GOP"
);
let frames = parser.flush();
assert_eq!(frames.len(), 1, "force-flushed at EOF, not dropped");
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))); }
}