100% codec coverage + disc/ and aacs/ module refactors
Codec coverage (DVD + BD + UHD): - E-AC-3 (Dolby Digital Plus): bsid detection, frame size calc — 8 tests - DTS-HD MA/HR: extension substream (0x64582025) detection — 8 tests - LPCM: BD header skip, raw PCM extraction — 6 tests - DVD VobSub subtitles: passthrough parser — 5 tests - Dolby Vision: verified RPU NAL type 62 preserved in HEVC — 1 test Module refactors: - disc.rs → disc/mod.rs + bluray.rs + dvd.rs + encrypt.rs - aacs/mod.rs (1661 lines) → mod.rs (21) + keydb.rs + keys.rs + decrypt.rs - All public APIs preserved, all tests pass 270 tests total, 0 failures.
This commit is contained in:
+197
-18
@@ -3,6 +3,9 @@
|
||||
//! AC3 frames are self-contained and always start with syncword 0x0B77.
|
||||
//! Each PES packet typically contains exactly one AC3 frame.
|
||||
//! All AC3 frames are effectively keyframes (no inter-frame dependencies).
|
||||
//!
|
||||
//! E-AC-3 shares the same syncword but uses bsid >= 11 (typically 16).
|
||||
//! Frame size is derived from the frmsiz field instead of fscod/frmsizecod.
|
||||
|
||||
use super::{pts_to_ns, CodecParser, Frame, PesPacket};
|
||||
|
||||
@@ -28,15 +31,67 @@ impl CodecParser for Ac3Parser {
|
||||
|
||||
let pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0);
|
||||
|
||||
// Find AC3 syncword (0x0B77) — skip any garbage before it
|
||||
let data = &pes.data;
|
||||
let start = find_ac3_sync(data).unwrap_or(0);
|
||||
let mut frames = Vec::new();
|
||||
let mut pos = 0;
|
||||
|
||||
vec![Frame {
|
||||
pts_ns,
|
||||
keyframe: true,
|
||||
data: data[start..].to_vec(),
|
||||
}]
|
||||
while pos < data.len() {
|
||||
let sync = find_ac3_sync(&data[pos..]);
|
||||
let start = match sync {
|
||||
Some(offset) => pos + offset,
|
||||
None => break,
|
||||
};
|
||||
|
||||
let remaining = &data[start..];
|
||||
|
||||
// Need at least 6 bytes to inspect bsid / frame size fields
|
||||
if remaining.len() < 6 {
|
||||
// Emit whatever remains as a single frame
|
||||
frames.push(Frame {
|
||||
pts_ns,
|
||||
keyframe: true,
|
||||
data: remaining.to_vec(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
let bsid = get_bsid(remaining);
|
||||
|
||||
if bsid >= 11 {
|
||||
// E-AC-3 frame size from frmsiz field (bytes 2-3)
|
||||
let frame_size = eac3_frame_size(remaining);
|
||||
|
||||
let end = start + frame_size.min(data.len() - start);
|
||||
frames.push(Frame {
|
||||
pts_ns,
|
||||
keyframe: true,
|
||||
data: data[start..end].to_vec(),
|
||||
});
|
||||
pos = end;
|
||||
} else {
|
||||
// AC-3: emit everything from syncword to next syncword (or end)
|
||||
let next_sync = find_ac3_sync(&data[start + 2..]).map(|o| start + 2 + o);
|
||||
let end = next_sync.unwrap_or(data.len());
|
||||
frames.push(Frame {
|
||||
pts_ns,
|
||||
keyframe: true,
|
||||
data: data[start..end].to_vec(),
|
||||
});
|
||||
pos = end;
|
||||
}
|
||||
}
|
||||
|
||||
// If we found no syncword at all, emit the whole PES as a frame
|
||||
// (backwards-compatible with old behaviour).
|
||||
if frames.is_empty() {
|
||||
frames.push(Frame {
|
||||
pts_ns,
|
||||
keyframe: true,
|
||||
data: data.to_vec(),
|
||||
});
|
||||
}
|
||||
|
||||
frames
|
||||
}
|
||||
|
||||
fn codec_private(&self) -> Option<Vec<u8>> {
|
||||
@@ -44,7 +99,7 @@ impl CodecParser for Ac3Parser {
|
||||
}
|
||||
}
|
||||
|
||||
/// Find AC3 syncword (0x0B77) in data.
|
||||
/// Find AC3/E-AC-3 syncword (0x0B77) in data.
|
||||
fn find_ac3_sync(data: &[u8]) -> Option<usize> {
|
||||
for i in 0..data.len().saturating_sub(1) {
|
||||
if data[i] == 0x0B && data[i + 1] == 0x77 {
|
||||
@@ -54,6 +109,23 @@ fn find_ac3_sync(data: &[u8]) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Extract bsid from an AC-3/E-AC-3 frame starting at the syncword.
|
||||
/// bsid is at byte 5, bits 7..3.
|
||||
/// AC-3: bsid <= 10, E-AC-3: bsid >= 11 (typically 16).
|
||||
pub fn get_bsid(data: &[u8]) -> u8 {
|
||||
debug_assert!(data.len() >= 6);
|
||||
(data[5] >> 3) & 0x1F
|
||||
}
|
||||
|
||||
/// Calculate E-AC-3 frame size in bytes from the frmsiz field.
|
||||
/// frmsiz is at bits [2:0] of byte 2 concatenated with byte 3.
|
||||
/// Frame size = (frmsiz + 1) * 2 bytes.
|
||||
pub fn eac3_frame_size(data: &[u8]) -> usize {
|
||||
debug_assert!(data.len() >= 4);
|
||||
let frmsiz = ((data[2] as usize & 0x07) << 8) | (data[3] as usize);
|
||||
(frmsiz + 1) * 2
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -68,6 +140,29 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a minimal AC-3 header (bsid <= 10).
|
||||
fn make_ac3_header(bsid: u8) -> Vec<u8> {
|
||||
// 0x0B 0x77 <byte2> <byte3> <byte4> <byte5=bsid>
|
||||
let byte5 = (bsid & 0x1F) << 3;
|
||||
vec![0x0B, 0x77, 0x00, 0x00, 0x00, byte5, 0xAA, 0xBB]
|
||||
}
|
||||
|
||||
/// Build a minimal E-AC-3 header with the given bsid and frmsiz.
|
||||
/// frmsiz encodes frame size: frame_bytes = (frmsiz + 1) * 2.
|
||||
fn make_eac3_header(bsid: u8, frmsiz: u16, payload_fill: u8) -> Vec<u8> {
|
||||
let byte2 = (frmsiz >> 8) as u8 & 0x07;
|
||||
let byte3 = (frmsiz & 0xFF) as u8;
|
||||
let byte5 = (bsid & 0x1F) << 3;
|
||||
let frame_size = (frmsiz as usize + 1) * 2;
|
||||
let mut data = vec![0x0B, 0x77, byte2, byte3, 0x00, byte5];
|
||||
// Pad to full frame size
|
||||
while data.len() < frame_size {
|
||||
data.push(payload_fill);
|
||||
}
|
||||
data.truncate(frame_size);
|
||||
data
|
||||
}
|
||||
|
||||
// --- syncword detection ---
|
||||
|
||||
#[test]
|
||||
@@ -94,14 +189,98 @@ mod tests {
|
||||
assert_eq!(find_ac3_sync(&data), None);
|
||||
}
|
||||
|
||||
// --- parse syncword → frame extracted ---
|
||||
// --- bsid detection ---
|
||||
|
||||
#[test]
|
||||
fn bsid_ac3() {
|
||||
let header = make_ac3_header(8);
|
||||
assert_eq!(get_bsid(&header), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bsid_eac3() {
|
||||
let header = make_eac3_header(16, 99, 0x00);
|
||||
assert_eq!(get_bsid(&header), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bsid_boundary_10() {
|
||||
let header = make_ac3_header(10);
|
||||
assert_eq!(get_bsid(&header), 10);
|
||||
// bsid 10 should be treated as AC-3 (<= 10)
|
||||
assert!(get_bsid(&header) <= 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bsid_boundary_11() {
|
||||
let header = make_eac3_header(11, 3, 0x00);
|
||||
assert_eq!(get_bsid(&header), 11);
|
||||
// bsid 11 should be treated as E-AC-3 (>= 11)
|
||||
assert!(get_bsid(&header) >= 11);
|
||||
}
|
||||
|
||||
// --- E-AC-3 frame size calculation ---
|
||||
|
||||
#[test]
|
||||
fn eac3_frame_size_basic() {
|
||||
// frmsiz = 99 → frame_size = (99+1)*2 = 200 bytes
|
||||
let header = make_eac3_header(16, 99, 0xDD);
|
||||
assert_eq!(eac3_frame_size(&header), 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eac3_frame_size_min() {
|
||||
// frmsiz = 0 → frame_size = (0+1)*2 = 2 bytes
|
||||
let data = [0x0B, 0x77, 0x00, 0x00, 0x00, 0x80];
|
||||
assert_eq!(eac3_frame_size(&data), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eac3_frame_size_large() {
|
||||
// frmsiz = 0x7FF (max 11-bit) → (2047+1)*2 = 4096
|
||||
let data = [0x0B, 0x77, 0x07, 0xFF, 0x00, 0x80];
|
||||
assert_eq!(eac3_frame_size(&data), 4096);
|
||||
}
|
||||
|
||||
// --- parse: E-AC-3 frame extraction ---
|
||||
|
||||
#[test]
|
||||
fn parse_eac3_single_frame() {
|
||||
let mut parser = Ac3Parser::new();
|
||||
// frmsiz = 9 → frame_size = 20 bytes
|
||||
let data = make_eac3_header(16, 9, 0xCC);
|
||||
assert_eq!(data.len(), 20);
|
||||
let pes = make_pes(data.clone(), Some(90000));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].data.len(), 20);
|
||||
assert_eq!(frames[0].pts_ns, 1_000_000_000);
|
||||
assert!(frames[0].keyframe);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_eac3_frame_with_garbage_prefix() {
|
||||
let mut parser = Ac3Parser::new();
|
||||
let mut data = vec![0xFF, 0xFE]; // garbage
|
||||
data.extend_from_slice(&make_eac3_header(16, 4, 0xAA)); // frmsiz=4 → 10 bytes
|
||||
let pes = make_pes(data, Some(0));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].data[0], 0x0B);
|
||||
assert_eq!(frames[0].data[1], 0x77);
|
||||
assert_eq!(frames[0].data.len(), 10);
|
||||
}
|
||||
|
||||
// --- parse syncword → frame extracted (AC-3) ---
|
||||
|
||||
#[test]
|
||||
fn parse_syncword() {
|
||||
let mut parser = Ac3Parser::new();
|
||||
|
||||
// AC3 frame starting with syncword
|
||||
let data = vec![0x0B, 0x77, 0x44, 0x55, 0x66, 0x77, 0x88];
|
||||
// AC3 frame starting with syncword (bsid=8)
|
||||
let data = make_ac3_header(8);
|
||||
let pes = make_pes(data.clone(), Some(90000));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
@@ -115,15 +294,14 @@ mod tests {
|
||||
let mut parser = Ac3Parser::new();
|
||||
|
||||
// Garbage bytes before syncword
|
||||
let data = vec![0xFF, 0xFE, 0x0B, 0x77, 0x44, 0x55];
|
||||
let mut data = vec![0xFF, 0xFE];
|
||||
data.extend_from_slice(&make_ac3_header(8));
|
||||
let pes = make_pes(data, Some(0));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
// Data should start from the syncword
|
||||
assert_eq!(frames[0].data[0], 0x0B);
|
||||
assert_eq!(frames[0].data[1], 0x77);
|
||||
assert_eq!(frames[0].data.len(), 4); // syncword + 2 payload bytes
|
||||
}
|
||||
|
||||
// --- all frames are keyframes ---
|
||||
@@ -132,8 +310,9 @@ mod tests {
|
||||
fn all_keyframes() {
|
||||
let mut parser = Ac3Parser::new();
|
||||
|
||||
for i in 0..5 {
|
||||
let data = vec![0x0B, 0x77, 0x00, i];
|
||||
for i in 0..5u8 {
|
||||
let mut data = make_ac3_header(8);
|
||||
data.push(i);
|
||||
let pes = make_pes(data, Some(90000 * i as i64));
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
@@ -172,7 +351,7 @@ mod tests {
|
||||
#[test]
|
||||
fn pts_conversion() {
|
||||
let mut parser = Ac3Parser::new();
|
||||
let data = vec![0x0B, 0x77, 0x00, 0x01];
|
||||
let data = make_ac3_header(8);
|
||||
// 45000 ticks = 0.5 seconds → 500_000_000 ns
|
||||
let pes = make_pes(data, Some(45000));
|
||||
let frames = parser.parse(&pes);
|
||||
@@ -185,7 +364,7 @@ mod tests {
|
||||
#[test]
|
||||
fn no_pts() {
|
||||
let mut parser = Ac3Parser::new();
|
||||
let data = vec![0x0B, 0x77, 0x00, 0x01];
|
||||
let data = make_ac3_header(8);
|
||||
let pes = make_pes(data, None);
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
|
||||
Reference in New Issue
Block a user