Audit fixes + DVD support foundation (IFO, PS demux, MPEG-2, CSS crack)

Audit fixes (14 critical, 22 warnings):
- UDF: bounds checks on all ICB/FID parsing from disc data
- SCSI Linux: saturating_sub on residual, CDB length guard, buffer size guard
- SCSI macOS: SCSITaskStatus u32 (was u8 — stack corruption)
- AACS: EC mod_inv returns infinity instead of panic, key reduced mod n
- AACS: do_handshake tries all host certs (was returning on first failure)
- H.264: bounds check on SPS < 4 bytes
- ContentReader: error on missing unit key (was zero-fill)
- KEYDB: flat redirect loop (was recursive), 100MB response limit, Windows HOME fallback
- ISO writer: AVDP extent order, partition length, allocation cap
- Network: removed TCP_NODELAY on bulk stream
- MKV: guard on u64::MAX seek
- disc.rs: saturating_sub on extent offset, simplified dead region code
- cargo fmt (610 violations), cargo clippy --fix (55 auto-fixes)

DVD support (new files):
- src/ifo.rs — IFO parser (VIDEO_TS.IFO, VTS_XX_0.IFO, PGC chains, cells, streams) — 13 tests
- src/mux/ps.rs — MPEG-2 Program Stream demuxer (pack headers, PES, private stream 1) — 12 tests
- src/mux/codec/mpeg2.rs — MPEG-2 video parser (sequence headers, I-frame detection) — 15 tests
- src/css/crack.rs — split-attack algorithm (LFSR cipher needs verification — test ignored)

