wip: rc6 VFR/DVD/CSS base (held for bulletproofing + split)
This commit is contained in:
+35
-20
@@ -262,7 +262,20 @@ pub fn crack_title_key(sector: &[u8]) -> Option<[u8; 5]> {
|
|||||||
/// Inner body of [`crack_title_key`] — the actual AttackPattern search. Split
|
/// Inner body of [`crack_title_key`] — the actual AttackPattern search. Split
|
||||||
/// out so the public entry point can wall-clock the whole attempt for the
|
/// out so the public entry point can wall-clock the whole attempt for the
|
||||||
/// runaway guard without threading a timer through every return path.
|
/// runaway guard without threading a timer through every return path.
|
||||||
fn crack_title_key_inner(sector: &[u8]) -> Option<[u8; 5]> {
|
/// AttackPattern crib: the predicted 10-byte plaintext at byte 0x80.
|
||||||
|
///
|
||||||
|
/// Scans the clear header `sec[0x00..0x80]` (never scrambled) for the longest
|
||||||
|
/// run that repeats with a cycle length in 2..0x2F. If the run is long enough
|
||||||
|
/// (`plen > 3` and at least two full cycles), the plaintext at 0x80 is taken to
|
||||||
|
/// be that periodic run continuing forward. Returns `None` for an unscrambled
|
||||||
|
/// sector or one with no usable run — such a sector can be neither cracked nor
|
||||||
|
/// key-validated, only descrambled with an externally-cached key.
|
||||||
|
///
|
||||||
|
/// The header is untouched by `descramble_sector`, so the crib is identical
|
||||||
|
/// before and after descramble: the decrypt path uses it as a per-sector
|
||||||
|
/// "did the cached key descramble correctly?" oracle (the predicted plaintext
|
||||||
|
/// must reappear at 0x80), and the cracker uses it as its known plaintext.
|
||||||
|
pub(crate) fn attack_crib(sector: &[u8]) -> Option<[u8; 10]> {
|
||||||
if sector.len() < SECTOR_SIZE || sector[FLAG_BYTE] & 0x30 == 0 {
|
if sector.len() < SECTOR_SIZE || sector[FLAG_BYTE] & 0x30 == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -285,14 +298,6 @@ fn crack_title_key_inner(sector: &[u8]) -> Option<[u8; 5]> {
|
|||||||
|
|
||||||
// Need at least a few repeated bytes and at least one full cycle.
|
// Need at least a few repeated bytes and at least one full cycle.
|
||||||
if best_plen > 3 && best_p > 0 && best_plen / best_p >= 2 {
|
if best_plen > 3 && best_p > 0 && best_plen / best_p >= 2 {
|
||||||
let seed: [u8; 5] = [
|
|
||||||
sector[SEED_OFFSET],
|
|
||||||
sector[SEED_OFFSET + 1],
|
|
||||||
sector[SEED_OFFSET + 2],
|
|
||||||
sector[SEED_OFFSET + 3],
|
|
||||||
sector[SEED_OFFSET + 4],
|
|
||||||
];
|
|
||||||
|
|
||||||
// The known plaintext is the periodic run continuing past 0x80. The
|
// The known plaintext is the periodic run continuing past 0x80. The
|
||||||
// crib starts at `0x80 - (best_plen/best_p)*best_p` and continues
|
// crib starts at `0x80 - (best_plen/best_p)*best_p` and continues
|
||||||
// through the encrypted region; the bytes at and after 0x80 are the
|
// through the encrypted region; the bytes at and after 0x80 are the
|
||||||
@@ -300,30 +305,40 @@ fn crack_title_key_inner(sector: &[u8]) -> Option<[u8; 5]> {
|
|||||||
let cycles = best_plen / best_p;
|
let cycles = best_plen / best_p;
|
||||||
let plain_start = 0x80 - cycles * best_p;
|
let plain_start = 0x80 - cycles * best_p;
|
||||||
|
|
||||||
// The cipher is the 10 bytes at 0x80; the crib is their predicted
|
// Each predicted byte is the run sample one or more periods back:
|
||||||
// plaintext. The periodic run (period `best_p`) is known to continue
|
// `sec[plain_start + (i % best_p)]`. For in-run offsets
|
||||||
// through 0x80, so each predicted byte is the run sample one or more
|
|
||||||
// periods back: `sec[plain_start + (i % best_p)]`. For in-run offsets
|
|
||||||
// (`plain_start + i < 0x80`) the run is exactly periodic, so this
|
// (`plain_start + i < 0x80`) the run is exactly periodic, so this
|
||||||
// equals `sec[plain_start + i]`; for offsets at/after 0x80 the raw
|
// equals `sec[plain_start + i]`; for offsets at/after 0x80 the raw
|
||||||
// byte is ciphertext, so we MUST wrap within the period rather than
|
// byte is ciphertext, so we MUST wrap within the period rather than
|
||||||
// read it. (Reading `&sec[plain_start..+10]` directly — as before —
|
// read it. (Reading `&sec[plain_start..+10]` directly — as before —
|
||||||
// pulled ciphertext into the crib whenever the run covered fewer than
|
// pulled ciphertext into the crib whenever the run covered fewer than
|
||||||
// 10 bytes before 0x80, producing false-negative key recovery.)
|
// 10 bytes before 0x80, producing false-negative key recovery.)
|
||||||
let crypted = §or[0x80..0x80 + 10];
|
|
||||||
let mut plain = [0u8; 10];
|
let mut plain = [0u8; 10];
|
||||||
for (i, p) in plain.iter_mut().enumerate() {
|
for (i, p) in plain.iter_mut().enumerate() {
|
||||||
*p = sector[plain_start + (i % best_p)];
|
*p = sector[plain_start + (i % best_p)];
|
||||||
}
|
}
|
||||||
|
Some(plain)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(key) = recover_title_key_from_plain(crypted, &plain, &seed) {
|
fn crack_title_key_inner(sector: &[u8]) -> Option<[u8; 5]> {
|
||||||
// Verify against the same predicted plaintext.
|
let plain = attack_crib(sector)?;
|
||||||
if descramble_matches(sector, &key, &plain) {
|
let seed: [u8; 5] = [
|
||||||
return Some(key);
|
sector[SEED_OFFSET],
|
||||||
}
|
sector[SEED_OFFSET + 1],
|
||||||
|
sector[SEED_OFFSET + 2],
|
||||||
|
sector[SEED_OFFSET + 3],
|
||||||
|
sector[SEED_OFFSET + 4],
|
||||||
|
];
|
||||||
|
let crypted = §or[0x80..0x80 + 10];
|
||||||
|
if let Some(key) = recover_title_key_from_plain(crypted, &plain, &seed) {
|
||||||
|
// Verify against the same predicted plaintext.
|
||||||
|
if descramble_matches(sector, &key, &plain) {
|
||||||
|
return Some(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+156
-35
@@ -183,7 +183,7 @@ impl DecryptKeys {
|
|||||||
/// scrambled unit decrypted.
|
/// scrambled unit decrypted.
|
||||||
pub fn decrypt_sectors(
|
pub fn decrypt_sectors(
|
||||||
buf: &mut [u8],
|
buf: &mut [u8],
|
||||||
keys: &DecryptKeys,
|
keys: &mut DecryptKeys,
|
||||||
unit_key_idx: usize,
|
unit_key_idx: usize,
|
||||||
) -> Result<usize, crate::error::Error> {
|
) -> Result<usize, crate::error::Error> {
|
||||||
let dropped: usize = match keys {
|
let dropped: usize = match keys {
|
||||||
@@ -351,8 +351,41 @@ pub fn decrypt_sectors(
|
|||||||
dropped_bytes.into_inner()
|
dropped_bytes.into_inner()
|
||||||
}
|
}
|
||||||
DecryptKeys::Css { title_key } => {
|
DecryptKeys::Css { title_key } => {
|
||||||
|
// CSS has no supplied key list: the ONLY source of a title key is
|
||||||
|
// cracking the data, and the key changes per VTS/VOB region. So
|
||||||
|
// `title_key` is a CACHE of the last crack, not a fixed disc key —
|
||||||
|
// applying it blindly across a region boundary descrambles with the
|
||||||
|
// wrong key (valid headers, garbage payload). Validate it on every
|
||||||
|
// scrambled sector and re-crack on a miss (libdvdcss's on-demand
|
||||||
|
// per-region rekey; the same validate-then-rekey shape the AACS arm
|
||||||
|
// above uses, but re-cracking instead of picking from a list).
|
||||||
|
//
|
||||||
|
// The clear header (<0x80) is never scrambled, so its periodic crib
|
||||||
|
// predicts the plaintext at 0x80. Descramble with the cached key; if
|
||||||
|
// the crib fails to reappear the key region changed (or the primed
|
||||||
|
// key was wrong) — restore the ciphertext, re-crack from this very
|
||||||
|
// sector, and descramble again. A crib-less sector (no periodic run)
|
||||||
|
// can be neither validated nor cracked, so it rides the cached key —
|
||||||
|
// correct, because it lives in the same region as the nearby crib
|
||||||
|
// sector that set the cache.
|
||||||
for chunk in buf.chunks_mut(2048) {
|
for chunk in buf.chunks_mut(2048) {
|
||||||
|
if chunk.len() < 2048 || !css::is_scrambled(chunk) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let crib = css::stevenson::attack_crib(chunk);
|
||||||
|
let original: Option<Vec<u8>> = crib.as_ref().map(|_| chunk.to_vec());
|
||||||
css::lfsr::descramble_sector(title_key, chunk);
|
css::lfsr::descramble_sector(title_key, chunk);
|
||||||
|
if let (Some(crib), Some(original)) = (crib, original) {
|
||||||
|
if chunk[0x80..0x80 + 10] != crib[..] {
|
||||||
|
// Cached key is stale for this region — restore the
|
||||||
|
// ciphertext and crack this sector's own key.
|
||||||
|
chunk.copy_from_slice(&original);
|
||||||
|
if let Some(fresh) = css::stevenson::crack_title_key(chunk) {
|
||||||
|
*title_key = fresh;
|
||||||
|
}
|
||||||
|
css::lfsr::descramble_sector(title_key, chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
@@ -381,11 +414,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
let snapshot = unit.clone();
|
let snapshot = unit.clone();
|
||||||
|
|
||||||
let keys = DecryptKeys::Aacs {
|
let mut keys = DecryptKeys::Aacs {
|
||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
decrypt_sectors(&mut unit, &keys, 0).unwrap();
|
decrypt_sectors(&mut unit, &mut keys, 0).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
unit, snapshot,
|
unit, snapshot,
|
||||||
"non-m2ts unit must be restored after failed decrypt"
|
"non-m2ts unit must be restored after failed decrypt"
|
||||||
@@ -424,7 +457,7 @@ mod tests {
|
|||||||
/// bytes byte-for-byte unchanged — no regression on real discs.
|
/// bytes byte-for-byte unchanged — no regression on real discs.
|
||||||
#[test]
|
#[test]
|
||||||
fn aacs_clear_trailing_partial_is_tolerated_unchanged() {
|
fn aacs_clear_trailing_partial_is_tolerated_unchanged() {
|
||||||
let keys = DecryptKeys::Aacs {
|
let mut keys = DecryptKeys::Aacs {
|
||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
@@ -434,7 +467,7 @@ mod tests {
|
|||||||
let mut buf = unit;
|
let mut buf = unit;
|
||||||
buf.extend_from_slice(&tail);
|
buf.extend_from_slice(&tail);
|
||||||
|
|
||||||
decrypt_sectors(&mut buf, &keys, 0).expect("clear trailing partial is Ok");
|
decrypt_sectors(&mut buf, &mut keys, 0).expect("clear trailing partial is Ok");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&buf[aacs::ALIGNED_UNIT_LEN..],
|
&buf[aacs::ALIGNED_UNIT_LEN..],
|
||||||
@@ -449,7 +482,7 @@ mod tests {
|
|||||||
/// corruption, so we must fail loud with `DecryptFailed`.
|
/// corruption, so we must fail loud with `DecryptFailed`.
|
||||||
#[test]
|
#[test]
|
||||||
fn aacs_scrambled_trailing_partial_is_rejected() {
|
fn aacs_scrambled_trailing_partial_is_rejected() {
|
||||||
let keys = DecryptKeys::Aacs {
|
let mut keys = DecryptKeys::Aacs {
|
||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
@@ -459,7 +492,7 @@ mod tests {
|
|||||||
let mut buf = unit;
|
let mut buf = unit;
|
||||||
buf.extend_from_slice(&tail);
|
buf.extend_from_slice(&tail);
|
||||||
|
|
||||||
let err = decrypt_sectors(&mut buf, &keys, 0)
|
let err = decrypt_sectors(&mut buf, &mut keys, 0)
|
||||||
.expect_err("scrambled trailing partial must be rejected");
|
.expect_err("scrambled trailing partial must be rejected");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err.code(),
|
err.code(),
|
||||||
@@ -471,12 +504,12 @@ mod tests {
|
|||||||
/// An empty buffer is a valid no-op (zero units), not an error.
|
/// An empty buffer is a valid no-op (zero units), not an error.
|
||||||
#[test]
|
#[test]
|
||||||
fn aacs_empty_buffer_is_ok() {
|
fn aacs_empty_buffer_is_ok() {
|
||||||
let keys = DecryptKeys::Aacs {
|
let mut keys = DecryptKeys::Aacs {
|
||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut buf: Vec<u8> = Vec::new();
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
assert!(decrypt_sectors(&mut buf, &keys, 0).is_ok());
|
assert!(decrypt_sectors(&mut buf, &mut keys, 0).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An exact multiple of the unit length has no trailing partial: behavior
|
/// An exact multiple of the unit length has no trailing partial: behavior
|
||||||
@@ -484,14 +517,14 @@ mod tests {
|
|||||||
/// attempted. Two clear units must round-trip untouched and return `Ok`.
|
/// attempted. Two clear units must round-trip untouched and return `Ok`.
|
||||||
#[test]
|
#[test]
|
||||||
fn aacs_exact_multiple_unchanged() {
|
fn aacs_exact_multiple_unchanged() {
|
||||||
let keys = DecryptKeys::Aacs {
|
let mut keys = DecryptKeys::Aacs {
|
||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut buf = clear_ts_region(aacs::ALIGNED_UNIT_LEN * 2);
|
let mut buf = clear_ts_region(aacs::ALIGNED_UNIT_LEN * 2);
|
||||||
let snapshot = buf.clone();
|
let snapshot = buf.clone();
|
||||||
|
|
||||||
decrypt_sectors(&mut buf, &keys, 0).expect("exact-multiple buffer is Ok");
|
decrypt_sectors(&mut buf, &mut keys, 0).expect("exact-multiple buffer is Ok");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
buf, snapshot,
|
buf, snapshot,
|
||||||
@@ -512,7 +545,7 @@ mod tests {
|
|||||||
fn none_keys_is_noop() {
|
fn none_keys_is_noop() {
|
||||||
let mut buf: Vec<u8> = (0..4096u32).map(|i| (i % 256) as u8).collect();
|
let mut buf: Vec<u8> = (0..4096u32).map(|i| (i % 256) as u8).collect();
|
||||||
let snapshot = buf.clone();
|
let snapshot = buf.clone();
|
||||||
decrypt_sectors(&mut buf, &DecryptKeys::None, 0).expect("None is always Ok");
|
decrypt_sectors(&mut buf, &mut DecryptKeys::None, 0).expect("None is always Ok");
|
||||||
assert_eq!(buf, snapshot, "None must not touch the buffer");
|
assert_eq!(buf, snapshot, "None must not touch the buffer");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,8 +597,8 @@ mod tests {
|
|||||||
let title_key = [0x42, 0x13, 0x37, 0xBE, 0xEF];
|
let title_key = [0x42, 0x13, 0x37, 0xBE, 0xEF];
|
||||||
let seed = [0xDE, 0xAD, 0xBE, 0xEF, 0x42];
|
let seed = [0xDE, 0xAD, 0xBE, 0xEF, 0x42];
|
||||||
let (mut sector, plaintext) = make_css_sector(&title_key, &seed, 0xA5);
|
let (mut sector, plaintext) = make_css_sector(&title_key, &seed, 0xA5);
|
||||||
let keys = DecryptKeys::Css { title_key };
|
let mut keys = DecryptKeys::Css { title_key };
|
||||||
decrypt_sectors(&mut sector, &keys, 0).expect("CSS decrypt is Ok");
|
decrypt_sectors(&mut sector, &mut keys, 0).expect("CSS decrypt is Ok");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
§or[0x80..2048],
|
§or[0x80..2048],
|
||||||
&plaintext[0x80..2048],
|
&plaintext[0x80..2048],
|
||||||
@@ -594,8 +627,8 @@ mod tests {
|
|||||||
let (s1, p1) = make_css_sector(&title_key, &[0x66, 0x77, 0x88, 0x99, 0xAA], 0xC3);
|
let (s1, p1) = make_css_sector(&title_key, &[0x66, 0x77, 0x88, 0x99, 0xAA], 0xC3);
|
||||||
let mut buf = s0;
|
let mut buf = s0;
|
||||||
buf.extend_from_slice(&s1);
|
buf.extend_from_slice(&s1);
|
||||||
let keys = DecryptKeys::Css { title_key };
|
let mut keys = DecryptKeys::Css { title_key };
|
||||||
decrypt_sectors(&mut buf, &keys, 0).expect("CSS multi-sector decrypt is Ok");
|
decrypt_sectors(&mut buf, &mut keys, 0).expect("CSS multi-sector decrypt is Ok");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
&buf[0x80..2048],
|
&buf[0x80..2048],
|
||||||
&p0[0x80..2048],
|
&p0[0x80..2048],
|
||||||
@@ -608,6 +641,94 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a CSS sector whose clear header ends in a periodic run that
|
||||||
|
/// continues into the encrypted region — the crackable shape `attack_crib`/
|
||||||
|
/// `crack_title_key` recover a key from (a constant body fill gives a
|
||||||
|
/// degenerate crib the cracker can't pin a unique key on). Returns
|
||||||
|
/// (scrambled_sector, plaintext_body).
|
||||||
|
fn make_crackable_css_sector(
|
||||||
|
title_key: &[u8; 5],
|
||||||
|
seed: &[u8; 5],
|
||||||
|
period: usize,
|
||||||
|
) -> (Vec<u8>, Vec<u8>) {
|
||||||
|
let mut plaintext = vec![0u8; 2048];
|
||||||
|
plaintext[0x14] = 0x10; // scramble flag
|
||||||
|
// Periodic run from 0x59 (just above the seed) through 0x80 and on into
|
||||||
|
// the encrypted region; phase anchored to offset 0 so it is continuous
|
||||||
|
// across the 0x80 boundary.
|
||||||
|
let pat: Vec<u8> = (0..period)
|
||||||
|
.map(|k| (0xA0u8.wrapping_add(k as u8)) ^ 0x5A)
|
||||||
|
.collect();
|
||||||
|
for (i, b) in plaintext.iter_mut().enumerate().skip(0x59) {
|
||||||
|
*b = pat[i % period];
|
||||||
|
}
|
||||||
|
plaintext[0x54..0x59].copy_from_slice(seed); // seed sits below the run
|
||||||
|
let body = plaintext.clone();
|
||||||
|
css::lfsr::scramble_sector(title_key, &mut plaintext);
|
||||||
|
(plaintext, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CSS title keys are per-VTS/VOB region: a real disc holds DIFFERENT keys
|
||||||
|
/// for different regions and the only way to get each is to crack it. The
|
||||||
|
/// decrypt path must re-crack when the cached key stops descrambling (its
|
||||||
|
/// crib no longer reappears at 0x80) instead of blindly applying one key
|
||||||
|
/// across a region boundary — the bug that pixelated every freemkv DVD rip.
|
||||||
|
///
|
||||||
|
/// Two sectors scrambled under DIFFERENT keys, cache primed to ONLY the
|
||||||
|
/// first (exactly what the one-shot scan crack leaves). Sector 0 validates +
|
||||||
|
/// descrambles with the cached key; sector 1's cached-key descramble fails
|
||||||
|
/// the crib, so the path re-cracks sector 1's own key and recovers its
|
||||||
|
/// plaintext. Before the fix (blind single-key apply) sector 1 was garbage.
|
||||||
|
///
|
||||||
|
/// Grounding: the CSS arm's `attack_crib` → `chunk[0x80..] != crib` →
|
||||||
|
/// `crack_title_key` → `*title_key = fresh` rekey.
|
||||||
|
/// Mutation: drop the rekey branch (apply the cached key always) → sector 1's
|
||||||
|
/// body no longer matches its plaintext; this fails.
|
||||||
|
#[test]
|
||||||
|
fn css_rekeys_when_title_key_region_changes() {
|
||||||
|
let key_a = [0x42, 0x13, 0x37, 0xBE, 0xEF];
|
||||||
|
let key_b = [0x07, 0x5A, 0xC3, 0x10, 0x88]; // a DIFFERENT region's key
|
||||||
|
let (s0, p0) = make_crackable_css_sector(&key_a, &[0x11, 0x22, 0x33, 0x44, 0x55], 4);
|
||||||
|
let (s1, p1) = make_crackable_css_sector(&key_b, &[0x66, 0x77, 0x88, 0x99, 0xAA], 4);
|
||||||
|
// Precondition: each sector must be crackable on its own (the rekey
|
||||||
|
// depends on it). If this fails the fixture, not the path, is at fault.
|
||||||
|
assert_eq!(
|
||||||
|
crate::css::stevenson::crack_title_key(&s0),
|
||||||
|
Some(key_a),
|
||||||
|
"fixture s0 must crack to key_a standalone"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
crate::css::stevenson::crack_title_key(&s1),
|
||||||
|
Some(key_b),
|
||||||
|
"fixture s1 must crack to key_b standalone"
|
||||||
|
);
|
||||||
|
let mut buf = s0;
|
||||||
|
buf.extend_from_slice(&s1);
|
||||||
|
|
||||||
|
// Cache primed to key_a only — exactly what the one-shot scan crack yields.
|
||||||
|
let mut keys = DecryptKeys::Css { title_key: key_a };
|
||||||
|
decrypt_sectors(&mut buf, &mut keys, 0).expect("CSS multi-region decrypt is Ok");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
&buf[0x80..2048],
|
||||||
|
&p0[0x80..2048],
|
||||||
|
"region A sector descrambles with the cached (primed) key"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
&buf[2048 + 0x80..4096],
|
||||||
|
&p1[0x80..2048],
|
||||||
|
"region B sector must descramble after the path re-cracks its own key"
|
||||||
|
);
|
||||||
|
// The cache must have advanced to region B's key.
|
||||||
|
match keys {
|
||||||
|
DecryptKeys::Css { title_key } => assert_eq!(
|
||||||
|
title_key, key_b,
|
||||||
|
"cache must hold region B's key after the rekey"
|
||||||
|
),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The CSS path leaves UNSCRAMBLED sectors (flag clear) byte-for-byte
|
/// The CSS path leaves UNSCRAMBLED sectors (flag clear) byte-for-byte
|
||||||
/// untouched — descramble_sector early-returns on a zero flag. A clear
|
/// untouched — descramble_sector early-returns on a zero flag. A clear
|
||||||
/// sector mixed into the buffer must not be corrupted.
|
/// sector mixed into the buffer must not be corrupted.
|
||||||
@@ -622,8 +743,8 @@ mod tests {
|
|||||||
let mut sector = vec![0x77u8; 2048];
|
let mut sector = vec![0x77u8; 2048];
|
||||||
sector[0x14] = 0x00; // not scrambled
|
sector[0x14] = 0x00; // not scrambled
|
||||||
let snapshot = sector.clone();
|
let snapshot = sector.clone();
|
||||||
let keys = DecryptKeys::Css { title_key };
|
let mut keys = DecryptKeys::Css { title_key };
|
||||||
decrypt_sectors(&mut sector, &keys, 0).unwrap();
|
decrypt_sectors(&mut sector, &mut keys, 0).unwrap();
|
||||||
assert_eq!(sector, snapshot, "clear CSS sector must be left untouched");
|
assert_eq!(sector, snapshot, "clear CSS sector must be left untouched");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -636,8 +757,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn css_empty_buffer_is_ok() {
|
fn css_empty_buffer_is_ok() {
|
||||||
let mut buf: Vec<u8> = Vec::new();
|
let mut buf: Vec<u8> = Vec::new();
|
||||||
let keys = DecryptKeys::Css { title_key: [0; 5] };
|
let mut keys = DecryptKeys::Css { title_key: [0; 5] };
|
||||||
assert!(decrypt_sectors(&mut buf, &keys, 0).is_ok());
|
assert!(decrypt_sectors(&mut buf, &mut keys, 0).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── AACS unit-key index selection ──────────────────────────────────────
|
// ── AACS unit-key index selection ──────────────────────────────────────
|
||||||
@@ -653,12 +774,12 @@ mod tests {
|
|||||||
/// fails.
|
/// fails.
|
||||||
#[test]
|
#[test]
|
||||||
fn aacs_out_of_range_unit_key_idx_errors() {
|
fn aacs_out_of_range_unit_key_idx_errors() {
|
||||||
let keys = DecryptKeys::Aacs {
|
let mut keys = DecryptKeys::Aacs {
|
||||||
unit_keys: vec![(0, [0xAB; 16])],
|
unit_keys: vec![(0, [0xAB; 16])],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut buf = clear_ts_region(aacs::ALIGNED_UNIT_LEN);
|
let mut buf = clear_ts_region(aacs::ALIGNED_UNIT_LEN);
|
||||||
let err = decrypt_sectors(&mut buf, &keys, 5)
|
let err = decrypt_sectors(&mut buf, &mut keys, 5)
|
||||||
.expect_err("unit_key_idx 5 is out of range for a 1-key list");
|
.expect_err("unit_key_idx 5 is out of range for a 1-key list");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err.code(),
|
err.code(),
|
||||||
@@ -673,12 +794,12 @@ mod tests {
|
|||||||
/// Mutation: defaulting to [0u8;16] on None would proceed; this fails.
|
/// Mutation: defaulting to [0u8;16] on None would proceed; this fails.
|
||||||
#[test]
|
#[test]
|
||||||
fn aacs_empty_unit_keys_errors() {
|
fn aacs_empty_unit_keys_errors() {
|
||||||
let keys = DecryptKeys::Aacs {
|
let mut keys = DecryptKeys::Aacs {
|
||||||
unit_keys: vec![],
|
unit_keys: vec![],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut buf = clear_ts_region(aacs::ALIGNED_UNIT_LEN);
|
let mut buf = clear_ts_region(aacs::ALIGNED_UNIT_LEN);
|
||||||
let err = decrypt_sectors(&mut buf, &keys, 0).expect_err("empty unit_keys must error");
|
let err = decrypt_sectors(&mut buf, &mut keys, 0).expect_err("empty unit_keys must error");
|
||||||
assert_eq!(err.code(), crate::error::Error::DecryptFailed.code());
|
assert_eq!(err.code(), crate::error::Error::DecryptFailed.code());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,14 +872,14 @@ mod tests {
|
|||||||
"encrypted unit must look scrambled before decrypt"
|
"encrypted unit must look scrambled before decrypt"
|
||||||
);
|
);
|
||||||
|
|
||||||
let keys = DecryptKeys::Aacs {
|
let mut keys = DecryptKeys::Aacs {
|
||||||
unit_keys: vec![(0, key0), (1, key1)], // two CPS units
|
unit_keys: vec![(0, key0), (1, key1)], // two CPS units
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Call with the default hint (idx 0) — the fix must fall back to key1.
|
// Call with the default hint (idx 0) — the fix must fall back to key1.
|
||||||
let mut buf = unit;
|
let mut buf = unit;
|
||||||
decrypt_sectors(&mut buf, &keys, 0).expect("multi-CPS decrypt must succeed");
|
decrypt_sectors(&mut buf, &mut keys, 0).expect("multi-CPS decrypt must succeed");
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
!aacs::is_aacs_scrambled(&buf),
|
!aacs::is_aacs_scrambled(&buf),
|
||||||
@@ -785,12 +906,12 @@ mod tests {
|
|||||||
let mut unit = clear_ts_unit();
|
let mut unit = clear_ts_unit();
|
||||||
aacs_encrypt_unit_for_test(&mut unit, &key);
|
aacs_encrypt_unit_for_test(&mut unit, &key);
|
||||||
|
|
||||||
let keys = DecryptKeys::Aacs {
|
let mut keys = DecryptKeys::Aacs {
|
||||||
unit_keys: vec![(0, key)],
|
unit_keys: vec![(0, key)],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut buf = unit;
|
let mut buf = unit;
|
||||||
decrypt_sectors(&mut buf, &keys, 0).expect("single-key disc must decrypt");
|
decrypt_sectors(&mut buf, &mut keys, 0).expect("single-key disc must decrypt");
|
||||||
assert!(
|
assert!(
|
||||||
!aacs::is_aacs_scrambled(&buf),
|
!aacs::is_aacs_scrambled(&buf),
|
||||||
"single-key disc: TS syncs must be restored"
|
"single-key disc: TS syncs must be restored"
|
||||||
@@ -829,13 +950,13 @@ mod tests {
|
|||||||
"encrypted unit must look scrambled going in"
|
"encrypted unit must look scrambled going in"
|
||||||
);
|
);
|
||||||
|
|
||||||
let keys = DecryptKeys::Aacs {
|
let mut keys = DecryptKeys::Aacs {
|
||||||
unit_keys: vec![(0, wrong_key)],
|
unit_keys: vec![(0, wrong_key)],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut buf = unit;
|
let mut buf = unit;
|
||||||
let dropped =
|
let dropped = decrypt_sectors(&mut buf, &mut keys, 0)
|
||||||
decrypt_sectors(&mut buf, &keys, 0).expect("undecryptable unit is not a hard error");
|
.expect("undecryptable unit is not a hard error");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dropped,
|
dropped,
|
||||||
@@ -872,11 +993,11 @@ mod tests {
|
|||||||
buf.extend_from_slice(&unit_a);
|
buf.extend_from_slice(&unit_a);
|
||||||
buf.extend_from_slice(&unit_b);
|
buf.extend_from_slice(&unit_b);
|
||||||
|
|
||||||
let keys = DecryptKeys::Aacs {
|
let mut keys = DecryptKeys::Aacs {
|
||||||
unit_keys: vec![(0, key)],
|
unit_keys: vec![(0, key)],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let dropped = decrypt_sectors(&mut buf, &keys, 0).expect("partial decrypt is Ok");
|
let dropped = decrypt_sectors(&mut buf, &mut keys, 0).expect("partial decrypt is Ok");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dropped,
|
dropped,
|
||||||
@@ -901,12 +1022,12 @@ mod tests {
|
|||||||
let key = [0x77u8; 16];
|
let key = [0x77u8; 16];
|
||||||
let mut unit = clear_ts_unit();
|
let mut unit = clear_ts_unit();
|
||||||
aacs_encrypt_unit_for_test(&mut unit, &key);
|
aacs_encrypt_unit_for_test(&mut unit, &key);
|
||||||
let keys = DecryptKeys::Aacs {
|
let mut keys = DecryptKeys::Aacs {
|
||||||
unit_keys: vec![(0, key)],
|
unit_keys: vec![(0, key)],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
let mut buf = unit;
|
let mut buf = unit;
|
||||||
let dropped = decrypt_sectors(&mut buf, &keys, 0).expect("clean decrypt");
|
let dropped = decrypt_sectors(&mut buf, &mut keys, 0).expect("clean decrypt");
|
||||||
assert_eq!(dropped, 0, "a fully-decrypted buffer must report no loss");
|
assert_eq!(dropped, 0, "a fully-decrypted buffer must report no loss");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-8
@@ -148,14 +148,15 @@ pub fn dvd_cell_row(idx: usize, cell: &crate::ifo::DvdCell, dropped: bool) -> St
|
|||||||
"keep(plain-feature)"
|
"keep(plain-feature)"
|
||||||
};
|
};
|
||||||
format!(
|
format!(
|
||||||
"tag=dvd.cell idx={idx} cat=0x{:02X} type={} block_mode={} block_type={} \
|
"tag=dvd.cell idx={idx} cat=0x{:02X} block_mode={} block_type={} \
|
||||||
seamless={} ilv={} plain={} first={} last={} dur={:.1}s {}",
|
seamless={} ilv={} stc={} angle={} plain={} first={} last={} dur={:.1}s {}",
|
||||||
cell.category,
|
cell.category,
|
||||||
c.cell_type,
|
|
||||||
c.block_mode,
|
c.block_mode,
|
||||||
c.block_type,
|
c.block_type,
|
||||||
c.seamless_play as u8,
|
c.seamless_play as u8,
|
||||||
c.interleaved as u8,
|
c.interleaved as u8,
|
||||||
|
c.stc_discontinuity as u8,
|
||||||
|
c.seamless_angle as u8,
|
||||||
c.is_plain_feature() as u8,
|
c.is_plain_feature() as u8,
|
||||||
cell.first_sector,
|
cell.first_sector,
|
||||||
cell.last_sector,
|
cell.last_sector,
|
||||||
@@ -726,23 +727,25 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let row = dvd_cell_row(0, &plain, false);
|
let row = dvd_cell_row(0, &plain, false);
|
||||||
assert!(row.contains("cat=0x00"), "{row}");
|
assert!(row.contains("cat=0x00"), "{row}");
|
||||||
assert!(row.contains("type=0"), "{row}");
|
assert!(row.contains("block_mode=0"), "{row}");
|
||||||
assert!(row.contains("first=100"), "{row}");
|
assert!(row.contains("first=100"), "{row}");
|
||||||
assert!(row.contains("last=199"), "{row}");
|
assert!(row.contains("last=199"), "{row}");
|
||||||
assert!(row.contains("dur=12.5s"), "{row}");
|
assert!(row.contains("dur=12.5s"), "{row}");
|
||||||
assert!(row.contains("keep(plain-feature)"), "{row}");
|
assert!(row.contains("keep(plain-feature)"), "{row}");
|
||||||
assert!(!row.contains("DROP"), "{row}");
|
assert!(!row.contains("DROP"), "{row}");
|
||||||
|
|
||||||
// 0x80 = middle-of-angle-block (cell_type=2), shown dropped.
|
// 0x90 = in-block cell of an angle block (block_mode=2, block_type=1),
|
||||||
|
// shown dropped as a leading secondary piece.
|
||||||
let sec = crate::ifo::DvdCell {
|
let sec = crate::ifo::DvdCell {
|
||||||
first_sector: 0,
|
first_sector: 0,
|
||||||
last_sector: 9,
|
last_sector: 9,
|
||||||
category: 0x80,
|
category: 0x90,
|
||||||
duration_secs: 1.0,
|
duration_secs: 1.0,
|
||||||
};
|
};
|
||||||
let row = dvd_cell_row(0, &sec, true);
|
let row = dvd_cell_row(0, &sec, true);
|
||||||
assert!(row.contains("cat=0x80"), "{row}");
|
assert!(row.contains("cat=0x90"), "{row}");
|
||||||
assert!(row.contains("type=2"), "{row}");
|
assert!(row.contains("block_mode=2"), "{row}");
|
||||||
|
assert!(row.contains("block_type=1"), "{row}");
|
||||||
assert!(row.contains("DROP(leading-secondary-block-piece)"), "{row}");
|
assert!(row.contains("DROP(leading-secondary-block-piece)"), "{row}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -1098,14 +1098,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// End-to-end bug-4 fix: a feature PGC that opens with a leading
|
/// End-to-end bug-4 fix: a feature PGC that opens with a leading
|
||||||
/// interleaved-angle sub-block cell (category 0x80 = middle-of-angle-block)
|
/// interleaved-angle sub-block cell (category 0x90 = in-block cell of an
|
||||||
/// must have that cell DROPPED from the muxed extents, so the rip starts at
|
/// angle block) must have that cell DROPPED from the muxed extents, so the
|
||||||
/// the real feature. Chapters shift earlier by the dropped duration.
|
/// rip starts at the real feature. Chapters shift earlier by the dropped duration.
|
||||||
#[test]
|
#[test]
|
||||||
fn scan_dvd_titles_drops_leading_scene_index_cell() {
|
fn scan_dvd_titles_drops_leading_scene_index_cell() {
|
||||||
let mut disc = MemDisc::new();
|
let mut disc = MemDisc::new();
|
||||||
let vmg = build_vmg(&[(2, 1, 1)]);
|
let vmg = build_vmg(&[(2, 1, 1)]);
|
||||||
// Cell 0: leading scene-index/angle sub-block (cat 0x80), 5s, sectors 0..9.
|
// Cell 0: leading scene-index/angle sub-block (cat 0x90), 5s, sectors 0..9.
|
||||||
// Cell 1: feature start (cat 0x00), 59s, sectors 100..199.
|
// Cell 1: feature start (cat 0x00), 59s, sectors 100..199.
|
||||||
// Cell 2: feature (cat 0x00), 59s, sectors 300..399.
|
// Cell 2: feature (cat 0x00), 59s, sectors 300..399.
|
||||||
// Programs: prog0 → cell 1 (feature start), prog1 → cell 3.
|
// Programs: prog0 → cell 1 (feature start), prog1 → cell 3.
|
||||||
@@ -1113,7 +1113,7 @@ mod tests {
|
|||||||
1000,
|
1000,
|
||||||
0x00,
|
0x00,
|
||||||
&[
|
&[
|
||||||
(0, 9, 0x80, 0x05),
|
(0, 9, 0x90, 0x05),
|
||||||
(100, 199, 0x00, 0x59),
|
(100, 199, 0x00, 0x59),
|
||||||
(300, 399, 0x00, 0x59),
|
(300, 399, 0x00, 0x59),
|
||||||
],
|
],
|
||||||
@@ -1137,7 +1137,7 @@ mod tests {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
let t = &Disc::scan_dvd_titles(&mut disc, &udf)[0];
|
let t = &Disc::scan_dvd_titles(&mut disc, &udf)[0];
|
||||||
// The leading 0x80 cell is dropped: 2 feature extents, not 3.
|
// The leading 0x90 cell is dropped: 2 feature extents, not 3.
|
||||||
assert_eq!(t.extents.len(), 2, "leading angle sub-block cell dropped");
|
assert_eq!(t.extents.len(), 2, "leading angle sub-block cell dropped");
|
||||||
// First extent starts at the feature cell (vob 1000 + 100), not at 1000+0.
|
// First extent starts at the feature cell (vob 1000 + 100), not at 1000+0.
|
||||||
assert_eq!(t.extents[0].start_lba, 1000 + 100);
|
assert_eq!(t.extents[0].start_lba, 1000 + 100);
|
||||||
|
|||||||
+68
-63
@@ -58,12 +58,12 @@ pub struct DvdTitle {
|
|||||||
pub struct DvdCell {
|
pub struct DvdCell {
|
||||||
pub first_sector: u32,
|
pub first_sector: u32,
|
||||||
pub last_sector: u32,
|
pub last_sector: u32,
|
||||||
/// Raw cell-category byte at `cell_playback + 0` (DVD-Video spec).
|
/// Raw cell-category byte at `cell_playback + 0` (libdvdread layout).
|
||||||
/// Packs cell_type (bits 7-6), block_mode (bits 5-4), block_type
|
/// Packs block_mode (bits 7-6), block_type (bits 5-4), seamless_play
|
||||||
/// (bits 3-2), seamless_play (bit 1), interleaved (bit 0). Carried so
|
/// (bit 3), interleaved (bit 2), stc_discontinuity (bit 1),
|
||||||
/// the extent builder can recognise non-feature leading cells
|
/// seamless_angle (bit 0). Carried so the extent builder can recognise
|
||||||
/// (scene-index / interleaved angle sub-blocks) and the diagnostic dump
|
/// non-feature leading cells (interleaved angle sub-blocks) and the
|
||||||
/// can show why a cell was kept or dropped.
|
/// diagnostic dump can show why a cell was kept or dropped.
|
||||||
pub category: u8,
|
pub category: u8,
|
||||||
/// Per-cell playback duration in seconds (BCD time at `cell_playback + 4`).
|
/// Per-cell playback duration in seconds (BCD time at `cell_playback + 4`).
|
||||||
/// Used by the diagnostic dump and the conservative leading-cell filter
|
/// Used by the diagnostic dump and the conservative leading-cell filter
|
||||||
@@ -72,51 +72,54 @@ pub struct DvdCell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decoded view of a cell-category byte (`cell_playback + 0`), per the
|
/// Decoded view of a cell-category byte (`cell_playback + 0`), per the
|
||||||
/// DVD-Video spec `cell_playback_information` layout.
|
/// DVD-Video spec / libdvdread `cell_playback_t` layout. Byte-0 bitfields,
|
||||||
|
/// MSB-first: `block_mode`(7-6), `block_type`(5-4), `seamless_play`(3),
|
||||||
|
/// `interleaved`(2), `stc_discontinuity`(1), `seamless_angle`(0). (The real
|
||||||
|
/// `cell_type` is a karaoke-only field in byte 1, not used here.)
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct CellCategory {
|
pub struct CellCategory {
|
||||||
/// bits 7-6: 0=normal, 1=first cell of angle block, 2=middle, 3=last.
|
/// bits 7-6: 0=not in block, 1=first cell of block, 2=in block, 3=last cell.
|
||||||
pub cell_type: u8,
|
|
||||||
/// bits 5-4: 0=not in block, 1=first cell of block, 2=in block, 3=last.
|
|
||||||
pub block_mode: u8,
|
pub block_mode: u8,
|
||||||
/// bits 3-2: 0=not part of a block, 1=angle block.
|
/// bits 5-4: 0=not part of a block, 1=angle block.
|
||||||
pub block_type: u8,
|
pub block_type: u8,
|
||||||
/// bit 1: seamless playback (STC continuous).
|
/// bit 3: seamless playback (STC continuous).
|
||||||
pub seamless_play: bool,
|
pub seamless_play: bool,
|
||||||
/// bit 0: interleaved (multi-angle / seamless-branch interleave).
|
/// bit 2: interleaved (multi-angle / seamless-branch interleave).
|
||||||
pub interleaved: bool,
|
pub interleaved: bool,
|
||||||
|
/// bit 1: STC discontinuity at the start of this cell.
|
||||||
|
pub stc_discontinuity: bool,
|
||||||
|
/// bit 0: seamless angle change.
|
||||||
|
pub seamless_angle: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CellCategory {
|
impl CellCategory {
|
||||||
/// Decode the raw `cell_playback + 0` byte.
|
/// Decode the raw `cell_playback + 0` byte (libdvdread `read_cell_playback`).
|
||||||
pub fn decode(raw: u8) -> Self {
|
pub fn decode(raw: u8) -> Self {
|
||||||
CellCategory {
|
CellCategory {
|
||||||
cell_type: (raw >> 6) & 0x03,
|
block_mode: (raw >> 6) & 0x03,
|
||||||
block_mode: (raw >> 4) & 0x03,
|
block_type: (raw >> 4) & 0x03,
|
||||||
block_type: (raw >> 2) & 0x03,
|
seamless_play: (raw & 0x08) != 0,
|
||||||
seamless_play: (raw & 0x02) != 0,
|
interleaved: (raw & 0x04) != 0,
|
||||||
interleaved: (raw & 0x01) != 0,
|
stc_discontinuity: (raw & 0x02) != 0,
|
||||||
|
seamless_angle: (raw & 0x01) != 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A plain feature cell: not part of any angle/interleave block. Every
|
/// A plain feature cell: not part of any angle/interleave block. Every cell
|
||||||
/// cell of a normal single-angle feature decodes to this (`category`
|
/// of a normal single-angle feature decodes to this (`block_mode` and
|
||||||
/// byte `0x00`, or `0x00` in every block field with only the
|
/// `block_type` both 0, only the seamless/interleaved flags possibly set).
|
||||||
/// seamless/interleaved flags possibly set). Such a cell is NEVER
|
/// Such a cell is NEVER dropped by the leading-cell filter.
|
||||||
/// dropped by the leading-cell filter.
|
|
||||||
pub fn is_plain_feature(&self) -> bool {
|
pub fn is_plain_feature(&self) -> bool {
|
||||||
self.cell_type == 0 && self.block_mode == 0 && self.block_type == 0
|
self.block_mode == 0 && self.block_type == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marks a non-first piece of an angle / interleaved block: a "middle" or
|
/// Marks a non-first piece of an angle block: an "in-block" or "last of
|
||||||
/// "last" cell of an angle block (`cell_type ∈ {2,3}`), or an
|
/// block" cell (`block_mode ∈ {2,3}`) of an angle block (`block_type==1`).
|
||||||
/// in-block / last-of-block cell (`block_mode ∈ {2,3}`). Concatenating
|
/// Concatenating these back-to-back with the first angle duplicates content
|
||||||
/// these back-to-back with the first angle duplicates content at the head
|
/// at the head of the feature. Conservative: the FIRST cell of a block
|
||||||
/// of the feature. Conservative: the FIRST cell of a block
|
/// (`block_mode==1`) is NOT flagged — it is the angle we keep.
|
||||||
/// (`cell_type==1` / `block_mode==1`) is NOT flagged — it is the angle we
|
|
||||||
/// keep.
|
|
||||||
pub fn is_secondary_block_piece(&self) -> bool {
|
pub fn is_secondary_block_piece(&self) -> bool {
|
||||||
matches!(self.cell_type, 2 | 3) || matches!(self.block_mode, 2 | 3)
|
self.block_type == 1 && matches!(self.block_mode, 2 | 3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,7 +570,7 @@ fn parse_audio_attr(data: &[u8], offset: usize) -> Result<DvdAudioAttr> {
|
|||||||
_ => Codec::Unknown(coding_mode),
|
_ => Codec::Unknown(coding_mode),
|
||||||
};
|
};
|
||||||
|
|
||||||
let sample_rate_flag = (b0 >> 3) & 0x03;
|
let sample_rate_flag = (b1 >> 4) & 0x03; // sample_frequency: byte 1 bits 5-4 (libdvdread audio_attr_t)
|
||||||
let sample_rate = match sample_rate_flag {
|
let sample_rate = match sample_rate_flag {
|
||||||
0 => 48000,
|
0 => 48000,
|
||||||
1 => 96000,
|
1 => 96000,
|
||||||
@@ -1174,11 +1177,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn audio_attr_dts() {
|
fn audio_attr_dts() {
|
||||||
let mut data = vec![0u8; 16];
|
let mut data = vec![0u8; 16];
|
||||||
// DTS (coding=6), 96kHz (rate=1), 2 channels (stored as 1)
|
// DTS (coding=6), 96kHz (rate=1, byte1 bits 5-4), 2 channels (stored as 1)
|
||||||
// b0: bits 7-5=110(DTS), bits 4-3=01(96k) => 0b110_01_000 = 0xC8
|
// b0: bits 7-5=110(DTS) => 0b110_00000 = 0xC0
|
||||||
data[0] = 0xC8;
|
data[0] = 0xC0;
|
||||||
// b1: bits 2-0=001 (channels-1=1) => 0x01
|
// b1: bits 5-4=01(96k), bits 2-0=001(channels-1=1) => 0b00_01_0_001 = 0x11
|
||||||
data[1] = 0x01;
|
data[1] = 0x11;
|
||||||
data[2] = b'f';
|
data[2] = b'f';
|
||||||
data[3] = b'r';
|
data[3] = b'r';
|
||||||
|
|
||||||
@@ -1546,13 +1549,13 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CellCategory decodes the spec bitfields: cell_type (7-6), block_mode
|
/// CellCategory decodes the libdvdread byte-0 bitfields: block_mode (7-6),
|
||||||
/// (5-4), block_type (3-2), seamless (1), interleaved (0).
|
/// block_type (5-4), seamless_play (3), interleaved (2),
|
||||||
|
/// stc_discontinuity (1), seamless_angle (0).
|
||||||
#[test]
|
#[test]
|
||||||
fn cell_category_decode_bits() {
|
fn cell_category_decode_bits() {
|
||||||
// 0x00 → plain feature, nothing set.
|
// 0x00 → plain feature, nothing set.
|
||||||
let c = CellCategory::decode(0x00);
|
let c = CellCategory::decode(0x00);
|
||||||
assert_eq!(c.cell_type, 0);
|
|
||||||
assert_eq!(c.block_mode, 0);
|
assert_eq!(c.block_mode, 0);
|
||||||
assert_eq!(c.block_type, 0);
|
assert_eq!(c.block_type, 0);
|
||||||
assert!(!c.seamless_play);
|
assert!(!c.seamless_play);
|
||||||
@@ -1560,27 +1563,29 @@ mod tests {
|
|||||||
assert!(c.is_plain_feature());
|
assert!(c.is_plain_feature());
|
||||||
assert!(!c.is_secondary_block_piece());
|
assert!(!c.is_secondary_block_piece());
|
||||||
|
|
||||||
// cell_type=1 (first of angle block), block_mode=1 (first of block):
|
// block_mode=1 (first cell of block), block_type=1 (angle block):
|
||||||
// 0b01_01_00_0_0 = 0x50. This is the angle we KEEP — not secondary.
|
// 0b01_01_0000 = 0x50. This is the angle we KEEP — not secondary.
|
||||||
let c = CellCategory::decode(0b01_01_00_00);
|
let c = CellCategory::decode(0b01_01_0000);
|
||||||
assert_eq!(c.cell_type, 1);
|
|
||||||
assert_eq!(c.block_mode, 1);
|
assert_eq!(c.block_mode, 1);
|
||||||
|
assert_eq!(c.block_type, 1);
|
||||||
assert!(!c.is_plain_feature());
|
assert!(!c.is_plain_feature());
|
||||||
assert!(!c.is_secondary_block_piece());
|
assert!(!c.is_secondary_block_piece());
|
||||||
|
|
||||||
// cell_type=2 (middle of angle block): 0b10_00_00_00 = 0x80 → secondary.
|
// block_mode=2 (in block) / 3 (last of block) of an angle block
|
||||||
assert!(CellCategory::decode(0b10_00_00_00).is_secondary_block_piece());
|
// (block_type=1) → secondary.
|
||||||
// cell_type=3 (last of angle block) → secondary.
|
assert!(CellCategory::decode(0b10_01_0000).is_secondary_block_piece());
|
||||||
assert!(CellCategory::decode(0b11_00_00_00).is_secondary_block_piece());
|
assert!(CellCategory::decode(0b11_01_0000).is_secondary_block_piece());
|
||||||
// block_mode=2 (in block) → secondary; block_mode=3 (last of block) → secondary.
|
// First cell of the block (block_mode=1) is NEVER secondary.
|
||||||
assert!(CellCategory::decode(0b00_10_00_00).is_secondary_block_piece());
|
assert!(!CellCategory::decode(0b01_01_0000).is_secondary_block_piece());
|
||||||
assert!(CellCategory::decode(0b00_11_00_00).is_secondary_block_piece());
|
|
||||||
|
|
||||||
// seamless (bit1) + interleaved (bit0) on an otherwise-plain cell must
|
// The low flags (seamless_play bit3, interleaved bit2, stc bit1,
|
||||||
// NOT make it secondary — they don't mark non-feature content.
|
// seamless_angle bit0) on an otherwise-plain cell must NOT make it
|
||||||
let c = CellCategory::decode(0b00_00_00_11);
|
// secondary — they don't mark non-feature content.
|
||||||
|
let c = CellCategory::decode(0b0000_1111);
|
||||||
assert!(c.seamless_play);
|
assert!(c.seamless_play);
|
||||||
assert!(c.interleaved);
|
assert!(c.interleaved);
|
||||||
|
assert!(c.stc_discontinuity);
|
||||||
|
assert!(c.seamless_angle);
|
||||||
assert!(c.is_plain_feature());
|
assert!(c.is_plain_feature());
|
||||||
assert!(!c.is_secondary_block_piece());
|
assert!(!c.is_secondary_block_piece());
|
||||||
}
|
}
|
||||||
@@ -1614,9 +1619,9 @@ mod tests {
|
|||||||
chapters: 2,
|
chapters: 2,
|
||||||
duration_secs: 100.0,
|
duration_secs: 100.0,
|
||||||
cells: vec![
|
cells: vec![
|
||||||
cell(0, 9, 0b10_00_00_00), // middle of angle block → drop
|
cell(0, 9, 0b10_01_0000), // in-block cell of angle block → drop
|
||||||
cell(10, 19, 0b00_11_00_00), // last of block → drop
|
cell(10, 19, 0b11_01_0000), // last cell of angle block → drop
|
||||||
cell(20, 119, 0x00), // feature starts here
|
cell(20, 119, 0x00), // feature starts here
|
||||||
cell(120, 219, 0x00),
|
cell(120, 219, 0x00),
|
||||||
],
|
],
|
||||||
chapter_times: vec![0.0, 50.0],
|
chapter_times: vec![0.0, 50.0],
|
||||||
@@ -1637,7 +1642,7 @@ mod tests {
|
|||||||
let t = DvdTitle {
|
let t = DvdTitle {
|
||||||
chapters: 1,
|
chapters: 1,
|
||||||
duration_secs: 100.0,
|
duration_secs: 100.0,
|
||||||
cells: vec![cell(0, 9, 0b10_00_00_00), cell(10, 19, 0b11_00_00_00)],
|
cells: vec![cell(0, 9, 0b10_01_0000), cell(10, 19, 0b11_01_0000)],
|
||||||
chapter_times: vec![0.0],
|
chapter_times: vec![0.0],
|
||||||
palette: None,
|
palette: None,
|
||||||
};
|
};
|
||||||
@@ -1669,8 +1674,8 @@ mod tests {
|
|||||||
pgc[0xE8] = 0x00;
|
pgc[0xE8] = 0x00;
|
||||||
pgc[0xE9] = 0xEA;
|
pgc[0xE9] = 0xEA;
|
||||||
pgc.resize(0xEA + 48, 0);
|
pgc.resize(0xEA + 48, 0);
|
||||||
// Cell 0: category byte = 0x80 (middle of angle block), 5s BCD.
|
// Cell 0: category byte = 0x90 (in-block cell of angle block), 5s BCD.
|
||||||
pgc[0xEA] = 0x80;
|
pgc[0xEA] = 0x90;
|
||||||
pgc[0xEA + 6] = 0x05;
|
pgc[0xEA + 6] = 0x05;
|
||||||
pgc[0xEA + 8..0xEA + 12].copy_from_slice(&10u32.to_be_bytes());
|
pgc[0xEA + 8..0xEA + 12].copy_from_slice(&10u32.to_be_bytes());
|
||||||
// Cell 1: category 0x00 (plain feature), 7s BCD.
|
// Cell 1: category 0x00 (plain feature), 7s BCD.
|
||||||
@@ -1678,7 +1683,7 @@ mod tests {
|
|||||||
pgc[0xEA + 24 + 6] = 0x07;
|
pgc[0xEA + 24 + 6] = 0x07;
|
||||||
pgc[0xEA + 24 + 8..0xEA + 24 + 12].copy_from_slice(&20u32.to_be_bytes());
|
pgc[0xEA + 24 + 8..0xEA + 24 + 12].copy_from_slice(&20u32.to_be_bytes());
|
||||||
let title = parse_pgc(&pgc, 0, 2).unwrap();
|
let title = parse_pgc(&pgc, 0, 2).unwrap();
|
||||||
assert_eq!(title.cells[0].category, 0x80);
|
assert_eq!(title.cells[0].category, 0x90);
|
||||||
assert!((title.cells[0].duration_secs - 5.0).abs() < 0.01);
|
assert!((title.cells[0].duration_secs - 5.0).abs() < 0.01);
|
||||||
assert_eq!(title.cells[1].category, 0x00);
|
assert_eq!(title.cells[1].category, 0x00);
|
||||||
assert!((title.cells[1].duration_secs - 7.0).abs() < 0.01);
|
assert!((title.cells[1].duration_secs - 7.0).abs() < 0.01);
|
||||||
|
|||||||
+363
-160
@@ -101,32 +101,39 @@ pub struct Mpeg2Parser {
|
|||||||
/// `(absolute ES offset of a PES's first byte, PTS in ns)` for every PES
|
/// `(absolute ES offset of a PES's first byte, PTS in ns)` for every PES
|
||||||
/// that carried a timestamp, in ascending offset order.
|
/// that carried a timestamp, in ascending offset order.
|
||||||
pts_marks: VecDeque<(u64, i64)>,
|
pts_marks: VecDeque<(u64, i64)>,
|
||||||
/// Per-frame presentation interval (ns), derived from the sequence header
|
/// Full-frame presentation interval (ns) at the sequence-header display rate
|
||||||
/// frame rate. DVD stamps a PTS only ~once per VOBU (every ~0.5 s), so
|
/// (`1/frame_rate`). The field period is half this. Per-frame durations are
|
||||||
/// frames between marks must be timed by `temporal_reference` × this
|
/// `nb_fields × field_period`, so 2:3-telecined frames alternate 2- and
|
||||||
/// interval. 0 until a sequence header with a valid frame rate is seen.
|
/// 3-field durations. 0 until a sequence header with a valid frame rate.
|
||||||
frame_duration_ns: i64,
|
frame_duration_ns: i64,
|
||||||
/// Cumulative count of coded pictures emitted in all GOPs before the
|
/// `progressive_sequence` from the sequence extension — selects the
|
||||||
/// current one. `temporal_reference` is GOP-relative (display order within
|
/// `nb_fields` rules for `repeat_first_field` pictures.
|
||||||
/// the GOP); adding this base makes a whole-stream display index.
|
progressive_sequence: bool,
|
||||||
gop_base: u64,
|
/// Pictures of the current GOP, buffered in DECODE order until the GOP
|
||||||
/// Coded pictures emitted in the current GOP so far (folded into
|
/// completes (the next GOP/sequence header). Held so each frame's PTS can be
|
||||||
/// `gop_base` at the next GOP boundary).
|
/// the display-order prefix-sum of field durations — exact for 2:3 pulldown
|
||||||
gop_count: u64,
|
/// without ever reordering emitted blocks (B-frames keep decode order; only
|
||||||
/// Display index of the last frame that carried an explicit PES PTS, used
|
/// their PTS is lower).
|
||||||
/// to anchor interpolated timestamps to the real disc timeline (so video
|
gop_buf: Vec<BufferedPicture>,
|
||||||
/// stays in sync with the PES-timestamped audio tracks).
|
/// Total field-display periods of all frames already emitted, in display
|
||||||
anchor_index: Option<u64>,
|
/// order — the running base for each new frame's display time.
|
||||||
/// PTS (ns) of the anchor frame.
|
emitted_fields: u64,
|
||||||
anchor_pts: i64,
|
/// PTS (ns) that display-field 0 of the whole stream maps to. Re-locked from
|
||||||
/// Frames emitted before the first PES PTS anchor is known, held with their
|
/// each GOP's first PES PTS so video stays in sync with the PES-timestamped
|
||||||
/// display index. A DVD title can open with a still-frame/first-play
|
/// audio. None until the first PES timestamp is seen.
|
||||||
/// sequence whose PTS lands a few frames in; buffering until the anchor lets
|
origin_pts_ns: Option<i64>,
|
||||||
/// those leading frames take the disc's real timeline instead of a 0 base.
|
}
|
||||||
pending: Vec<(u64, Frame)>,
|
|
||||||
/// Accumulated `data.len()` of frames currently in `pending`. Bounds the
|
/// One coded picture buffered awaiting its GOP's completion (see `gop_buf`).
|
||||||
/// pre-anchor hold by BYTES, not just frame count (see [`MAX_PENDING_BYTES`]).
|
struct BufferedPicture {
|
||||||
pending_bytes: usize,
|
/// `temporal_reference` — display order within the GOP.
|
||||||
|
tr: u64,
|
||||||
|
/// Field-display periods this picture occupies (`picture_nb_fields`).
|
||||||
|
nb_fields: u8,
|
||||||
|
/// This picture's own PES PTS (ns), if its access unit carried one.
|
||||||
|
explicit_pts: Option<i64>,
|
||||||
|
/// The emitted frame (PTS + duration filled in at GOP flush).
|
||||||
|
frame: Frame,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Mpeg2Parser {
|
impl Default for Mpeg2Parser {
|
||||||
@@ -144,12 +151,10 @@ impl Mpeg2Parser {
|
|||||||
base_offset: 0,
|
base_offset: 0,
|
||||||
pts_marks: VecDeque::new(),
|
pts_marks: VecDeque::new(),
|
||||||
frame_duration_ns: 0,
|
frame_duration_ns: 0,
|
||||||
gop_base: 0,
|
progressive_sequence: false,
|
||||||
gop_count: 0,
|
gop_buf: Vec::new(),
|
||||||
anchor_index: None,
|
emitted_fields: 0,
|
||||||
anchor_pts: 0,
|
origin_pts_ns: None,
|
||||||
pending: Vec::new(),
|
|
||||||
pending_bytes: 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,22 +179,6 @@ impl Mpeg2Parser {
|
|||||||
parse_aspect_ratio(hdr)
|
parse_aspect_ratio(hdr)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The PTS (ns) to assign to an access unit whose first relevant byte is at
|
|
||||||
/// absolute ES offset `target`: the most recent PES timestamp at or before
|
|
||||||
/// that offset (the PES that contains the access unit's start). Falls back
|
|
||||||
/// to 0 when no timestamp has been seen yet.
|
|
||||||
fn pts_for(&self, target: u64) -> i64 {
|
|
||||||
let mut best = 0;
|
|
||||||
for &(off, pts) in &self.pts_marks {
|
|
||||||
if off <= target {
|
|
||||||
best = pts;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
best
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Drain every complete access unit from `buf`, returning one Frame each.
|
/// Drain every complete access unit from `buf`, returning one Frame each.
|
||||||
/// When `force` is true (EOF flush, or buffer-cap backstop) the trailing
|
/// When `force` is true (EOF flush, or buffer-cap backstop) the trailing
|
||||||
/// in-progress access unit is emitted even without a following boundary.
|
/// in-progress access unit is emitted even without a following boundary.
|
||||||
@@ -247,12 +236,12 @@ impl Mpeg2Parser {
|
|||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
let pic_abs = self.base_offset + pic as u64;
|
|
||||||
let end_abs = self.base_offset + end as u64;
|
let end_abs = self.base_offset + end as u64;
|
||||||
let data = self.buf[..end].to_vec();
|
let data = self.buf[..end].to_vec();
|
||||||
|
|
||||||
// Phase 2 — mutate self.
|
// Phase 2 — mutate self.
|
||||||
if let Some(h) = hdr {
|
if let Some(h) = hdr {
|
||||||
|
self.progressive_sequence = parse_progressive_sequence(&h);
|
||||||
self.seq_header = Some(h);
|
self.seq_header = Some(h);
|
||||||
if let Some((num, den)) = self.frame_rate() {
|
if let Some((num, den)) = self.frame_rate() {
|
||||||
if num > 0 {
|
if num > 0 {
|
||||||
@@ -260,11 +249,7 @@ impl Mpeg2Parser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if gop_boundary && self.gop_count > 0 {
|
let nb_fields = picture_nb_fields(&data, self.progressive_sequence);
|
||||||
self.gop_base += self.gop_count;
|
|
||||||
self.gop_count = 0;
|
|
||||||
}
|
|
||||||
let display_index = self.gop_base + tr;
|
|
||||||
|
|
||||||
// An explicit PES PTS for this access unit, if any. By the mark-drain
|
// An explicit PES PTS for this access unit, if any. By the mark-drain
|
||||||
// invariant the front mark's offset is >= this AU's start, so a front
|
// invariant the front mark's offset is >= this AU's start, so a front
|
||||||
@@ -275,71 +260,29 @@ impl Mpeg2Parser {
|
|||||||
.filter(|&&(off, _)| off < end_abs)
|
.filter(|&&(off, _)| off < end_abs)
|
||||||
.map(|&(_, p)| p);
|
.map(|&(_, p)| p);
|
||||||
|
|
||||||
let duration_ns = (self.frame_duration_ns > 0).then_some(self.frame_duration_ns as u64);
|
// A GOP boundary means the buffered run is a COMPLETE GOP (all its
|
||||||
let mut frame = Frame {
|
// pictures display before the next GOP's), so flush it before
|
||||||
pts_ns: 0,
|
// starting the new one. `temporal_reference` resets to 0 at the
|
||||||
keyframe,
|
// boundary, keeping each GOP's display order self-contained.
|
||||||
data,
|
if gop_boundary && !self.gop_buf.is_empty() {
|
||||||
duration_ns,
|
self.flush_gop(&mut out);
|
||||||
};
|
}
|
||||||
|
self.gop_buf.push(BufferedPicture {
|
||||||
if self.frame_duration_ns > 0 {
|
tr,
|
||||||
// Reconstruct from display order; anchor to the real PES PTS so
|
nb_fields,
|
||||||
// video stays in sync with the PES-timestamped audio.
|
explicit_pts: explicit,
|
||||||
match explicit {
|
frame: Frame {
|
||||||
Some(p) => {
|
pts_ns: 0,
|
||||||
self.anchor_index = Some(display_index);
|
keyframe,
|
||||||
self.anchor_pts = p;
|
data,
|
||||||
// Backfill any leading frames held before the anchor was
|
duration_ns: None,
|
||||||
// known (still-frame / first-play opening): give each the
|
},
|
||||||
// disc's real timeline relative to this anchor.
|
});
|
||||||
for (di, mut held) in self.pending.drain(..) {
|
// Safety cap: a stream with no GOP/sequence boundaries would buffer
|
||||||
held.pts_ns =
|
// unbounded. Force-flush a pathologically long run as its own GOP.
|
||||||
p + (di as i64 - display_index as i64) * self.frame_duration_ns;
|
if self.gop_buf.len() >= MAX_PENDING_FRAMES {
|
||||||
out.push(held);
|
self.flush_gop(&mut out);
|
||||||
}
|
|
||||||
self.pending_bytes = 0;
|
|
||||||
frame.pts_ns = p;
|
|
||||||
out.push(frame);
|
|
||||||
}
|
|
||||||
None => match self.anchor_index {
|
|
||||||
Some(ai) => {
|
|
||||||
frame.pts_ns = self.anchor_pts
|
|
||||||
+ (display_index as i64 - ai as i64) * self.frame_duration_ns;
|
|
||||||
out.push(frame);
|
|
||||||
}
|
|
||||||
None if self.pending.len() < MAX_PENDING_FRAMES
|
|
||||||
&& self.pending_bytes < MAX_PENDING_BYTES =>
|
|
||||||
{
|
|
||||||
// No anchor yet — hold so leading frames get the
|
|
||||||
// disc's real timeline once the first PTS arrives,
|
|
||||||
// not a 0 base.
|
|
||||||
self.pending_bytes += frame.data.len();
|
|
||||||
self.pending.push((display_index, frame));
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// Hold cap (count OR bytes) reached without a PTS
|
|
||||||
// anchor ever arriving. Release everything held so
|
|
||||||
// far on the 0-base timeline rather than growing the
|
|
||||||
// buffer unbounded, then emit this frame the same way.
|
|
||||||
for (di, mut held) in self.pending.drain(..) {
|
|
||||||
held.pts_ns = di as i64 * self.frame_duration_ns;
|
|
||||||
out.push(held);
|
|
||||||
}
|
|
||||||
self.pending_bytes = 0;
|
|
||||||
frame.pts_ns = display_index as i64 * self.frame_duration_ns;
|
|
||||||
out.push(frame);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No frame rate yet (no sequence header) — fall back to the
|
|
||||||
// nearest preceding PES timestamp.
|
|
||||||
frame.pts_ns = self.pts_for(pic_abs);
|
|
||||||
out.push(frame);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.gop_count += 1;
|
|
||||||
self.buf.drain(..end);
|
self.buf.drain(..end);
|
||||||
self.base_offset = end_abs;
|
self.base_offset = end_abs;
|
||||||
// Drop PTS marks fully consumed by the emitted AU; keep the mark at
|
// Drop PTS marks fully consumed by the emitted AU; keep the mark at
|
||||||
@@ -352,8 +295,64 @@ impl Mpeg2Parser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// EOF: emit the final (possibly incomplete) GOP so nothing is dropped.
|
||||||
|
if force {
|
||||||
|
self.flush_gop(&mut out);
|
||||||
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Emit the buffered GOP. Each frame's PTS is the display-order prefix-sum of
|
||||||
|
/// field durations from the timeline origin; its block duration is its own
|
||||||
|
/// `nb_fields × field_period`. Frames are emitted in DECODE (buffer) order —
|
||||||
|
/// B-frames keep their position with a correctly LOWER PTS, never reordered
|
||||||
|
/// (reordering emitted blocks is what corrupts the picture). The origin is
|
||||||
|
/// (re-)locked to the GOP's PES PTS; because that is a *presentation*
|
||||||
|
/// timestamp, backing out the carrying frame's display-field offset keeps the
|
||||||
|
/// timeline continuous and monotonic across GOP boundaries.
|
||||||
|
fn flush_gop(&mut self, out: &mut Vec<Frame>) {
|
||||||
|
let n = self.gop_buf.len();
|
||||||
|
if n == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let field_period = self.frame_duration_ns / 2;
|
||||||
|
if field_period <= 0 {
|
||||||
|
// No sequence header / frame rate yet (malformed lead-in): emit in
|
||||||
|
// decode order off each AU's own PES PTS, with no field timing.
|
||||||
|
for bp in self.gop_buf.drain(..) {
|
||||||
|
let mut f = bp.frame;
|
||||||
|
f.pts_ns = bp.explicit_pts.unwrap_or(0);
|
||||||
|
out.push(f);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Fields displayed BEFORE each picture within this GOP: order indices by
|
||||||
|
// temporal_reference (display order) and prefix-sum `nb_fields`.
|
||||||
|
let mut order: Vec<usize> = (0..n).collect();
|
||||||
|
order.sort_by_key(|&i| self.gop_buf[i].tr);
|
||||||
|
let mut cum_before = vec![0u64; n];
|
||||||
|
let mut running = 0u64;
|
||||||
|
for &i in &order {
|
||||||
|
cum_before[i] = running;
|
||||||
|
running += self.gop_buf[i].nb_fields as u64;
|
||||||
|
}
|
||||||
|
let gop_fields = running;
|
||||||
|
let base = self.emitted_fields;
|
||||||
|
// (Re-)lock the timeline origin to the GOP's PES PTS.
|
||||||
|
for &i in &order {
|
||||||
|
if let Some(p) = self.gop_buf[i].explicit_pts {
|
||||||
|
self.origin_pts_ns = Some(p - field_period * (base + cum_before[i]) as i64);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let origin = self.origin_pts_ns.unwrap_or(0);
|
||||||
|
for (i, mut bp) in self.gop_buf.drain(..).enumerate() {
|
||||||
|
bp.frame.pts_ns = origin + field_period * (base + cum_before[i]) as i64;
|
||||||
|
bp.frame.duration_ns = Some(bp.nb_fields as u64 * field_period as u64);
|
||||||
|
out.push(bp.frame);
|
||||||
|
}
|
||||||
|
self.emitted_fields += gop_fields;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CodecParser for Mpeg2Parser {
|
impl CodecParser for Mpeg2Parser {
|
||||||
@@ -374,23 +373,9 @@ impl CodecParser for Mpeg2Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn flush(&mut self) -> Vec<Frame> {
|
fn flush(&mut self) -> Vec<Frame> {
|
||||||
let mut out = self.drain_complete_aus(true);
|
// drain_complete_aus(true) force-completes the trailing access unit and
|
||||||
// EOF: if no PES ever supplied a PTS/DTS, `self.pending` still holds the
|
// flushes the final GOP, so nothing is left buffered at EOF.
|
||||||
// frames buffered while waiting for an anchor (the opening keyframe +
|
self.drain_complete_aus(true)
|
||||||
// first ~20s). Without this they'd be silently dropped — a 100%-recovery
|
|
||||||
// violation. Emit each with the same 0-base fallback the no-anchor
|
|
||||||
// overflow arm uses (`display_index * frame_duration_ns`), ordered by
|
|
||||||
// display_index so presentation order is preserved.
|
|
||||||
if !self.pending.is_empty() {
|
|
||||||
let mut held: Vec<(u64, Frame)> = self.pending.drain(..).collect();
|
|
||||||
held.sort_by_key(|(di, _)| *di);
|
|
||||||
for (di, mut frame) in held {
|
|
||||||
frame.pts_ns = di as i64 * self.frame_duration_ns;
|
|
||||||
out.push(frame);
|
|
||||||
}
|
|
||||||
self.pending_bytes = 0;
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn codec_private(&self) -> Option<Vec<u8>> {
|
fn codec_private(&self) -> Option<Vec<u8>> {
|
||||||
@@ -493,11 +478,145 @@ fn parse_aspect_ratio(hdr: &[u8]) -> Option<(u8, u8)> {
|
|||||||
Some(ASPECT_RATIOS[ar_code])
|
Some(ASPECT_RATIOS[ar_code])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Number of field-display periods a coded picture occupies, from its picture
|
||||||
|
/// coding extension (`00 00 01 B5`, ext-id `1000`), per ISO/IEC 13818-2 §6.3.10
|
||||||
|
/// and ffmpeg `mpeg_field_start` (`nb_fields = repeat_pict + 2`). This is what
|
||||||
|
/// times soft-telecined (2:3 pulldown) DVD video correctly: a
|
||||||
|
/// `repeat_first_field` frame occupies 3 fields, a normal frame 2, so honoring
|
||||||
|
/// it spreads the ~23.976 coded frames across the 29.97 display span with no
|
||||||
|
/// gap (the "play, pause, play" judder). `progressive_sequence` comes from the
|
||||||
|
/// sequence extension. Returns 2 (a normal frame) when no picture coding
|
||||||
|
/// extension is present.
|
||||||
|
fn picture_nb_fields(au: &[u8], progressive_sequence: bool) -> u8 {
|
||||||
|
let mut search = 0;
|
||||||
|
while let Some(q) = find_code(au, search, SEQ_EXT_CODE) {
|
||||||
|
search = q + 4;
|
||||||
|
// The picture coding extension is the B5 whose ext-id nibble is 1000.
|
||||||
|
if au.get(q + 4).map(|b| b >> 4) != Some(0b1000) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Extension bytes e2..=e4 = au[q+6 ..= q+8].
|
||||||
|
let (Some(&e2), Some(&e3), Some(&e4)) = (au.get(q + 6), au.get(q + 7), au.get(q + 8))
|
||||||
|
else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
// picture_structure (e2 bits 1-0): 11 = frame picture. A field picture
|
||||||
|
// (01/10) occupies a single field; two combine into one frame upstream.
|
||||||
|
if e2 & 0x03 != 0b11 {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
let tff = (e3 >> 7) & 1;
|
||||||
|
let rff = (e3 >> 1) & 1;
|
||||||
|
let progressive_frame = (e4 >> 7) & 1;
|
||||||
|
let repeat_pict = if rff == 0 {
|
||||||
|
0
|
||||||
|
} else if progressive_sequence {
|
||||||
|
if tff == 1 { 4 } else { 2 }
|
||||||
|
} else if progressive_frame == 1 {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
return repeat_pict + 2;
|
||||||
|
}
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read `progressive_sequence` from a captured sequence header's sequence
|
||||||
|
/// extension (`00 00 01 B5`, ext-id `0001`). False when absent (MPEG-1 / no
|
||||||
|
/// extension) — the interlaced default. Bit layout after the start code:
|
||||||
|
/// ext-id(4) profile_and_level(8) **progressive_sequence(1)** … so it is bit 3
|
||||||
|
/// of the second extension byte (`hdr[q+5]`).
|
||||||
|
fn parse_progressive_sequence(hdr: &[u8]) -> bool {
|
||||||
|
let mut search = 0;
|
||||||
|
while let Some(q) = find_code(hdr, search, SEQ_EXT_CODE) {
|
||||||
|
search = q + 4;
|
||||||
|
if hdr.get(q + 4).map(|b| b >> 4) != Some(0b0001) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return hdr.get(q + 5).map(|&b| (b >> 3) & 1 == 1).unwrap_or(false);
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::mux::ts::PesPacket;
|
use crate::mux::ts::PesPacket;
|
||||||
|
|
||||||
|
/// Build a picture coding extension (`00 00 01 B5`, ext-id 1000) carrying the
|
||||||
|
/// given pulldown flags, for `picture_nb_fields` tests.
|
||||||
|
fn pic_coding_ext(tff: u8, rff: u8, progressive_frame: u8, frame_picture: bool) -> Vec<u8> {
|
||||||
|
let e0 = 0x80; // ext-id 1000, f_code high nibble 0
|
||||||
|
let e1 = 0x00;
|
||||||
|
let e2 = if frame_picture { 0x03 } else { 0x01 }; // picture_structure bits 1-0
|
||||||
|
let e3 = (tff << 7) | (rff << 1);
|
||||||
|
let e4 = progressive_frame << 7;
|
||||||
|
vec![0x00, 0x00, 0x01, SEQ_EXT_CODE, e0, e1, e2, e3, e4]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nb_fields_normal_frame_is_two() {
|
||||||
|
assert_eq!(picture_nb_fields(&pic_coding_ext(0, 0, 0, true), false), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nb_fields_telecine_repeat_field_is_three() {
|
||||||
|
// NTSC 2:3 soft telecine: interlaced sequence, progressive frame, rff=1.
|
||||||
|
assert_eq!(picture_nb_fields(&pic_coding_ext(0, 1, 1, true), false), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nb_fields_field_picture_is_one() {
|
||||||
|
assert_eq!(picture_nb_fields(&pic_coding_ext(0, 0, 0, false), false), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nb_fields_progressive_seq_rff_tff_is_six() {
|
||||||
|
assert_eq!(picture_nb_fields(&pic_coding_ext(1, 1, 0, true), true), 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nb_fields_progressive_seq_rff_no_tff_is_four() {
|
||||||
|
assert_eq!(picture_nb_fields(&pic_coding_ext(0, 1, 0, true), true), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nb_fields_no_picture_ext_defaults_two() {
|
||||||
|
// A picture header with no coding extension → assume a normal 2-field frame.
|
||||||
|
assert_eq!(picture_nb_fields(&[0, 0, 1, 0x00, 0, 0], false), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn progressive_sequence_parsed_from_seq_ext() {
|
||||||
|
// Sequence extension: 00 00 01 B5, e0 ext-id 0001 (0x1_), e1 bit3 = progressive_sequence.
|
||||||
|
assert!(parse_progressive_sequence(&[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
SEQ_EXT_CODE,
|
||||||
|
0x10,
|
||||||
|
0x08
|
||||||
|
]));
|
||||||
|
assert!(!parse_progressive_sequence(&[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
SEQ_EXT_CODE,
|
||||||
|
0x10,
|
||||||
|
0x00
|
||||||
|
]));
|
||||||
|
// No sequence extension at all → interlaced default (false).
|
||||||
|
assert!(!parse_progressive_sequence(&[
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
SEQ_HEADER_CODE,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
|
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
|
||||||
PesPacket {
|
PesPacket {
|
||||||
pid: 0x1011,
|
pid: 0x1011,
|
||||||
@@ -651,9 +770,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn two_pictures_emit_two_frames_at_the_boundary() {
|
fn two_pictures_in_one_gop_emit_both_on_flush() {
|
||||||
// pic1's frame is emitted as soon as pic2's start code is seen; pic2 on
|
// Two pictures with no GOP/sequence boundary between them are ONE GOP.
|
||||||
// flush. Each frame contains exactly its own picture.
|
// The VFR timeline needs the whole GOP (a P-frame's PTS depends on its
|
||||||
|
// later B-frames), so they buffer until the GOP closes / EOF, then emit
|
||||||
|
// in DECODE order, each containing exactly its own picture.
|
||||||
let mut parser = Mpeg2Parser::new();
|
let mut parser = Mpeg2Parser::new();
|
||||||
|
|
||||||
let mut pic1 = make_picture_header(PICTURE_TYPE_I);
|
let mut pic1 = make_picture_header(PICTURE_TYPE_I);
|
||||||
@@ -664,17 +785,13 @@ mod tests {
|
|||||||
let mut stream = pic1.clone();
|
let mut stream = pic1.clone();
|
||||||
stream.extend_from_slice(&pic2);
|
stream.extend_from_slice(&pic2);
|
||||||
|
|
||||||
let mut frames = parser.parse(&make_pes(stream, Some(0)));
|
let frames = parser.parse(&make_pes(stream, Some(0)));
|
||||||
assert_eq!(
|
assert!(frames.is_empty(), "same GOP — buffered until flush");
|
||||||
frames.len(),
|
|
||||||
1,
|
let frames = parser.flush();
|
||||||
"first picture emitted at second's boundary"
|
assert_eq!(frames.len(), 2);
|
||||||
);
|
|
||||||
assert_eq!(frames[0].data, pic1);
|
assert_eq!(frames[0].data, pic1);
|
||||||
assert!(frames[0].keyframe);
|
assert!(frames[0].keyframe);
|
||||||
|
|
||||||
frames.extend(parser.flush());
|
|
||||||
assert_eq!(frames.len(), 2);
|
|
||||||
assert_eq!(frames[1].data, pic2);
|
assert_eq!(frames[1].data, pic2);
|
||||||
assert!(!frames[1].keyframe);
|
assert!(!frames[1].keyframe);
|
||||||
}
|
}
|
||||||
@@ -703,23 +820,24 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn each_picture_gets_the_pts_of_the_pes_that_began_it() {
|
fn each_picture_gets_the_pts_of_the_pes_that_began_it() {
|
||||||
|
// With no sequence header (no frame rate) the parser falls back to each
|
||||||
|
// AU's own PES PTS. Both pictures are one GOP → emitted on flush in
|
||||||
|
// decode order, each carrying the PTS of the PES that began it.
|
||||||
let mut parser = Mpeg2Parser::new();
|
let mut parser = Mpeg2Parser::new();
|
||||||
|
|
||||||
// PES 1: pic1 (PTS 90000) + start of pic2's bytes carried later.
|
|
||||||
let mut pic1 = make_picture_header(PICTURE_TYPE_I);
|
let mut pic1 = make_picture_header(PICTURE_TYPE_I);
|
||||||
pic1.extend_from_slice(&vec![0x11; 50]);
|
pic1.extend_from_slice(&vec![0x11; 50]);
|
||||||
let frames1 = parser.parse(&make_pes(pic1, Some(90000)));
|
let frames1 = parser.parse(&make_pes(pic1, Some(90000)));
|
||||||
assert!(frames1.is_empty(), "pic1 awaits pic2's boundary");
|
assert!(frames1.is_empty(), "buffered until flush");
|
||||||
|
|
||||||
// PES 2: pic2 (PTS 180000).
|
|
||||||
let mut pic2 = make_picture_header(2);
|
let mut pic2 = make_picture_header(2);
|
||||||
pic2.extend_from_slice(&vec![0x22; 50]);
|
pic2.extend_from_slice(&vec![0x22; 50]);
|
||||||
let mut frames = parser.parse(&make_pes(pic2, Some(180000)));
|
let frames2 = parser.parse(&make_pes(pic2, Some(180000)));
|
||||||
assert_eq!(frames.len(), 1, "pic1 emitted when pic2 starts");
|
assert!(frames2.is_empty(), "same GOP — still buffered");
|
||||||
assert_eq!(frames[0].pts_ns, 1_000_000_000, "pic1 → PTS 90000");
|
|
||||||
|
|
||||||
frames.extend(parser.flush());
|
let frames = parser.flush();
|
||||||
assert_eq!(frames.len(), 2);
|
assert_eq!(frames.len(), 2);
|
||||||
|
assert_eq!(frames[0].pts_ns, 1_000_000_000, "pic1 → PTS 90000");
|
||||||
assert_eq!(frames[1].pts_ns, 2_000_000_000, "pic2 → PTS 180000");
|
assert_eq!(frames[1].pts_ns, 2_000_000_000, "pic2 → PTS 180000");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -761,6 +879,89 @@ mod tests {
|
|||||||
assert_eq!(frames[0].duration_ns, Some(40_000_000));
|
assert_eq!(frames[0].duration_ns, Some(40_000_000));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A frame-picture AU with a picture coding extension carrying pulldown
|
||||||
|
/// flags (progressive_frame=1, so rff=1 → 3 fields), for VFR timing tests.
|
||||||
|
fn make_pulldown_picture(coding_type: u8, tr: u16, rff: u8) -> Vec<u8> {
|
||||||
|
let mut au = make_picture_header_tr(coding_type, tr);
|
||||||
|
// 00 00 01 B5 | e0 ext-id 1000 | e1 | e2 frame-pic | e3 rff<<1 | e4 prog_frame
|
||||||
|
au.extend_from_slice(&[
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
SEQ_EXT_CODE,
|
||||||
|
0x80,
|
||||||
|
0x00,
|
||||||
|
0x03,
|
||||||
|
rff << 1,
|
||||||
|
0x80,
|
||||||
|
]);
|
||||||
|
au.extend_from_slice(&[0xAA; 16]);
|
||||||
|
au
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn telecine_pts_accumulates_by_field_durations_not_a_fixed_grid() {
|
||||||
|
// NTSC film, frame_rate_code 4 = 29.97 → field_period ≈ 16.683 ms. A 2:3
|
||||||
|
// frame (rff=1) occupies 3 fields, a 2:2 frame 2 fields. PTS must
|
||||||
|
// accumulate by ACTUAL field durations so the next frame starts exactly
|
||||||
|
// when this one ends — closing the fixed-29.97-grid gap that judders.
|
||||||
|
let mut p = Mpeg2Parser::new();
|
||||||
|
let field = 1_000_000_000i64 * 1001 / 30000 / 2;
|
||||||
|
|
||||||
|
let mut a = make_seq_header(720, 480, 2, 4);
|
||||||
|
a.extend_from_slice(&gop());
|
||||||
|
a.extend(make_pulldown_picture(1, 0, 1)); // I tr0, 3 fields, PES anchor 0
|
||||||
|
a.extend(make_pulldown_picture(2, 1, 0)); // P tr1, 2 fields
|
||||||
|
let mut frames = p.parse(&make_pes(a, Some(0)));
|
||||||
|
frames.extend(p.flush());
|
||||||
|
|
||||||
|
assert_eq!(frames.len(), 2);
|
||||||
|
assert_eq!(frames[0].pts_ns, 0, "I anchored to PES PTS 0");
|
||||||
|
assert_eq!(
|
||||||
|
frames[0].duration_ns,
|
||||||
|
Some(3 * field as u64),
|
||||||
|
"I = 3 fields"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
frames[1].pts_ns,
|
||||||
|
3 * field,
|
||||||
|
"P starts exactly at I-end (3 fields), not the 1/29.97 grid"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
frames[1].duration_ns,
|
||||||
|
Some(2 * field as u64),
|
||||||
|
"P = 2 fields"
|
||||||
|
);
|
||||||
|
assert!(frames[1].pts_ns > frames[0].pts_ns, "strictly monotonic");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn b_frames_emit_in_decode_order_with_lower_display_pts() {
|
||||||
|
// Decode order I(tr0) P(tr2) B(tr1): emitted in DECODE order, but the
|
||||||
|
// B-frame carries a LOWER (earlier) display PTS than the P that precedes
|
||||||
|
// it in the stream — never reordered (reordering corrupts the picture).
|
||||||
|
let mut p = Mpeg2Parser::new();
|
||||||
|
let field = 1_000_000_000i64 * 1001 / 30000 / 2;
|
||||||
|
|
||||||
|
let mut a = make_seq_header(720, 480, 2, 4);
|
||||||
|
a.extend_from_slice(&gop());
|
||||||
|
a.extend(make_pulldown_picture(1, 0, 0)); // I tr0 (displays 1st), PES anchor 0
|
||||||
|
a.extend(make_pulldown_picture(2, 2, 0)); // P tr2 (displays 3rd)
|
||||||
|
a.extend(make_pulldown_picture(3, 1, 0)); // B tr1 (displays 2nd)
|
||||||
|
let mut frames = p.parse(&make_pes(a, Some(0)));
|
||||||
|
frames.extend(p.flush());
|
||||||
|
|
||||||
|
assert_eq!(frames.len(), 3);
|
||||||
|
assert!(frames[0].keyframe, "decode order preserved: I first");
|
||||||
|
assert_eq!(frames[0].pts_ns, 0, "I (tr0) displays 1st");
|
||||||
|
assert_eq!(frames[1].pts_ns, 4 * field, "P (tr2) displays 3rd");
|
||||||
|
assert_eq!(frames[2].pts_ns, 2 * field, "B (tr1) displays 2nd");
|
||||||
|
assert!(
|
||||||
|
frames[2].pts_ns < frames[1].pts_ns,
|
||||||
|
"B emitted AFTER P (decode order) but displays BEFORE it (lower PTS)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn temporal_reference_resets_each_gop_via_gop_base() {
|
fn temporal_reference_resets_each_gop_via_gop_base() {
|
||||||
// Across a GOP boundary, temporal_reference restarts at 0 but the
|
// Across a GOP boundary, temporal_reference restarts at 0 but the
|
||||||
@@ -973,9 +1174,10 @@ mod tests {
|
|||||||
let mut a = make_seq_header(1920, 1080, 3, 4);
|
let mut a = make_seq_header(1920, 1080, 3, 4);
|
||||||
a.extend_from_slice(&make_picture_header(PICTURE_TYPE_I));
|
a.extend_from_slice(&make_picture_header(PICTURE_TYPE_I));
|
||||||
a.extend_from_slice(&[0xAA; 20]);
|
a.extend_from_slice(&[0xAA; 20]);
|
||||||
a.extend_from_slice(&gop()); // boundary → AU A emits
|
a.extend_from_slice(&gop()); // trailing GOP header starts the next GOP
|
||||||
let fa = parser.parse(&make_pes(a, Some(0)));
|
let _fa = parser.parse(&make_pes(a, Some(0)));
|
||||||
assert_eq!(fa.len(), 1);
|
// Header A is captured during parse (codec_private) even though its GOP
|
||||||
|
// only emits once header B's picture closes it / on flush.
|
||||||
assert_eq!(parser.resolution(), Some((1920, 1080)));
|
assert_eq!(parser.resolution(), Some((1920, 1080)));
|
||||||
|
|
||||||
// AU B: a NEW 720x480 seq header + I picture. Its extension/header must
|
// AU B: a NEW 720x480 seq header + I picture. Its extension/header must
|
||||||
@@ -1062,11 +1264,12 @@ mod tests {
|
|||||||
// > MAX_AU_BUFFER of slice bytes with no following picture/seq/GOP.
|
// > MAX_AU_BUFFER of slice bytes with no following picture/seq/GOP.
|
||||||
data.extend(std::iter::repeat_n(0xAA, MAX_AU_BUFFER + 1024));
|
data.extend(std::iter::repeat_n(0xAA, MAX_AU_BUFFER + 1024));
|
||||||
let frames = parser.parse(&make_pes(data, Some(0)));
|
let frames = parser.parse(&make_pes(data, Some(0)));
|
||||||
assert_eq!(
|
assert!(
|
||||||
frames.len(),
|
frames.is_empty(),
|
||||||
1,
|
"over-cap AU is force-COMPLETED (bounded) but buffered in its GOP"
|
||||||
"over-cap AU force-flushed rather than buffered"
|
|
||||||
);
|
);
|
||||||
|
let frames = parser.flush();
|
||||||
|
assert_eq!(frames.len(), 1, "force-flushed at EOF, not dropped");
|
||||||
assert!(frames[0].keyframe);
|
assert!(frames[0].keyframe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-6
@@ -435,17 +435,18 @@ pub const FIELD_ORDER: u32 = 0x9D;
|
|||||||
// FlagInterlaced values: 1 = interlaced, 2 = progressive (0 = undetermined).
|
// FlagInterlaced values: 1 = interlaced, 2 = progressive (0 = undetermined).
|
||||||
pub const INTERLACED_INTERLACED: u64 = 1;
|
pub const INTERLACED_INTERLACED: u64 = 1;
|
||||||
pub const INTERLACED_PROGRESSIVE: u64 = 2;
|
pub const INTERLACED_PROGRESSIVE: u64 = 2;
|
||||||
// FieldOrder values (Matroska): 0/2 = top-field-first, 1/9 = bottom-field-first.
|
// FieldOrder values (Matroska / RFC 9559, element 0x9D): 1 = top-field-first,
|
||||||
// NTSC DVD (480i), PAL DVD (576i) and HD (1080i) are all emitted top-field-first
|
// 6 = bottom-field-first, 2 = undetermined, 0 = progressive. NTSC DVD (480i),
|
||||||
// — the muxer hardcodes TFF for every interlaced DVD/HD source (DV is the only
|
// PAL DVD (576i) and HD (1080i) are all emitted top-field-first — the muxer
|
||||||
// common BFF source and freemkv does not produce it). 0xFF is our sentinel for
|
// hardcodes TFF for every interlaced DVD/HD source (DV is the only common BFF
|
||||||
|
// source and freemkv does not produce it). 0xFF is our sentinel for
|
||||||
// "undetermined / omit".
|
// "undetermined / omit".
|
||||||
pub const FIELD_ORDER_TFF: u8 = 2;
|
pub const FIELD_ORDER_TFF: u8 = 1;
|
||||||
// Bottom-field-first. Retained for completeness/round-trip tests; the muxer
|
// Bottom-field-first. Retained for completeness/round-trip tests; the muxer
|
||||||
// emits TFF for all DVD/HD interlaced content (DV is the only common BFF
|
// emits TFF for all DVD/HD interlaced content (DV is the only common BFF
|
||||||
// source and freemkv does not produce it).
|
// source and freemkv does not produce it).
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub const FIELD_ORDER_BFF: u8 = 9;
|
pub const FIELD_ORDER_BFF: u8 = 6;
|
||||||
pub const FIELD_ORDER_UNDETERMINED: u8 = 0xFF;
|
pub const FIELD_ORDER_UNDETERMINED: u8 = 0xFF;
|
||||||
pub const DISPLAY_WIDTH: u32 = 0x54B0;
|
pub const DISPLAY_WIDTH: u32 = 0x54B0;
|
||||||
pub const DISPLAY_HEIGHT: u32 = 0x54BA;
|
pub const DISPLAY_HEIGHT: u32 = 0x54BA;
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ impl<S: SectorSource> SectorSource for DecryptingSectorSource<S> {
|
|||||||
// count is bytes of scrambled units no key could decrypt — silent
|
// count is bytes of scrambled units no key could decrypt — silent
|
||||||
// decrypt loss the TS assembler will drop. Tally it so the mux loss
|
// decrypt loss the TS assembler will drop. Tally it so the mux loss
|
||||||
// accounting (and the abort gate) can see partial decrypt failure.
|
// accounting (and the abort gate) can see partial decrypt failure.
|
||||||
let dropped = decrypt_sectors(&mut buf[..n], &self.keys, self.unit_key_idx)?;
|
let dropped = decrypt_sectors(&mut buf[..n], &mut self.keys, self.unit_key_idx)?;
|
||||||
if dropped > 0 {
|
if dropped > 0 {
|
||||||
self.decrypt_dropped
|
self.decrypt_dropped
|
||||||
.fetch_add(dropped as u64, Ordering::Relaxed);
|
.fetch_add(dropped as u64, Ordering::Relaxed);
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ fn decrypt_sectors_with_aacs_keys_works() {
|
|||||||
aacs::decrypt_unit(&mut unit, &unit_key); // decrypt_unit is idempotent on already-encrypted data
|
aacs::decrypt_unit(&mut unit, &unit_key); // decrypt_unit is idempotent on already-encrypted data
|
||||||
|
|
||||||
// Now we have encrypted data - create DecryptKeys with actual keys
|
// Now we have encrypted data - create DecryptKeys with actual keys
|
||||||
let keys = DecryptKeys::Aacs {
|
let mut keys = DecryptKeys::Aacs {
|
||||||
unit_keys: vec![(0u32, unit_key)],
|
unit_keys: vec![(0u32, unit_key)],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// decrypt_sectors should handle this without error
|
// decrypt_sectors should handle this without error
|
||||||
let result = libfreemkv::decrypt::decrypt_sectors(&mut unit, &keys, 0);
|
let result = libfreemkv::decrypt::decrypt_sectors(&mut unit, &mut keys, 0);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
result.is_ok(),
|
result.is_ok(),
|
||||||
@@ -50,8 +50,8 @@ fn decrypt_sectors_with_aacs_keys_works() {
|
|||||||
fn decrypt_sectors_with_none_keys_is_noop() {
|
fn decrypt_sectors_with_none_keys_is_noop() {
|
||||||
let mut sector = vec![0x42u8; 2048];
|
let mut sector = vec![0x42u8; 2048];
|
||||||
|
|
||||||
let keys = DecryptKeys::None;
|
let mut keys = DecryptKeys::None;
|
||||||
let result = libfreemkv::decrypt::decrypt_sectors(&mut sector, &keys, 0);
|
let result = libfreemkv::decrypt::decrypt_sectors(&mut sector, &mut keys, 0);
|
||||||
|
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -70,10 +70,10 @@ fn decrypt_sectors_with_css_keys_works() {
|
|||||||
sector[0x14] |= 0x30;
|
sector[0x14] |= 0x30;
|
||||||
|
|
||||||
let title_key: [u8; 5] = [0x42, 0x13, 0x37, 0xBE, 0xEF]; // Not used - defined later
|
let title_key: [u8; 5] = [0x42, 0x13, 0x37, 0xBE, 0xEF]; // Not used - defined later
|
||||||
let keys = DecryptKeys::Css { title_key };
|
let mut keys = DecryptKeys::Css { title_key };
|
||||||
|
|
||||||
// Descramble (CSS uses same operation for encrypt/decrypt)
|
// Descramble (CSS uses same operation for encrypt/decrypt)
|
||||||
libfreemkv::decrypt::decrypt_sectors(&mut sector, &keys, 0).unwrap();
|
libfreemkv::decrypt::decrypt_sectors(&mut sector, &mut keys, 0).unwrap();
|
||||||
|
|
||||||
// Flag should be cleared
|
// Flag should be cleared
|
||||||
assert_eq!(sector[0x14] & 0x30, 0x00, "CSS flag should be cleared");
|
assert_eq!(sector[0x14] & 0x30, 0x00, "CSS flag should be cleared");
|
||||||
|
|||||||
Reference in New Issue
Block a user