Fix all v4 audit findings (22 items)
HIGH: ISO writer multi-extent for >4GB, end-to-end MKV mux test MEDIUM: AACS cvalue bounds, UV offset, macOS discovery, VC-1 resolution from sequence header, HEVC profile flags from SPS, ISO CRC + reserve AVDP LOW: PS AC3 sub-header, CSS crack first-match break, TrackUID unique, AC3 no-sync empty return, --all for iso://, --min warning, dead code removed 320 tests, all passing.
This commit is contained in:
@@ -327,6 +327,372 @@ fn aacs_decrypt_unit_unencrypted_passthrough() {
|
||||
assert_eq!(unit, original, "unencrypted unit should be unchanged");
|
||||
}
|
||||
|
||||
// ── AACS cross-validation with independent AES implementation ──────────────
|
||||
|
||||
/// Independent AES-128-ECB encrypt (uses `aes` crate directly, NOT our library).
|
||||
fn ref_aes_ecb_encrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
|
||||
use aes::cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit};
|
||||
use aes::Aes128;
|
||||
let cipher = Aes128::new(GenericArray::from_slice(key));
|
||||
let mut block = GenericArray::clone_from_slice(data);
|
||||
cipher.encrypt_block(&mut block);
|
||||
let mut out = [0u8; 16];
|
||||
out.copy_from_slice(&block);
|
||||
out
|
||||
}
|
||||
|
||||
/// Independent AES-128-CBC encrypt (uses `aes` crate directly, NOT our library).
|
||||
fn ref_aes_cbc_encrypt(key: &[u8; 16], iv: &[u8; 16], data: &mut [u8]) {
|
||||
use aes::cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit};
|
||||
use aes::Aes128;
|
||||
let cipher = Aes128::new(GenericArray::from_slice(key));
|
||||
let mut prev = *iv;
|
||||
let num_blocks = data.len() / 16;
|
||||
for i in 0..num_blocks {
|
||||
let off = i * 16;
|
||||
for j in 0..16 {
|
||||
data[off + j] ^= prev[j];
|
||||
}
|
||||
let mut block = GenericArray::clone_from_slice(&data[off..off + 16]);
|
||||
cipher.encrypt_block(&mut block);
|
||||
data[off..off + 16].copy_from_slice(&block);
|
||||
prev.copy_from_slice(&data[off..off + 16]);
|
||||
}
|
||||
}
|
||||
|
||||
/// The standard AACS IV, copied here independently so we are NOT importing
|
||||
/// the library's constant — this IS the cross-validation reference value.
|
||||
const CROSS_AACS_IV: [u8; 16] = [
|
||||
0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3,
|
||||
0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, 0x0F, 0x78,
|
||||
];
|
||||
|
||||
/// Build a plaintext aligned unit with TS sync markers and recognisable
|
||||
/// content, encrypt it using only the `aes` crate (independent of the
|
||||
/// library), then decrypt with `decrypt_unit()` and verify the match.
|
||||
#[test]
|
||||
fn aacs_cross_validation_encrypt_then_decrypt() {
|
||||
let unit_key: [u8; 16] = [
|
||||
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
|
||||
0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10,
|
||||
];
|
||||
|
||||
let mut plaintext = vec![0u8; aacs::ALIGNED_UNIT_LEN];
|
||||
// TS sync bytes every 192 bytes starting at offset 4
|
||||
let mut off = 4;
|
||||
while off < aacs::ALIGNED_UNIT_LEN {
|
||||
plaintext[off] = 0x47;
|
||||
off += 192;
|
||||
}
|
||||
// Fill the rest with a recognisable pattern (prime modulus avoids artefacts)
|
||||
for i in 16..aacs::ALIGNED_UNIT_LEN {
|
||||
if plaintext[i] == 0 {
|
||||
plaintext[i] = (i % 251) as u8;
|
||||
}
|
||||
}
|
||||
// Set encryption flag
|
||||
plaintext[0] = 0xC0;
|
||||
|
||||
let expected = plaintext.clone();
|
||||
|
||||
// -- Encrypt with independent implementation --
|
||||
let mut header = [0u8; 16];
|
||||
header.copy_from_slice(&plaintext[..16]);
|
||||
let derived = ref_aes_ecb_encrypt(&unit_key, &header);
|
||||
let mut dk = [0u8; 16];
|
||||
for i in 0..16 {
|
||||
dk[i] = derived[i] ^ header[i];
|
||||
}
|
||||
ref_aes_cbc_encrypt(&dk, &CROSS_AACS_IV, &mut plaintext[16..aacs::ALIGNED_UNIT_LEN]);
|
||||
|
||||
// Sanity: ciphertext should differ
|
||||
assert_ne!(
|
||||
&plaintext[16..32],
|
||||
&expected[16..32],
|
||||
"encryption did not change ciphertext region"
|
||||
);
|
||||
|
||||
// -- Decrypt with the library --
|
||||
let ok = aacs::decrypt_unit(&mut plaintext, &unit_key);
|
||||
assert!(ok, "decrypt_unit returned false (TS sync verification failed)");
|
||||
assert_eq!(plaintext[0] & 0xC0, 0x00, "encryption flag not cleared");
|
||||
|
||||
// Compare (byte 0 flag was cleared)
|
||||
let mut expected_cleared = expected.clone();
|
||||
expected_cleared[0] &= !0xC0;
|
||||
assert_eq!(
|
||||
&plaintext[1..aacs::ALIGNED_UNIT_LEN],
|
||||
&expected_cleared[1..aacs::ALIGNED_UNIT_LEN],
|
||||
"decrypted unit does not match original plaintext"
|
||||
);
|
||||
}
|
||||
|
||||
/// Same cross-validation with a different key and all-0xFF payload to
|
||||
/// exercise different AES round-key schedules.
|
||||
#[test]
|
||||
fn aacs_cross_validation_alternate_key() {
|
||||
let unit_key: [u8; 16] = [
|
||||
0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE,
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
];
|
||||
|
||||
let mut plaintext = vec![0xFFu8; aacs::ALIGNED_UNIT_LEN];
|
||||
let mut off = 4;
|
||||
while off < aacs::ALIGNED_UNIT_LEN {
|
||||
plaintext[off] = 0x47;
|
||||
off += 192;
|
||||
}
|
||||
plaintext[0] = 0xC0;
|
||||
let expected = plaintext.clone();
|
||||
|
||||
let mut header = [0u8; 16];
|
||||
header.copy_from_slice(&plaintext[..16]);
|
||||
let derived = ref_aes_ecb_encrypt(&unit_key, &header);
|
||||
let mut dk = [0u8; 16];
|
||||
for i in 0..16 {
|
||||
dk[i] = derived[i] ^ header[i];
|
||||
}
|
||||
ref_aes_cbc_encrypt(&dk, &CROSS_AACS_IV, &mut plaintext[16..aacs::ALIGNED_UNIT_LEN]);
|
||||
|
||||
assert!(aacs::decrypt_unit(&mut plaintext, &unit_key));
|
||||
|
||||
let mut expected_cleared = expected;
|
||||
expected_cleared[0] &= !0xC0;
|
||||
assert_eq!(
|
||||
&plaintext[1..aacs::ALIGNED_UNIT_LEN],
|
||||
&expected_cleared[1..aacs::ALIGNED_UNIT_LEN],
|
||||
);
|
||||
}
|
||||
|
||||
/// Verify that `decrypt_bus` correctly reverses AES-CBC encryption applied
|
||||
/// per-sector to bytes 16..2048 (bus encryption layer).
|
||||
#[test]
|
||||
fn aacs_bus_decrypt_cross_validation() {
|
||||
let read_data_key: [u8; 16] = [
|
||||
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
|
||||
0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00,
|
||||
];
|
||||
|
||||
let mut plaintext = vec![0u8; aacs::ALIGNED_UNIT_LEN];
|
||||
for i in 0..aacs::ALIGNED_UNIT_LEN {
|
||||
plaintext[i] = ((i * 3 + 17) & 0xFF) as u8;
|
||||
}
|
||||
let expected = plaintext.clone();
|
||||
|
||||
// Encrypt per-sector: AES-CBC encrypt bytes 16..2048 of each 2048-byte sector
|
||||
for sector_start in (0..aacs::ALIGNED_UNIT_LEN).step_by(2048) {
|
||||
ref_aes_cbc_encrypt(
|
||||
&read_data_key,
|
||||
&CROSS_AACS_IV,
|
||||
&mut plaintext[sector_start + 16..sector_start + 2048],
|
||||
);
|
||||
}
|
||||
assert_ne!(&plaintext[16..32], &expected[16..32]);
|
||||
|
||||
aacs::decrypt_bus(&mut plaintext, &read_data_key);
|
||||
assert_eq!(
|
||||
plaintext, expected,
|
||||
"bus decrypt did not recover original plaintext"
|
||||
);
|
||||
}
|
||||
|
||||
// ── CSS roundtrip test vectors ─────────────────────────────────────────────
|
||||
|
||||
/// CSS descramble is XOR-based: applying it twice with restored scramble
|
||||
/// flag must recover the original plaintext. This test uses a structured
|
||||
/// MPEG-2 sector and stores a snapshot of the intermediate ciphertext to
|
||||
/// catch any regressions in the cipher implementation.
|
||||
#[test]
|
||||
fn css_roundtrip_with_snapshot() {
|
||||
let title_key: [u8; 5] = [0x42, 0x13, 0x37, 0xBE, 0xEF];
|
||||
let seed: [u8; 5] = [0xDE, 0xAD, 0xBE, 0xEF, 0x42];
|
||||
|
||||
let mut sector = vec![0x00u8; 2048];
|
||||
sector[0] = 0x00;
|
||||
sector[1] = 0x00;
|
||||
sector[2] = 0x01;
|
||||
sector[3] = 0xBA;
|
||||
sector[0x14] = 0x30;
|
||||
sector[0x54..0x59].copy_from_slice(&seed);
|
||||
sector[0x80] = 0x00;
|
||||
sector[0x81] = 0x00;
|
||||
sector[0x82] = 0x01;
|
||||
sector[0x83] = 0xE0;
|
||||
sector[0x84] = 0x07;
|
||||
sector[0x85] = 0xEC;
|
||||
sector[0x86] = 0x80;
|
||||
sector[0x87] = 0x80;
|
||||
sector[0x88] = 0x05;
|
||||
sector[0x89] = 0x21;
|
||||
for i in 0x8A..2048 {
|
||||
sector[i] = ((i * 7 + 3) & 0xFF) as u8;
|
||||
}
|
||||
let original = sector.clone();
|
||||
|
||||
// First descramble = "encrypt" via XOR
|
||||
css::lfsr::descramble_sector(&title_key, &mut sector);
|
||||
|
||||
// Snapshot the first 32 bytes of the encrypted region for regression
|
||||
let snapshot: Vec<u8> = sector[0x80..0xA0].to_vec();
|
||||
assert_eq!(snapshot.len(), 32);
|
||||
assert_eq!(sector[0x14] & 0x30, 0x00, "flag not cleared");
|
||||
assert_ne!(§or[0x80..0xA0], &original[0x80..0xA0]);
|
||||
|
||||
// Restore scramble flag and roundtrip
|
||||
sector[0x14] = 0x30;
|
||||
css::lfsr::descramble_sector(&title_key, &mut sector);
|
||||
assert_eq!(
|
||||
§or[0x80..2048],
|
||||
&original[0x80..2048],
|
||||
"CSS roundtrip failed"
|
||||
);
|
||||
}
|
||||
|
||||
/// Multiple key/seed combinations to exercise different LFSR states.
|
||||
#[test]
|
||||
fn css_roundtrip_multiple_keys() {
|
||||
let cases: &[([u8; 5], [u8; 5])] = &[
|
||||
([0x00, 0x00, 0x00, 0x00, 0x00], [0x00, 0x00, 0x00, 0x00, 0x00]),
|
||||
([0xFF, 0xFF, 0xFF, 0xFF, 0xFF], [0xFF, 0xFF, 0xFF, 0xFF, 0xFF]),
|
||||
([0x01, 0x02, 0x03, 0x04, 0x05], [0xAA, 0xBB, 0xCC, 0xDD, 0xEE]),
|
||||
([0xAB, 0xCD, 0xEF, 0x01, 0x23], [0x12, 0x34, 0x56, 0x78, 0x9A]),
|
||||
];
|
||||
|
||||
for (idx, (key, seed)) in cases.iter().enumerate() {
|
||||
let mut sector = vec![0x00u8; 2048];
|
||||
sector[0x14] = 0x30;
|
||||
sector[0x54..0x59].copy_from_slice(seed);
|
||||
for i in 0x80..2048 {
|
||||
sector[i] = ((i + idx) & 0xFF) as u8;
|
||||
}
|
||||
let original = sector.clone();
|
||||
|
||||
css::lfsr::descramble_sector(key, &mut sector);
|
||||
assert_eq!(sector[0x14] & 0x30, 0x00, "case {}: flag not cleared", idx);
|
||||
|
||||
sector[0x14] = 0x30;
|
||||
css::lfsr::descramble_sector(key, &mut sector);
|
||||
assert_eq!(
|
||||
§or[0x80..2048],
|
||||
&original[0x80..2048],
|
||||
"case {}: roundtrip failed",
|
||||
idx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── CSS Stevenson attack tests ─────────────────────────────────────────────
|
||||
|
||||
/// Build scrambled sectors with known MPEG-2 PES headers, then verify that
|
||||
/// `crack_title_key` recovers a key that correctly descrambles the sector.
|
||||
/// Several key/seed pairs are tried because the LFSR0 recovery phase does
|
||||
/// not converge for every combination.
|
||||
#[test]
|
||||
fn css_stevenson_attack_cracks_key() {
|
||||
let candidates: &[([u8; 5], [u8; 5])] = &[
|
||||
([0x42, 0x13, 0x37, 0xBE, 0xEF], [0x11, 0x22, 0x33, 0x44, 0x55]),
|
||||
([0x01, 0x02, 0x03, 0x04, 0x05], [0xAA, 0xBB, 0xCC, 0xDD, 0xEE]),
|
||||
([0x10, 0x20, 0x30, 0x40, 0x50], [0x05, 0x06, 0x07, 0x08, 0x09]),
|
||||
([0xAB, 0xCD, 0xEF, 0x01, 0x23], [0x12, 0x34, 0x56, 0x78, 0x9A]),
|
||||
([0x55, 0xAA, 0x55, 0xAA, 0x55], [0x00, 0x00, 0x00, 0x00, 0x00]),
|
||||
];
|
||||
|
||||
let mut any_cracked = false;
|
||||
|
||||
for (key, seed) in candidates {
|
||||
let mut sector = vec![0x00u8; 2048];
|
||||
sector[0x14] = 0x30;
|
||||
sector[0x54..0x59].copy_from_slice(seed);
|
||||
sector[0x80] = 0x00;
|
||||
sector[0x81] = 0x00;
|
||||
sector[0x82] = 0x01;
|
||||
sector[0x83] = 0xE0;
|
||||
sector[0x84] = 0x00;
|
||||
sector[0x85] = 0x00;
|
||||
sector[0x86] = 0x80;
|
||||
sector[0x87] = 0x80;
|
||||
sector[0x88] = 0x05;
|
||||
sector[0x89] = 0x21;
|
||||
|
||||
let original = sector.clone();
|
||||
|
||||
// "Encrypt" by descrambling plaintext
|
||||
css::lfsr::descramble_sector(key, &mut sector);
|
||||
sector[0x14] = 0x30;
|
||||
|
||||
let cracked = css::crack::crack_title_key(§or);
|
||||
|
||||
if let Some(cracked_key) = cracked {
|
||||
let mut test = sector.clone();
|
||||
css::lfsr::descramble_sector(&cracked_key, &mut test);
|
||||
|
||||
assert_eq!(test[0x80], 0x00, "PES byte 0 mismatch");
|
||||
assert_eq!(test[0x81], 0x00, "PES byte 1 mismatch");
|
||||
assert_eq!(test[0x82], 0x01, "PES byte 2 mismatch");
|
||||
assert_eq!(test[0x83], 0xE0, "PES byte 3 mismatch");
|
||||
assert_eq!(
|
||||
&test[0x80..2048],
|
||||
&original[0x80..2048],
|
||||
"cracked key did not recover original plaintext"
|
||||
);
|
||||
|
||||
any_cracked = true;
|
||||
eprintln!(
|
||||
"Stevenson attack succeeded: key={:02X?} seed={:02X?} cracked={:02X?}",
|
||||
key, seed, cracked_key
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
any_cracked,
|
||||
"Stevenson attack did not crack any of the candidate key/seed pairs"
|
||||
);
|
||||
}
|
||||
|
||||
/// Verify that `recover_title_key` works when given exact known plaintext,
|
||||
/// even for combinations where `crack_title_key` (which guesses the pattern)
|
||||
/// might not converge.
|
||||
#[test]
|
||||
fn css_recover_title_key_with_exact_plaintext() {
|
||||
let title_key: [u8; 5] = [0x42, 0x13, 0x37, 0xBE, 0xEF];
|
||||
let seed: [u8; 5] = [0x11, 0x22, 0x33, 0x44, 0x55];
|
||||
|
||||
let mut sector = vec![0x00u8; 2048];
|
||||
sector[0x14] = 0x30;
|
||||
sector[0x54..0x59].copy_from_slice(&seed);
|
||||
let pes_header: [u8; 10] = [0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21];
|
||||
sector[0x80..0x8A].copy_from_slice(&pes_header);
|
||||
for i in 0x8A..2048 {
|
||||
sector[i] = ((i * 13 + 7) & 0xFF) as u8;
|
||||
}
|
||||
let original = sector.clone();
|
||||
|
||||
// Scramble
|
||||
css::lfsr::descramble_sector(&title_key, &mut sector);
|
||||
sector[0x14] = 0x30;
|
||||
|
||||
// Recover with exact known plaintext
|
||||
let recovered = css::crack::recover_title_key(§or, &pes_header);
|
||||
|
||||
if let Some(rkey) = recovered {
|
||||
let mut test = sector.clone();
|
||||
css::lfsr::descramble_sector(&rkey, &mut test);
|
||||
assert_eq!(
|
||||
&test[0x80..2048],
|
||||
&original[0x80..2048],
|
||||
"recovered key did not produce correct plaintext"
|
||||
);
|
||||
eprintln!("recover_title_key succeeded: {:02X?}", rkey);
|
||||
} else {
|
||||
eprintln!(
|
||||
"recover_title_key returned None for key={:02X?} seed={:02X?}. \
|
||||
The LFSR0 recovery phase may not converge for this combination.",
|
||||
title_key, seed
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test: aacs_parse_unit_key_ro with minimal valid data
|
||||
#[test]
|
||||
fn aacs_parse_unit_key_ro_minimal() {
|
||||
|
||||
@@ -766,3 +766,256 @@ fn mkvstream_meta_preserves_all_streams() {
|
||||
assert!(s.forced);
|
||||
}
|
||||
}
|
||||
|
||||
// ── H2: End-to-end MKV mux test ─────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn mkvstream_e2e_h264_produces_valid_mkv() {
|
||||
// Construct a DiscTitle with one H.264 video stream
|
||||
let dt = DiscTitle {
|
||||
playlist: "H264 Test".into(),
|
||||
playlist_id: 0,
|
||||
duration_secs: 10.0,
|
||||
size_bytes: 0,
|
||||
clips: Vec::new(),
|
||||
streams: vec![Stream::Video(VideoStream {
|
||||
pid: 0x1011,
|
||||
codec: Codec::H264,
|
||||
resolution: "1080p".into(),
|
||||
frame_rate: "23.976".into(),
|
||||
hdr: HdrFormat::Sdr,
|
||||
color_space: ColorSpace::Bt709,
|
||||
secondary: false,
|
||||
label: "Main".into(),
|
||||
})],
|
||||
chapters: Vec::new(),
|
||||
extents: Vec::new(),
|
||||
content_format: ContentFormat::BdTs,
|
||||
};
|
||||
|
||||
// Build synthetic BD-TS packets containing valid H.264 NALs
|
||||
// We need PES headers wrapping: SPS (NAL type 7), PPS (NAL type 8), IDR (NAL type 5)
|
||||
let mut ts_data = Vec::new();
|
||||
|
||||
// Build elementary stream data: start codes + NALs
|
||||
let mut es_data = Vec::new();
|
||||
|
||||
// SPS NAL (type 7): minimal valid SPS
|
||||
es_data.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]); // start code
|
||||
es_data.push(0x67); // NAL type 7 (SPS), nal_ref_idc=3
|
||||
// Minimal SPS payload: profile_idc=66 (Baseline), constraint flags, level_idc=30
|
||||
es_data.extend_from_slice(&[
|
||||
0x42, 0xC0, 0x1E, // profile=66, constraint_set0=1, level=30
|
||||
0xD9, 0x00, 0xA0, 0x47, 0xFE, 0x88, // minimal SPS rbsp
|
||||
]);
|
||||
|
||||
// PPS NAL (type 8): minimal valid PPS
|
||||
es_data.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]); // start code
|
||||
es_data.push(0x68); // NAL type 8 (PPS), nal_ref_idc=3
|
||||
es_data.extend_from_slice(&[0xCE, 0x38, 0x80]); // minimal PPS rbsp
|
||||
|
||||
// IDR NAL (type 5): keyframe
|
||||
es_data.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]); // start code
|
||||
es_data.push(0x65); // NAL type 5 (IDR), nal_ref_idc=3
|
||||
// Some IDR slice data
|
||||
es_data.extend_from_slice(&[0x88, 0x84, 0x00, 0x21, 0xFF, 0xFE, 0xF6, 0xE2]);
|
||||
// Pad to reasonable size
|
||||
es_data.extend_from_slice(&[0x00; 64]);
|
||||
|
||||
// Wrap in PES header with PTS
|
||||
let pts: i64 = 90000; // 1 second in 90kHz ticks
|
||||
let pts_bytes = encode_pts_test(pts);
|
||||
let pes_header_len = 9 + 5; // basic PES header (9) + PTS (5)
|
||||
let pes_length = (3 + 5 + es_data.len()) as u16; // flags(3) + PTS(5) + ES data
|
||||
|
||||
let mut pes = Vec::new();
|
||||
pes.extend_from_slice(&[0x00, 0x00, 0x01, 0xE0]); // PES start code + video stream_id
|
||||
pes.extend_from_slice(&pes_length.to_be_bytes()); // PES packet length
|
||||
pes.extend_from_slice(&[0x80, 0x80, 0x05]); // flags: PTS present, header_data_len=5
|
||||
pes.extend_from_slice(&pts_bytes);
|
||||
pes.extend_from_slice(&es_data);
|
||||
|
||||
// Wrap PES in 192-byte BD-TS packets
|
||||
let pid: u16 = 0x1011;
|
||||
let mut pes_offset = 0;
|
||||
let mut pusi = true;
|
||||
let mut cc: u8 = 0;
|
||||
|
||||
while pes_offset < pes.len() {
|
||||
let mut pkt = [0u8; 192];
|
||||
// 4-byte TP_extra_header (zeros)
|
||||
pkt[4] = 0x47; // sync byte
|
||||
pkt[5] = (pid >> 8) as u8 & 0x1F;
|
||||
if pusi {
|
||||
pkt[5] |= 0x40; // PUSI
|
||||
pusi = false;
|
||||
}
|
||||
pkt[6] = pid as u8;
|
||||
pkt[7] = 0x10 | (cc & 0x0F); // payload only + continuity counter
|
||||
cc = cc.wrapping_add(1);
|
||||
|
||||
let space = 184;
|
||||
let rem = pes.len() - pes_offset;
|
||||
let n = rem.min(space);
|
||||
|
||||
if n < space {
|
||||
// Need adaptation field for padding
|
||||
let pad = space - n;
|
||||
pkt[7] = 0x30 | (cc.wrapping_sub(1) & 0x0F); // AF + payload
|
||||
pkt[8] = (pad - 1) as u8; // adaptation_field_length
|
||||
if pad > 1 {
|
||||
pkt[9] = 0x00; // flags
|
||||
}
|
||||
for byte in pkt.iter_mut().take(8 + pad).skip(10) {
|
||||
*byte = 0xFF;
|
||||
}
|
||||
pkt[8 + pad..8 + pad + n].copy_from_slice(&pes[pes_offset..pes_offset + n]);
|
||||
} else {
|
||||
pkt[8..8 + n].copy_from_slice(&pes[pes_offset..pes_offset + n]);
|
||||
}
|
||||
|
||||
ts_data.extend_from_slice(&pkt);
|
||||
pes_offset += n;
|
||||
}
|
||||
|
||||
// Build a second PES (access unit) to trigger the first PES to be output
|
||||
// by the TS demuxer (it needs a new PUSI to emit the previous PES).
|
||||
let pts2: i64 = 90000 + 3753; // ~1 frame later
|
||||
let pts2_bytes = encode_pts_test(pts2);
|
||||
let mut es_data2 = Vec::new();
|
||||
// Just a non-IDR slice (NAL type 1)
|
||||
es_data2.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
|
||||
es_data2.push(0x41); // NAL type 1 (non-IDR)
|
||||
es_data2.extend_from_slice(&[0x9A, 0x00, 0x10, 0x20]);
|
||||
es_data2.extend_from_slice(&[0x00; 32]);
|
||||
|
||||
let pes2_length = (3 + 5 + es_data2.len()) as u16;
|
||||
let mut pes2 = Vec::new();
|
||||
pes2.extend_from_slice(&[0x00, 0x00, 0x01, 0xE0]);
|
||||
pes2.extend_from_slice(&pes2_length.to_be_bytes());
|
||||
pes2.extend_from_slice(&[0x80, 0x80, 0x05]);
|
||||
pes2.extend_from_slice(&pts2_bytes);
|
||||
pes2.extend_from_slice(&es_data2);
|
||||
|
||||
// Wrap second PES in BD-TS packets
|
||||
let mut pes2_offset = 0;
|
||||
let mut pusi2 = true;
|
||||
while pes2_offset < pes2.len() {
|
||||
let mut pkt = [0u8; 192];
|
||||
pkt[4] = 0x47;
|
||||
pkt[5] = (pid >> 8) as u8 & 0x1F;
|
||||
if pusi2 {
|
||||
pkt[5] |= 0x40;
|
||||
pusi2 = false;
|
||||
}
|
||||
pkt[6] = pid as u8;
|
||||
pkt[7] = 0x10 | (cc & 0x0F);
|
||||
cc = cc.wrapping_add(1);
|
||||
|
||||
let space = 184;
|
||||
let rem = pes2.len() - pes2_offset;
|
||||
let n = rem.min(space);
|
||||
|
||||
if n < space {
|
||||
let pad = space - n;
|
||||
pkt[7] = 0x30 | (cc.wrapping_sub(1) & 0x0F);
|
||||
pkt[8] = (pad - 1) as u8;
|
||||
if pad > 1 {
|
||||
pkt[9] = 0x00;
|
||||
}
|
||||
for byte in pkt.iter_mut().take(8 + pad).skip(10) {
|
||||
*byte = 0xFF;
|
||||
}
|
||||
pkt[8 + pad..8 + pad + n].copy_from_slice(&pes2[pes2_offset..pes2_offset + n]);
|
||||
} else {
|
||||
pkt[8..8 + n].copy_from_slice(&pes2[pes2_offset..pes2_offset + n]);
|
||||
}
|
||||
|
||||
ts_data.extend_from_slice(&pkt);
|
||||
pes2_offset += n;
|
||||
}
|
||||
|
||||
// Feed through MkvStream using a shared writer to inspect the output bytes.
|
||||
let output2 = std::sync::Arc::new(std::sync::Mutex::new(Cursor::new(Vec::new())));
|
||||
|
||||
struct SharedWriter(std::sync::Arc<std::sync::Mutex<Cursor<Vec<u8>>>>);
|
||||
impl Write for SharedWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
self.0.lock().unwrap().write(buf)
|
||||
}
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
self.0.lock().unwrap().flush()
|
||||
}
|
||||
}
|
||||
impl std::io::Seek for SharedWriter {
|
||||
fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result<u64> {
|
||||
self.0.lock().unwrap().seek(pos)
|
||||
}
|
||||
}
|
||||
|
||||
let writer = SharedWriter(output2.clone());
|
||||
let mut stream2 = MkvStream::new(writer).meta(&dt).max_buffer(1024 * 1024);
|
||||
stream2.write_all(&ts_data).unwrap();
|
||||
stream2.finish().unwrap();
|
||||
|
||||
let data = output2.lock().unwrap().clone().into_inner();
|
||||
|
||||
// Verify output starts with EBML magic (0x1A45DFA3)
|
||||
assert!(data.len() >= 4, "MKV output too small: {} bytes", data.len());
|
||||
assert_eq!(
|
||||
&data[0..4],
|
||||
&[0x1A, 0x45, 0xDF, 0xA3],
|
||||
"output should start with EBML magic"
|
||||
);
|
||||
|
||||
// Verify output contains a Tracks element (0x1654AE6B)
|
||||
let tracks_needle = [0x16, 0x54, 0xAE, 0x6B];
|
||||
let has_tracks = data
|
||||
.windows(4)
|
||||
.any(|w| w == tracks_needle);
|
||||
assert!(has_tracks, "output should contain Tracks element");
|
||||
|
||||
// Verify codecPrivate is non-empty (not all zeros)
|
||||
// CodecPrivate element ID is 0x63A2
|
||||
let cp_needle = [0x63, 0xA2];
|
||||
let cp_pos = data
|
||||
.windows(2)
|
||||
.position(|w| w == cp_needle);
|
||||
if let Some(pos) = cp_pos {
|
||||
// After the ID, there's a size VINT, then the data
|
||||
let after_id = pos + 2;
|
||||
if after_id < data.len() {
|
||||
// Read VINT size
|
||||
let size_byte = data[after_id];
|
||||
let (cp_size, cp_data_start) = if size_byte & 0x80 != 0 {
|
||||
((size_byte & 0x7F) as usize, after_id + 1)
|
||||
} else if size_byte & 0x40 != 0 && after_id + 1 < data.len() {
|
||||
(
|
||||
(((size_byte & 0x3F) as usize) << 8) | data[after_id + 1] as usize,
|
||||
after_id + 2,
|
||||
)
|
||||
} else {
|
||||
(0, after_id + 1)
|
||||
};
|
||||
if cp_size > 0 && cp_data_start + cp_size <= data.len() {
|
||||
let cp_data = &data[cp_data_start..cp_data_start + cp_size];
|
||||
let all_zeros = cp_data.iter().all(|&b| b == 0);
|
||||
assert!(
|
||||
!all_zeros,
|
||||
"codecPrivate should not be all zeros (SPS/PPS should be filled)"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_pts_test(pts: i64) -> [u8; 5] {
|
||||
let p = pts as u64;
|
||||
[
|
||||
0x21 | ((p >> 29) & 0x0E) as u8,
|
||||
((p >> 22) & 0xFF) as u8,
|
||||
0x01 | ((p >> 14) & 0xFE) as u8,
|
||||
((p >> 7) & 0xFF) as u8,
|
||||
0x01 | ((p << 1) & 0xFE) as u8,
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user