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:
+13
-2
@@ -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
@@ -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
@@ -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
@@ -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 ---
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user