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:
MattJackson
2026-04-11 17:22:18 +00:00
parent fe723a7759
commit 515f2f6bc2
14 changed files with 2414 additions and 2097 deletions
+197 -18
View File
@@ -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);