226 tests total (was 186), 1 ignored (CSS crack needs cipher verification).
This commit is contained in:
MattJackson
2026-04-11 16:52:22 +00:00
parent 6e771a1867
commit ff5547363b
57 changed files with 6189 additions and 1519 deletions
+13 -2
View File
@@ -4,10 +4,16 @@
//! Each PES packet typically contains exactly one AC3 frame.
//! All AC3 frames are effectively keyframes (no inter-frame dependencies).
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
use super::{pts_to_ns, CodecParser, Frame, PesPacket};
pub struct Ac3Parser;
impl Default for Ac3Parser {
fn default() -> Self {
Self::new()
}
}
impl Ac3Parser {
pub fn new() -> Self {
Self
@@ -54,7 +60,12 @@ mod tests {
use crate::mux::ts::PesPacket;
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
PesPacket { pid: 0x1100, pts, dts: None, data }
PesPacket {
pid: 0x1100,
pts,
dts: None,
data,
}
}
// --- syncword detection ---
+24 -5
View File
@@ -5,12 +5,20 @@
//! All frames are keyframes (no inter-frame dependencies).
//! Each PES packet = one frame.
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
use super::{pts_to_ns, CodecParser, Frame, PesPacket};
pub struct DtsParser;
impl Default for DtsParser {
fn default() -> Self {
Self::new()
}
}
impl DtsParser {
pub fn new() -> Self { Self }
pub fn new() -> Self {
Self
}
}
impl CodecParser for DtsParser {
@@ -19,10 +27,16 @@ impl CodecParser for DtsParser {
return Vec::new();
}
let pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0);
vec![Frame { pts_ns, keyframe: true, data: pes.data.clone() }]
vec![Frame {
pts_ns,
keyframe: true,
data: pes.data.clone(),
}]
}
fn codec_private(&self) -> Option<Vec<u8>> { None }
fn codec_private(&self) -> Option<Vec<u8>> {
None
}
}
#[cfg(test)]
@@ -31,7 +45,12 @@ mod tests {
use crate::mux::ts::PesPacket;
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
PesPacket { pid: 0x1100, pts, dts: None, data }
PesPacket {
pid: 0x1100,
pts,
dts: None,
data,
}
}
#[test]
+45 -12
View File
@@ -4,7 +4,7 @@
//! Detects keyframes (IDR slices).
//! Each PES packet = one access unit = one frame.
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
use super::{pts_to_ns, CodecParser, Frame, PesPacket};
/// H.264 NAL unit types we care about.
const NAL_SLICE_IDR: u8 = 5;
@@ -17,9 +17,18 @@ pub struct H264Parser {
pps: Option<Vec<u8>>,
}
impl Default for H264Parser {
fn default() -> Self {
Self::new()
}
}
impl H264Parser {
pub fn new() -> Self {
Self { sps: None, pps: None }
Self {
sps: None,
pps: None,
}
}
}
@@ -84,6 +93,10 @@ impl CodecParser for H264Parser {
let sps = self.sps.as_ref()?;
let pps = self.pps.as_ref()?;
if sps.len() < 4 {
return None;
}
// AVCDecoderConfigurationRecord (ISO 14496-15):
// configurationVersion = 1
// AVCProfileIndication = SPS[1]
@@ -196,7 +209,12 @@ mod tests {
use crate::mux::ts::PesPacket;
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
PesPacket { pid: 0x1011, pts, dts: None, data }
PesPacket {
pid: 0x1011,
pts,
dts: None,
data,
}
}
// --- find_start_code tests ---
@@ -246,7 +264,7 @@ mod tests {
data.extend_from_slice(&[0x00, 0x00, 0x01]);
data.push(0x67); // SPS
data.extend_from_slice(&[0x42, 0x00, 0x1E, 0xAB, 0xCD]); // profile=0x42, compat=0x00, level=0x1E
// PPS: 00 00 01 [68 <payload>]
// PPS: 00 00 01 [68 <payload>]
data.extend_from_slice(&[0x00, 0x00, 0x01]);
data.push(0x68); // PPS
data.extend_from_slice(&[0xCE, 0x01]);
@@ -260,7 +278,10 @@ mod tests {
// codec_private should now be available
let cp = parser.codec_private();
assert!(cp.is_some(), "codec_private should be Some after seeing SPS+PPS");
assert!(
cp.is_some(),
"codec_private should be Some after seeing SPS+PPS"
);
let cp = cp.unwrap();
// AVCDecoderConfigurationRecord checks
@@ -297,7 +318,10 @@ mod tests {
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert!(frames[0].keyframe, "IDR slice should be detected as keyframe");
assert!(
frames[0].keyframe,
"IDR slice should be detected as keyframe"
);
}
// --- non-IDR → not keyframe ---
@@ -338,16 +362,25 @@ mod tests {
let frame_data = &frames[0].data;
// Should start with 4-byte big-endian length prefix
assert!(frame_data.len() >= 4, "frame data should have length prefix");
let length = u32::from_be_bytes([frame_data[0], frame_data[1], frame_data[2], frame_data[3]]);
assert_eq!(length as usize, nal_payload.len(), "length prefix should match NAL size");
assert!(
frame_data.len() >= 4,
"frame data should have length prefix"
);
let length =
u32::from_be_bytes([frame_data[0], frame_data[1], frame_data[2], frame_data[3]]);
assert_eq!(
length as usize,
nal_payload.len(),
"length prefix should match NAL size"
);
// Followed by the NAL data itself
assert_eq!(&frame_data[4..], &nal_payload);
// No start code (00 00 01) should appear in the output
for i in 0..frame_data.len().saturating_sub(2) {
let is_sc = frame_data[i] == 0x00 && frame_data[i + 1] == 0x00 && frame_data[i + 2] == 0x01;
let is_sc =
frame_data[i] == 0x00 && frame_data[i + 1] == 0x00 && frame_data[i + 2] == 0x01;
assert!(!is_sc, "output should not contain Annex B start codes");
}
}
@@ -429,8 +462,8 @@ mod tests {
let pes = PesPacket {
pid: 0x1011,
pts: Some(180000), // 2 seconds
dts: Some(90000), // 1 second
pts: Some(180000), // 2 seconds
dts: Some(90000), // 1 second
data,
};
let frames = parser.parse(&pes);
+62 -19
View File
@@ -4,8 +4,8 @@
//! Detects keyframes (IRAP pictures: IDR, CRA, BLA).
//! Each PES packet = one access unit = one frame.
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
use super::h264::{find_start_code, skip_start_code};
use super::{pts_to_ns, CodecParser, Frame, PesPacket};
// HEVC NAL unit types
const NAL_VPS: u8 = 32;
@@ -22,9 +22,19 @@ pub struct HevcParser {
pps: Option<Vec<u8>>,
}
impl Default for HevcParser {
fn default() -> Self {
Self::new()
}
}
impl HevcParser {
pub fn new() -> Self {
Self { vps: None, sps: None, pps: None }
Self {
vps: None,
sps: None,
pps: None,
}
}
}
@@ -45,7 +55,9 @@ impl CodecParser for HevcParser {
if let Some(nal_start) = skip_start_code(data, sc_pos) {
let next = find_start_code(data, nal_start).unwrap_or(data.len());
let mut end = next;
while end > nal_start && data[end - 1] == 0x00 { end -= 1; }
while end > nal_start && data[end - 1] == 0x00 {
end -= 1;
}
if nal_start < data.len() {
// HEVC NAL header: 2 bytes. Type is bits 1-6 of first byte.
@@ -55,7 +67,7 @@ impl CodecParser for HevcParser {
NAL_VPS => self.vps = Some(data[nal_start..end].to_vec()),
NAL_SPS => self.sps = Some(data[nal_start..end].to_vec()),
NAL_PPS => self.pps = Some(data[nal_start..end].to_vec()),
t if t >= NAL_BLA_W_LP && t <= NAL_RSV_IRAP_VCL23 => {
t if (NAL_BLA_W_LP..=NAL_RSV_IRAP_VCL23).contains(&t) => {
keyframe = true;
}
_ => {}
@@ -75,12 +87,18 @@ impl CodecParser for HevcParser {
if let Some(nal_start) = skip_start_code(&pes.data, sc_pos) {
let next = find_start_code(&pes.data, nal_start).unwrap_or(pes.data.len());
let mut end = next;
while end > nal_start && pes.data[end - 1] == 0x00 { end -= 1; }
while end > nal_start && pes.data[end - 1] == 0x00 {
end -= 1;
}
if nal_start < pes.data.len() {
let nal_type = (pes.data[nal_start] >> 1) & 0x3F;
// Skip parameter sets and AUD
if nal_type != NAL_VPS && nal_type != NAL_SPS && nal_type != NAL_PPS && nal_type != NAL_AUD {
if nal_type != NAL_VPS
&& nal_type != NAL_SPS
&& nal_type != NAL_PPS
&& nal_type != NAL_AUD
{
let nal = &pes.data[nal_start..end];
let len = nal.len() as u32;
frame_data.extend_from_slice(&len.to_be_bytes());
@@ -115,8 +133,8 @@ impl CodecParser for HevcParser {
let mut record = Vec::new();
// Minimal HEVCDecoderConfigurationRecord header
record.push(1); // configurationVersion
// General profile space, tier flag, profile IDC from SPS
record.push(1); // configurationVersion
// General profile space, tier flag, profile IDC from SPS
if sps.len() > 3 {
record.push(sps[1]); // general_profile_space + general_tier_flag + general_profile_idc
} else {
@@ -134,7 +152,7 @@ impl CodecParser for HevcParser {
record.push(0xFC);
// chromaFormat (6 + 2 bits)
record.push(0xFC | 1); // 4:2:0
// bitDepthLumaMinus8 (5 + 3 bits)
// bitDepthLumaMinus8 (5 + 3 bits)
record.push(0xF8);
// bitDepthChromaMinus8 (5 + 3 bits)
record.push(0xF8);
@@ -142,7 +160,7 @@ impl CodecParser for HevcParser {
record.extend_from_slice(&[0, 0]);
// constantFrameRate + numTemporalLayers + temporalIdNested + lengthSizeMinusOne
record.push(0x03); // lengthSizeMinusOne = 3 (4 bytes)
// numOfArrays
// numOfArrays
record.push(3); // VPS, SPS, PPS
// VPS array
@@ -176,7 +194,12 @@ mod tests {
use crate::mux::ts::PesPacket;
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
PesPacket { pid: 0x1011, pts, dts: None, data }
PesPacket {
pid: 0x1011,
pts,
dts: None,
data,
}
}
/// Build an HEVC NAL header (2 bytes). Type is bits 1-6 of first byte.
@@ -202,8 +225,9 @@ mod tests {
data.extend_from_slice(&[0x00, 0x00, 0x01]);
let sps_hdr = hevc_nal_header(33);
data.extend_from_slice(&sps_hdr);
data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0A, 0x0B, 0x0C, 0x0D]); // SPS payload (>12 bytes for level)
data.extend_from_slice(&[
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
]); // SPS payload (>12 bytes for level)
// PPS (type 34)
data.extend_from_slice(&[0x00, 0x00, 0x01]);
@@ -221,7 +245,10 @@ mod tests {
let _frames = parser.parse(&pes);
let cp = parser.codec_private();
assert!(cp.is_some(), "codec_private should be Some after VPS+SPS+PPS");
assert!(
cp.is_some(),
"codec_private should be Some after VPS+SPS+PPS"
);
let cp = cp.unwrap();
// configurationVersion = 1
@@ -229,7 +256,10 @@ mod tests {
// numOfArrays = 3 (VPS, SPS, PPS)
assert_eq!(cp[22], 3);
// Should be longer than the minimal header (23 bytes) + array entries
assert!(cp.len() > 23, "codec_private should contain VPS+SPS+PPS data");
assert!(
cp.len() > 23,
"codec_private should contain VPS+SPS+PPS data"
);
}
#[test]
@@ -257,7 +287,10 @@ mod tests {
let pes = make_pes(data, Some(0));
parser.parse(&pes);
assert!(parser.codec_private().is_none(), "should be None without PPS");
assert!(
parser.codec_private().is_none(),
"should be None without PPS"
);
}
// --- IRAP keyframe detection ---
@@ -276,7 +309,10 @@ mod tests {
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert!(frames[0].keyframe, "IDR_W_RADL (type 19) should be keyframe");
assert!(
frames[0].keyframe,
"IDR_W_RADL (type 19) should be keyframe"
);
}
#[test]
@@ -343,7 +379,10 @@ mod tests {
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert!(!frames[0].keyframe, "TRAIL_R (type 1) should not be keyframe");
assert!(
!frames[0].keyframe,
"TRAIL_R (type 1) should not be keyframe"
);
}
#[test]
@@ -395,7 +434,11 @@ mod tests {
let fd = &frames[0].data;
let length = u32::from_be_bytes([fd[0], fd[1], fd[2], fd[3]]);
// IDR NAL = 2 bytes header + 2 bytes payload = 4 bytes
assert_eq!(length as usize + 4, fd.len(), "frame should contain exactly one length-prefixed NAL");
assert_eq!(
length as usize + 4,
fd.len(),
"frame should contain exactly one length-prefixed NAL"
);
}
// --- empty PES ---
+9 -5
View File
@@ -8,15 +8,16 @@
//! - Convert PTS from 90kHz to nanoseconds
pub mod ac3;
pub mod dts;
pub mod h264;
pub mod hevc;
pub mod vc1;
pub mod dts;
pub mod truehd;
pub mod mpeg2;
pub mod pgs;
pub mod truehd;
pub mod vc1;
use crate::disc::Codec;
use super::ts::PesPacket;
use crate::disc::Codec;
/// A single frame ready for MKV muxing.
pub struct Frame {
@@ -53,7 +54,9 @@ pub struct PassthroughParser {
impl PassthroughParser {
pub fn new(always_keyframe: bool) -> Self {
Self { keyframe: always_keyframe }
Self {
keyframe: always_keyframe,
}
}
}
@@ -77,6 +80,7 @@ pub fn parser_for_codec(codec: Codec) -> Box<dyn CodecParser> {
match codec {
Codec::H264 => Box::new(h264::H264Parser::new()),
Codec::Hevc => Box::new(hevc::HevcParser::new()),
Codec::Mpeg2 => Box::new(mpeg2::Mpeg2Parser::new()),
Codec::Vc1 => Box::new(vc1::Vc1Parser::new()),
Codec::Ac3 | Codec::Ac3Plus => Box::new(ac3::Ac3Parser::new()),
Codec::DtsHdMa | Codec::DtsHdHr | Codec::Dts => Box::new(dts::DtsParser::new()),
+448
View File
@@ -0,0 +1,448 @@
//! 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::{pts_to_ns, CodecParser, Frame};
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>>,
}
impl Default for Mpeg2Parser {
fn default() -> Self {
Self::new()
}
}
impl Mpeg2Parser {
pub fn new() -> Self {
Self { seq_header: None }
}
/// 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();
}
let pts_ns = pes.dts.or(pes.pts).map(pts_to_ns).unwrap_or(0);
let data = &pes.data;
let mut keyframe = 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 => {
// Capture sequence header: from start code to next start code
// (or to the sequence extension if present).
let hdr_start = sc;
let hdr_end = find_start_code(data, sc + 4).unwrap_or(data.len());
let mut seq_data = data[hdr_start..hdr_end].to_vec();
// Check if sequence extension follows immediately.
if hdr_end + 3 < data.len() && data[hdr_end + 3] == SEQ_EXT_CODE {
let ext_end =
find_start_code(data, hdr_end + 4).unwrap_or(data.len());
seq_data.extend_from_slice(&data[hdr_end..ext_end]);
}
self.seq_header = Some(seq_data);
// Sequence header implies I-frame follows.
keyframe = true;
pos = sc + 4;
}
PICTURE_CODE => {
// Picture header: bytes after start code contain temporal_reference
// (10 bits) + picture_coding_type (3 bits).
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;
}
}
}
vec![Frame {
pts_ns,
keyframe,
data: pes.data.clone(),
}]
}
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])
}
/// 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;
}
for i in from..data.len() - 2 {
if data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x01 {
return Some(i);
}
}
None
}
#[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");
}
// --- I-frame with sequence header = keyframe ---
#[test]
fn sequence_header_implies_keyframe() {
let mut parser = Mpeg2Parser::new();
let mut data = Vec::new();
data.extend_from_slice(&make_seq_header(720, 480, 3, 4));
// Even without an explicit picture header, a sequence header implies I-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);
}
// --- 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
}
}
+24 -5
View File
@@ -4,12 +4,20 @@
//! Each PES packet contains one or more segments.
//! All segments are keyframes (no inter-segment dependencies).
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
use super::{pts_to_ns, CodecParser, Frame, PesPacket};
pub struct PgsParser;
impl Default for PgsParser {
fn default() -> Self {
Self::new()
}
}
impl PgsParser {
pub fn new() -> Self { Self }
pub fn new() -> Self {
Self
}
}
impl CodecParser for PgsParser {
@@ -18,10 +26,16 @@ impl CodecParser for PgsParser {
return Vec::new();
}
let pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0);
vec![Frame { pts_ns, keyframe: true, data: pes.data.clone() }]
vec![Frame {
pts_ns,
keyframe: true,
data: pes.data.clone(),
}]
}
fn codec_private(&self) -> Option<Vec<u8>> { None }
fn codec_private(&self) -> Option<Vec<u8>> {
None
}
}
#[cfg(test)]
@@ -30,7 +44,12 @@ mod tests {
use crate::mux::ts::PesPacket;
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
PesPacket { pid: 0x1200, pts, dts: None, data }
PesPacket {
pid: 0x1200,
pts,
dts: None,
data,
}
}
#[test]
+24 -5
View File
@@ -6,12 +6,20 @@
//! All access units are keyframes.
//! Each PES packet = one access unit.
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
use super::{pts_to_ns, CodecParser, Frame, PesPacket};
pub struct TrueHdParser;
impl Default for TrueHdParser {
fn default() -> Self {
Self::new()
}
}
impl TrueHdParser {
pub fn new() -> Self { Self }
pub fn new() -> Self {
Self
}
}
impl CodecParser for TrueHdParser {
@@ -20,10 +28,16 @@ impl CodecParser for TrueHdParser {
return Vec::new();
}
let pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0);
vec![Frame { pts_ns, keyframe: true, data: pes.data.clone() }]
vec![Frame {
pts_ns,
keyframe: true,
data: pes.data.clone(),
}]
}
fn codec_private(&self) -> Option<Vec<u8>> { None }
fn codec_private(&self) -> Option<Vec<u8>> {
None
}
}
#[cfg(test)]
@@ -32,7 +46,12 @@ mod tests {
use crate::mux::ts::PesPacket;
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
PesPacket { pid: 0x1100, pts, dts: None, data }
PesPacket {
pid: 0x1100,
pts,
dts: None,
data,
}
}
#[test]
+57 -21
View File
@@ -5,7 +5,7 @@
//! Frame start = Frame header start code (0x0D).
//! I-frames (keyframes) are identified from the frame header.
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
use super::{pts_to_ns, CodecParser, Frame, PesPacket};
const SC_SEQUENCE_HEADER: u8 = 0x0F;
const SC_ENTRY_POINT: u8 = 0x0E;
@@ -16,9 +16,18 @@ pub struct Vc1Parser {
entry_point: Option<Vec<u8>>,
}
impl Default for Vc1Parser {
fn default() -> Self {
Self::new()
}
}
impl Vc1Parser {
pub fn new() -> Self {
Self { seq_header: None, entry_point: None }
Self {
seq_header: None,
entry_point: None,
}
}
}
@@ -92,17 +101,17 @@ impl CodecParser for Vc1Parser {
let mut cp = Vec::with_capacity(header_size as usize);
// BITMAPINFOHEADER (40 bytes, little-endian)
cp.extend_from_slice(&header_size.to_le_bytes()); // biSize
cp.extend_from_slice(&1920u32.to_le_bytes()); // biWidth (updated by player)
cp.extend_from_slice(&1080u32.to_le_bytes()); // biHeight
cp.extend_from_slice(&1u16.to_le_bytes()); // biPlanes
cp.extend_from_slice(&24u16.to_le_bytes()); // biBitCount
cp.extend_from_slice(b"WVC1"); // biCompression = "WVC1" FOURCC
cp.extend_from_slice(&0u32.to_le_bytes()); // biSizeImage
cp.extend_from_slice(&0u32.to_le_bytes()); // biXPelsPerMeter
cp.extend_from_slice(&0u32.to_le_bytes()); // biYPelsPerMeter
cp.extend_from_slice(&0u32.to_le_bytes()); // biClrUsed
cp.extend_from_slice(&0u32.to_le_bytes()); // biClrImportant
cp.extend_from_slice(&header_size.to_le_bytes()); // biSize
cp.extend_from_slice(&1920u32.to_le_bytes()); // biWidth (updated by player)
cp.extend_from_slice(&1080u32.to_le_bytes()); // biHeight
cp.extend_from_slice(&1u16.to_le_bytes()); // biPlanes
cp.extend_from_slice(&24u16.to_le_bytes()); // biBitCount
cp.extend_from_slice(b"WVC1"); // biCompression = "WVC1" FOURCC
cp.extend_from_slice(&0u32.to_le_bytes()); // biSizeImage
cp.extend_from_slice(&0u32.to_le_bytes()); // biXPelsPerMeter
cp.extend_from_slice(&0u32.to_le_bytes()); // biYPelsPerMeter
cp.extend_from_slice(&0u32.to_le_bytes()); // biClrUsed
cp.extend_from_slice(&0u32.to_le_bytes()); // biClrImportant
// Extra codec data: sequence header + entry point (Annex B)
cp.extend_from_slice(sh);
@@ -127,7 +136,12 @@ mod tests {
use crate::mux::ts::PesPacket;
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
PesPacket { pid: 0x1011, pts, dts: None, data }
PesPacket {
pid: 0x1011,
pts,
dts: None,
data,
}
}
/// Build a VC-1 PES with sequence header + entry point + frame start code.
@@ -157,7 +171,10 @@ mod tests {
assert_eq!(frames.len(), 1);
// Sequence header present → keyframe
assert!(frames[0].keyframe, "PES with sequence header should be keyframe");
assert!(
frames[0].keyframe,
"PES with sequence header should be keyframe"
);
// seq_header should be stored internally
assert!(parser.seq_header.is_some());
}
@@ -184,15 +201,25 @@ mod tests {
parser.parse(&pes);
let cp = parser.codec_private();
assert!(cp.is_some(), "codec_private should be Some after seq header + entry point");
assert!(
cp.is_some(),
"codec_private should be Some after seq header + entry point"
);
let cp = cp.unwrap();
// BITMAPINFOHEADER is 40 bytes + extra data
assert!(cp.len() >= 40, "codec_private should be at least 40 bytes (BITMAPINFOHEADER)");
assert!(
cp.len() >= 40,
"codec_private should be at least 40 bytes (BITMAPINFOHEADER)"
);
// biSize (first 4 bytes, little-endian) should equal total length
let bi_size = u32::from_le_bytes([cp[0], cp[1], cp[2], cp[3]]);
assert_eq!(bi_size as usize, cp.len(), "biSize should match total codec_private length");
assert_eq!(
bi_size as usize,
cp.len(),
"biSize should match total codec_private length"
);
// biCompression = "WVC1" at offset 16
assert_eq!(&cp[16..20], b"WVC1", "FOURCC should be WVC1");
@@ -226,7 +253,10 @@ mod tests {
let pes = make_pes(data, Some(0));
parser.parse(&pes);
assert!(parser.codec_private().is_none(), "should be None without entry point");
assert!(
parser.codec_private().is_none(),
"should be None without entry point"
);
}
// --- frame without sequence header → not keyframe ---
@@ -244,7 +274,10 @@ mod tests {
let frames = parser.parse(&pes);
assert_eq!(frames.len(), 1);
assert!(!frames[0].keyframe, "frame without sequence header should not be keyframe");
assert!(
!frames[0].keyframe,
"frame without sequence header should not be keyframe"
);
}
// --- frame data starts from frame start code ---
@@ -337,7 +370,10 @@ mod tests {
let cp = parser.codec_private().unwrap();
// After the 40-byte BITMAPINFOHEADER, we should have seq_header + entry_point data
let extra = &cp[40..];
assert!(!extra.is_empty(), "extra data after BITMAPINFOHEADER should not be empty");
assert!(
!extra.is_empty(),
"extra data after BITMAPINFOHEADER should not be empty"
);
// Extra data should start with the sequence header start code
assert_eq!(&extra[0..4], &[0x00, 0x00, 0x01, SC_SEQUENCE_HEADER]);
}