Merge branch 'audit-lib-G5' into audit-fix-lib

This commit is contained in:
Matthew Jackson
2026-10-01 15:16:36 -07:00
19 changed files with 956 additions and 84 deletions
+19 -1
View File
@@ -896,6 +896,19 @@ mod tests {
let es_off = p + 14; // ES data follows the 14-byte PES header
let n = es.len().min(192 - es_off);
pkt[es_off..es_off + n].copy_from_slice(&es[..n]);
// Stuff through the adaptation field so the PES ends exactly at its last
// ES byte rather than carrying zero padding after it.
let len = es_off + n - p;
if len < 184 {
let start = 192 - len;
pkt.copy_within(p..p + len, start);
pkt[7] |= 0x20;
pkt[8] = (start - 9) as u8;
if start > 9 {
pkt[9] = 0;
pkt[10..start].fill(0xFF);
}
}
pkt
}
@@ -1675,7 +1688,12 @@ mod tests {
// Slot per track so two tracks sharing a sector do not
// overwrite each other; both stay on the 192-byte grid.
let off = (at - u64::from(lba)) as usize * SECTOR_BYTES + idx * 192;
let pkt = bd_pes_packet(t.pid, (i % 16) as u8, &pcs_display(t.forced));
// PCS + END: a display set cut off before its END is not
// emitted when a stream gap follows it.
let mut set = pcs_display(t.forced);
set[2] = (set.len() - 3) as u8;
set.extend_from_slice(&[0x80, 0, 0]);
let pkt = bd_pes_packet(t.pid, (i % 16) as u8, &set);
if off + pkt.len() <= want {
buf[off..off + pkt.len()].copy_from_slice(&pkt);
}
+3 -3
View File
@@ -492,9 +492,9 @@ impl CodecParser for Ac3Parser {
);
self.acc.clear();
self.held = None;
// Advance the cadence, as the other two paths out of this block do,
// so all three cannot disagree. Defensive: no input parsing frames
// and leaving a residue this large is currently reachable.
// Advance the cadence like the other two paths out of this block.
// No known input reaches this branch; it must still drop the held
// AU and advance, or a stale HeldAu would resume after the resync.
self.flush_pts_ns = frame_pts_ns;
} else {
self.acc.drain(keep_from);
+115 -2
View File
@@ -16,10 +16,10 @@ const ADTS_SAMPLE_RATE_VALID: [u32; 16] = [
0,
];
/// ADTS header verdict for the packet head.
// Length of the fixed ADTS header without CRC.
/// Length of the fixed ADTS header without CRC.
const ADTS_HEADER_BYTES: usize = 7;
/// ADTS header verdict for the packet head.
enum AdtsVerdict {
/// No 12-bit ADTS sync at the head — not an ADTS frame we can validate.
NoSync,
@@ -474,6 +474,119 @@ mod tests {
assert_eq!(&asc[2..], &[0x05, 0x04, 0x00, 0x00, 0x20, 0x00]);
}
fn bits_to_bytes(bits: &str) -> Vec<u8> {
let bits: String = bits.chars().filter(|c| !c.is_whitespace()).collect();
assert_eq!(bits.len() % 8, 0, "byte-aligned");
(0..bits.len() / 8)
.map(|i| u8::from_str_radix(&bits[i * 8..i * 8 + 8], 2).unwrap())
.collect()
}
fn pad8(bits: &str) -> String {
let bits: String = bits.chars().filter(|c| !c.is_whitespace()).collect();
let pad = (8 - bits.len() % 8) % 8;
bits + &"0".repeat(pad)
}
// An ADTS frame with channel_configuration 0 whose payload is `block`.
fn config0_frame(block: &[u8]) -> Vec<u8> {
let mut f = adts_frame(0);
f.extend_from_slice(block);
let total = f.len() as u32;
f[2] &= !1;
f[3] = (f[3] & 0x3C) | ((total >> 11) & 3) as u8;
f[4] = (total >> 3) as u8;
f[5] = (((total & 7) << 5) as u8) | 0x1F;
f
}
// 5.1: 2 front, 1 back, 1 LFE, all three mixdown fields, a 3-byte comment.
const PCE_51_FIELDS: &str = "0000 01 0011 0010 0000 0001 01 000 0000 \
1 0001 1 0010 1 101 \
01000 10000 10001 0000";
const PCE_51_COMMENT: &str = "00000011 10101010 01010101 11110000";
fn pce_51_block() -> Vec<u8> {
bits_to_bytes(&(pad8(&format!("101{PCE_51_FIELDS}")) + &PCE_51_COMMENT.replace(' ', "")))
}
#[test]
fn adts_pce_repacks_every_field_to_the_asc_start() {
let expect = bits_to_bytes(&(pad8(PCE_51_FIELDS) + &PCE_51_COMMENT.replace(' ', "")));
assert_eq!(adts_pce(&pce_51_block()).unwrap(), expect);
}
#[test]
fn adts_pce_without_mixdown_fields_and_with_assoc_and_cc_elements() {
// 1 front, 1 assoc data, 2 coupling channels: 5*(1+2) + 4*1 element bits.
let fields = "0000 01 0100 0001 0000 0000 00 001 0010 0 0 0 \
10000 \
0000 \
00001 10001";
let block = bits_to_bytes(&(pad8(&format!("101{fields}")) + "00000000"));
let expect = bits_to_bytes(&(pad8(fields) + "00000000"));
assert_eq!(adts_pce(&block).unwrap(), expect);
}
#[test]
fn adts_pce_truncated_comment_or_wrong_id_is_none() {
let block = pce_51_block();
assert_eq!(adts_pce(&block[..block.len() - 1]), None);
let mut not_pce = block;
not_pce[0] ^= 0x80;
assert_eq!(adts_pce(&not_pce), None);
}
#[test]
fn channel_config_0_asc_follows_the_pce_through_truncation_upgrade_and_change() {
let (mut config, mut changes) = (None, 0);
let block = pce_51_block();
// A PCE cut off by the frame end leaves the 2-byte ASC.
let cut = config0_frame(&block[..4]);
adts_header(&cut, &mut config, &mut changes, 0).unwrap();
assert_eq!(config.as_deref().map(<[u8]>::len), Some(2));
// The first frame that carries the whole PCE upgrades it; not a change.
let full = config0_frame(&block);
adts_header(&full, &mut config, &mut changes, 0).unwrap();
let asc = config.clone().unwrap();
assert_eq!(&asc[2..], adts_pce(&block).unwrap());
assert_eq!(changes, 0);
// A different layout is counted and the first config is kept.
let mut other = block;
other[1] ^= 0x10;
adts_header(&config0_frame(&other), &mut config, &mut changes, 0).unwrap();
assert_eq!(changes, 1);
assert_eq!(config, Some(asc));
}
#[test]
fn adts_template_encodes_the_channel_configuration_across_both_header_bytes() {
// AAC-LC, 48 kHz: ASC byte 0 = 0x11, byte 1 = (rate low bit << 7) | (channels << 3).
for channels in 1u8..=7 {
let t = adts_template(&[0x11, 0x80 | (channels << 3)]).unwrap();
assert_eq!(
t[2],
0x40 | (3 << 2) | (channels >> 2),
"channels {channels}"
);
assert_eq!(t[3], (channels & 3) << 6, "channels {channels}");
}
assert_eq!(adts_template(&[0x11, 0x80]), None, "0 needs an in-band PCE");
assert_eq!(adts_template(&[0x11, 0x80 | (8 << 3)]), None);
}
#[test]
fn adts_frame_length_field_holds_13_bits_and_blocks_are_1_to_4() {
let t = adts_template(&[0x11, 0x90]).unwrap();
let max = super::adts_frame(t, &vec![0u8; (1 << 13) - 1 - 7], 1).unwrap();
assert_eq!(max.len(), (1 << 13) - 1);
assert_eq!(adts_frame_len(&max), Some((1 << 13) - 1));
assert_eq!(super::adts_frame(t, &vec![0u8; (1 << 13) - 7], 1), None);
assert_eq!(super::adts_frame(t, &[0; 4], 0), None);
assert_eq!(super::adts_frame(t, &[0; 4], 5), None);
assert_eq!(super::adts_frame(t, &[0; 4], 4).unwrap()[6] & 3, 3);
}
#[test]
fn valid_adts_is_kept() {
let mut p = AdtsParser::new();
+3 -1
View File
@@ -22,6 +22,8 @@ use super::{Frame, PesPacket};
// Most bytes held after a lock while waiting for the next timestamp (see place_lock).
const MAX_HOLD: usize = 64 * 1024;
// Most bytes buffered across packets before the stream is judged corrupt and the packet dropped.
const MAX_BUFFERED: usize = 1024 * 1024;
pub(super) struct Header {
pub bytes: usize,
@@ -182,7 +184,7 @@ impl AudioFrames {
self.framed = true;
// Audio PES packets are small. Refuse pathological accumulation before
// copying; a corrupt size must not grow memory without bound.
if self.buf.len().saturating_add(pes.data.len()) > 1024 * 1024 {
if self.buf.len().saturating_add(pes.data.len()) > MAX_BUFFERED {
self.buf.clear();
self.tally.record_drop(
facts.presentation_ns().unwrap_or(self.next_pts),
+9
View File
@@ -168,6 +168,15 @@ mod tests {
assert!(!t.is_poisoned());
}
#[test]
fn a_negative_duration_is_counted_as_zero_lost_time() {
let mut t = DropTally::new("test");
t.record_drop(0, -5_000, 512, "bad");
t.record_collateral_drop(0, -1, 512, "resync-forward");
assert_eq!(t.dropped_frames(), 2);
assert_eq!(t.dropped_duration_ns(), 0);
}
#[test]
fn poisons_after_min_aus_over_half_dropped() {
let mut t = DropTally::new("test");
+65 -3
View File
@@ -23,10 +23,12 @@ pub struct DtsParser {
/// its FIRST byte, so an AU whose core arrived in an earlier PES keeps that
/// core's timestamp and source offset when its extensions arrive later.
acc: super::pesbuf::PesBuf,
/// PTS of the access unit currently being assembled in `buf` (the unit
/// PTS of the access unit currently being assembled in `acc` (the unit
/// starting at the first buffered core sync). Captured when that core
/// frame's PES first arrived; the trailing extension-substream PES
/// packets carry their own (later) PTS which must NOT override it.
/// `PTS_UNSET` after a gap or a forced flush; `front_pts` falls back to it
/// only when the front packet carries no timestamp.
pending_pts: i64,
/// The `front_pts` of the PREVIOUS emitted access unit. When the current
/// AU's `front_pts` differs, it began a new PES → re-base to it. When it is
@@ -73,8 +75,8 @@ impl DtsParser {
}
}
/// Number of access units dropped as undecodable so far. The mux/CLI reads
/// this to surface the count ("dropped N damaged DTS frames").
/// Number of access units dropped as undecodable so far. Only the tally's
/// end-of-stream summary logs it; no mux or CLI consumer reads this accessor.
pub fn dropped_frames(&self) -> u64 {
self.tally.dropped_frames()
}
@@ -2450,6 +2452,66 @@ mod tests {
assert_eq!(core_header_drop_reason(&d), Some(DropReason::PcmRes));
}
// CPF=1 inserts a 16-bit header CRC before the PCM resolution; the gate must skip exactly
// those bits. PCMR sits at absolute bits 95..=97 without the CRC, 111..=113 with it.
#[test]
fn a_crc_protected_core_header_reads_pcmr_after_the_16_crc_bits() {
let mut crc = make_dts_core(512);
crc[4] |= 0x02; // CPF
// The CRC bits are arbitrary, and set to a reserved code where PCMR sits without it.
crc[11] |= 0x01;
crc[12] = 0xFF;
crc[13] |= 0xFE;
assert_eq!(core_header_drop_reason(&crc), None);
// Decodable PCMR 3 whose trailing neighbours would read as reserved code 4 if the
// skip ran two bits long.
let mut d = make_dts_core(512);
d[4] |= 0x02;
d[14] |= 0xC0;
assert_eq!(core_header_drop_reason(&d), None);
// Reserved PCMR 7 is found at the shifted position.
let mut d = make_dts_core(512);
d[4] |= 0x02;
d[13] |= 0x01;
d[14] |= 0xC0;
assert_eq!(core_header_drop_reason(&d), Some(DropReason::PcmRes));
}
// Substreams over 64 KiB use the long header form (12-bit header size, 20-bit Fsize).
fn long_form_exss(total: usize, static_fields: Option<(&str, &str)>) -> Vec<u8> {
let mut bits = format!(
"{}{}1{}{:020b}",
"0".repeat(8),
"00",
"0".repeat(12),
total - 1
);
if let Some((rate, dur)) = static_fields {
bits.push_str(&format!("1{rate}{dur}"));
}
while bits.len() % 8 != 0 {
bits.push('0');
}
let mut d = vec![0u8; total.max(4 + bits.len() / 8)];
d[0..4].copy_from_slice(&DTS_HD_EXT_SYNC);
for (i, chunk) in bits.as_bytes().chunks(8).enumerate() {
d[4 + i] = u8::from_str_radix(std::str::from_utf8(chunk).unwrap(), 2).unwrap();
}
d
}
#[test]
fn exss_long_header_form_sizes_and_times_substreams_over_64_kib() {
let d = long_form_exss(70_000, Some(("10", "001")));
assert_eq!(exss_frame_size(&d), Some(70_000));
assert_eq!(exss_timing(&d), Some((1024, 48_000)));
let d = long_form_exss(1_000_000, None);
assert_eq!(exss_frame_size(&d), Some(1_000_000));
assert_eq!(exss_timing(&d), None);
}
#[test]
fn legal_multichannel_amode_is_not_dropped() {
// ETSI TS 102 114 §5.3.1: AMODE is 6 bits with 16 LEGAL codes (0-15), only
+40 -31
View File
@@ -45,7 +45,15 @@ impl DvdSubParser {
// `complete` is only true when `self.pending` was `Some` above, but
// borrow through `?` rather than a prod `unwrap()` on a fact the
// compiler can't see across the two lines.
let (facts, _, data) = self.pending.take()?;
let (facts, size, data) = self.pending.take()?;
if data.len() < size {
tracing::warn!(
target: "mux",
have = data.len(),
declared = size,
"dvdsub: emitting a truncated subpicture unit"
);
}
// Guaranteed Some: `pending` is only ever created from a PES with
// `pts.is_some()` (L054 dropped the no-PTS orphan path), so this
// never takes the fallback; kept as unwrap_or, never a prod unwrap.
@@ -165,7 +173,6 @@ impl CodecParser for DvdSubParser {
// ── YCbCr → RGB conversion and palette formatting ─────────────────────────
// Byte-2-as-Cb is invisible on achromatic (Cb=Cr=128) palette entries.
/// Convert a single YCbCr color to RGB, clamping to [0, 255].
///
/// Input: `[padding, Y, Cr, Cb]` (as stored in DVD IFO PGC data). Note the
@@ -174,15 +181,17 @@ impl CodecParser for DvdSubParser {
///
/// Returns `[R, G, B]`.
///
/// Uses full-range (JFIF) BT.601 coefficients, matching the VobSub `.idx` on-disk convention.
/// The DVD CLUT is studio-range (Y 16..=235, chroma 16..=240), so this applies
/// the limited-range BT.601 expansion: Y=16 maps to black, Y=235 to white. The
/// result is the full-range RGB a VobSub `.idx` palette carries.
pub fn ycbcr_to_rgb(color: &[u8; 4]) -> [u8; 3] {
let y = color[1] as f64;
let cr = color[2] as f64;
let cb = color[3] as f64;
let y = 1.164 * (color[1] as f64 - 16.0);
let cr = color[2] as f64 - 128.0;
let cb = color[3] as f64 - 128.0;
let r = y + 1.402 * (cr - 128.0);
let g = y - 0.344 * (cb - 128.0) - 0.714 * (cr - 128.0);
let b = y + 1.772 * (cb - 128.0);
let r = y + 1.596 * cr;
let g = y - 0.392 * cb - 0.813 * cr;
let b = y + 2.017 * cb;
[clamp_u8(r), clamp_u8(g), clamp_u8(b)]
}
@@ -424,22 +433,16 @@ mod tests {
#[test]
fn ycbcr_to_rgb_white() {
// White in YCbCr: Y=235, Cb=128, Cr=128 → R=235, G=235, B=235
// Studio-range white: Y=235 at neutral chroma expands to full white.
let color = [0x00, 235, 128, 128];
let [r, g, b] = ycbcr_to_rgb(&color);
assert_eq!(r, 235);
assert_eq!(g, 235);
assert_eq!(b, 235);
assert_eq!(ycbcr_to_rgb(&color), [255, 255, 255]);
}
#[test]
fn ycbcr_to_rgb_black() {
// Black: Y=16, Cb=128, Cr=128 → R=16, G=16, B=16
// Studio-range black: Y=16 at neutral chroma is true black.
let color = [0x00, 16, 128, 128];
let [r, g, b] = ycbcr_to_rgb(&color);
assert_eq!(r, 16);
assert_eq!(g, 16);
assert_eq!(b, 16);
assert_eq!(ycbcr_to_rgb(&color), [0, 0, 0]);
}
#[test]
@@ -465,9 +468,6 @@ mod tests {
// Approximate red: Y=82, Cr=240, Cb=90 — on disc as [pad, Y, Cr, Cb].
let color = [0x00, 82, 240, 90];
let [r, g, b] = ycbcr_to_rgb(&color);
// R = 82 + 1.402*(240-128) = 82 + 156.9 ≈ 239
// G = 82 - 0.344*(90-128) - 0.714*(240-128) = 82 + 13.1 - 79.97 ≈ 15
// B = 82 + 1.772*(90-128) = 82 - 67.3 ≈ 15
assert!(r > 200, "R should be high for red, got {}", r);
assert!(g < 30, "G should be low for red, got {}", g);
assert!(b < 30, "B should be low for red, got {}", b);
@@ -486,7 +486,7 @@ mod tests {
"on-disc red [0,Y=76,Cr=255,Cb=85] must render red-dominant, \
got R={r} G={g} B={b} (R and B swapped => byte 2/3 are transposed)"
);
assert_eq!([r, g, b], [254, 0, 0], "exact full-range BT.601 red");
assert_eq!([r, g, b], [255, 0, 0], "exact studio-range BT.601 red");
// And the converse: a saturated BLUE on-disc entry (Y=29, Cr=107, Cb=255)
// must not come out red.
@@ -545,8 +545,8 @@ mod tests {
#[test]
fn format_palette_hex_format() {
// Y=128, Cb=128, Cr=128 → R=128, G=128, B=128 → "808080"
let palette = vec![[0x00, 128, 128, 128]];
// Y=126 at neutral chroma → R=G=B=128 → "808080"
let palette = vec![[0x00, 126, 128, 128]];
let result = format_palette(&palette, 0, 0);
let text = String::from_utf8(result).unwrap();
assert_eq!(text, "palette: 808080\n");
@@ -556,7 +556,7 @@ mod tests {
fn format_palette_emits_size_line_before_palette() {
// With non-zero dimensions the `.idx` `size:` line is prepended ahead of
// the palette so players place/scale the VobSub bitmap (PAL 720x576).
let palette = vec![[0x00, 128, 128, 128]];
let palette = vec![[0x00, 126, 128, 128]];
let result = format_palette(&palette, 720, 576);
let text = String::from_utf8(result).unwrap();
assert_eq!(
@@ -569,7 +569,7 @@ mod tests {
fn format_palette_omits_size_line_when_dimensions_unknown() {
// 0 width/height (unknown resolution) omits the size line rather than
// emitting a 0x0 frame; the palette line is still present.
let palette = vec![[0x00, 128, 128, 128]];
let palette = vec![[0x00, 126, 128, 128]];
let result = format_palette(&palette, 0, 576);
let text = String::from_utf8(result).unwrap();
assert_eq!(text, "palette: 808080\n", "no size line when a dim is 0");
@@ -717,12 +717,21 @@ mod tests {
#[test]
fn ycbcr_blue_channel_clamps_high() {
// B = Y + 1.772*(Cb-128). Y=128, Cb=255 → 128 + 1.772*127 ≈ 353 → clamp 255.
// B = 1.164*(Y-16) + 2.017*(Cb-128) ≈ 130 + 256 → clamp 255.
// Cb is byte 3 in the on-disc [pad, Y, Cr, Cb] layout.
let [_r, _g, b] = ycbcr_to_rgb(&[0x00, 128, 128, 255]);
assert_eq!(b, 255, "blue clamps at 255");
}
#[test]
fn ycbcr_to_rgb_green_uses_both_chroma_terms() {
// Y=128, Cr=100, Cb=90: G = 1.164*112 - 0.392*(-38) - 0.813*(-28) = 167.9.
assert_eq!(ycbcr_to_rgb(&[0x00, 128, 100, 90])[1], 168);
// Each chroma term alone: Cr moves G by -0.813/step, Cb by -0.392/step.
assert_eq!(ycbcr_to_rgb(&[0x00, 128, 100, 128])[1], 153);
assert_eq!(ycbcr_to_rgb(&[0x00, 128, 128, 90])[1], 145);
}
#[test]
fn format_palette_empty_is_just_prefix() {
// An empty palette yields "palette: \n" (prefix + newline, no entries).
@@ -733,9 +742,9 @@ mod tests {
#[test]
fn format_palette_pads_each_channel_to_two_hex_digits() {
// Each RGB channel is formatted as exactly 2 hex digits (zero-padded).
// Y=16,neutral → 0x10 → "101010" (each channel two digits).
let result = format_palette(&[[0x00, 16, 128, 128]], 0, 0);
assert_eq!(String::from_utf8(result).unwrap(), "palette: 101010\n");
// Y=20,neutral → 5 → "050505" (each channel two digits).
let result = format_palette(&[[0x00, 20, 128, 128]], 0, 0);
assert_eq!(String::from_utf8(result).unwrap(), "palette: 050505\n");
}
// Text guard in codec/mod.rs can't see `source: facts.source` with no
+27
View File
@@ -252,6 +252,33 @@ mod tests {
assert_eq!(p.dropped_frames(), before + 1);
}
// The blocking-strategy bit (0xFFF9 = variable blocksize) is masked off the sync test.
#[test]
fn variable_blocksize_frames_are_validated_too() {
let mut p = FlacParser::new();
let mut good = make_flac_frame(100);
good[1] = 0xF9;
let n = good.len();
let c = crc16_ansi(&good[..n - 2]);
good[n - 2] = (c >> 8) as u8;
good[n - 1] = c as u8;
assert_eq!(p.parse(&make_pes(good.clone(), Some(0))).len(), 1);
let mut bad = good;
bad[20] ^= 0xFF;
assert!(p.parse(&make_pes(bad, Some(90_000))).is_empty());
assert_eq!(p.dropped_frames(), 1);
}
#[test]
fn a_set_reserved_bit_is_not_a_flac_sync() {
let mut p = FlacParser::new();
let mut f = make_flac_frame(100);
f[1] = 0xFA; // the mandatory-0 reserved bit set
f[20] ^= 0xFF;
assert_eq!(p.parse(&make_pes(f, Some(0))).len(), 1, "passed through");
assert_eq!(p.dropped_frames(), 0);
}
#[test]
fn non_flac_packet_passes_through() {
// A packet without the FLAC sync isn't a frame we can validate — never
+92
View File
@@ -1750,6 +1750,98 @@ mod tests {
types
}
// The parameter sets a later bare keyframe re-asserts are the ACTIVE ones, not the first-seen
// avcC copy: a player re-applies avcC at every keyframe, so reverting would undo the change.
#[test]
fn a_bare_keyframe_reasserts_the_active_param_sets_not_the_avcc_copy() {
let nals = |fd: &[u8]| -> Vec<Vec<u8>> {
let mut out = Vec::new();
let mut off = 0;
while off + 4 <= fd.len() {
let len =
u32::from_be_bytes([fd[off], fd[off + 1], fd[off + 2], fd[off + 3]]) as usize;
out.push(fd[off + 4..off + 4 + len].to_vec());
off += 4 + len;
}
out
};
let au = |sps: Option<u8>, pps: Option<u8>, slice: &[u8]| {
let mut d = Vec::new();
if let Some(b) = sps {
d.extend_from_slice(&[0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x1E, b]);
}
if let Some(b) = pps {
d.extend_from_slice(&[0x00, 0x00, 0x01, 0x68, b]);
}
d.extend_from_slice(&[0x00, 0x00, 0x01]);
d.extend_from_slice(slice);
d
};
let mut parser = H264Parser::new();
parser.parse(&make_pes(
au(Some(0xAA), Some(0x11), &[0x65, 0x10]),
Some(0),
));
// Mid-title redefinition of both sets, on a non-keyframe access unit.
let f = parser.parse(&make_pes(
au(Some(0xBB), Some(0x22), &[0x41, 0x9A]),
Some(3000),
));
assert_eq!(frame_nal_types(&f[0].data), vec![7, 8, 1]);
// A bare IDR: SPS then PPS, both the redefined bodies.
let f = parser.parse(&make_pes(au(None, None, &[0x65, 0x10]), Some(6000)));
let n = nals(&f[0].data);
assert_eq!(frame_nal_types(&f[0].data), vec![7, 8, 5]);
assert_eq!(n[0].last(), Some(&0xBB), "active SPS, not the avcC copy");
assert_eq!(n[1].last(), Some(&0x22), "active PPS, not the avcC copy");
}
#[test]
fn slice_types_map_to_coding_types() {
use CodingType::{B, I, P};
let expect = [P, B, I, P, I, P, B, I, P, I];
for (st, want) in expect.into_iter().enumerate() {
assert_eq!(h264_slice_coding_type(st as u32), Some(want), "{st}");
}
assert_eq!(h264_slice_coding_type(10), None);
assert_eq!(h264_slice_coding_type(u32::MAX), None);
}
#[test]
fn avcc_oversized_pps_returns_none() {
let mut data = vec![0x00, 0x00, 0x01, 0x67, 0x42, 0x00, 0x1E, 0xAA];
data.extend_from_slice(&[0x00, 0x00, 0x01, 0x68]);
data.extend_from_slice(&vec![0x11u8; 70_000]);
let mut parser = H264Parser::new();
parser.parse(&make_pes(data, Some(0)));
assert!(
parser.codec_private().is_none(),
"oversized PPS must not produce a truncated avcC"
);
}
// ISO 14496-15 §5.3.3.1.2: every profile whose SPS carries chroma_format_idc and bit depths
// gets the 4 avcC extension bytes; Baseline/Main/Extended must not.
#[test]
fn avcc_extension_bytes_follow_exactly_the_chroma_carrying_profiles() {
for profile in [
100u8, 110, 122, 144, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135,
] {
let sps = build_high_profile_sps(profile, 1, 0, 0);
let mut parser = H264Parser::new();
feed_sps_pps(&mut parser, &sps);
let cp = parser.codec_private().expect("avcC");
assert_eq!(cp.len(), sps.len() + 14 + 4, "profile {profile}");
}
for profile in [66u8, 77, 88] {
let sps = build_high_profile_sps(profile, 1, 0, 0);
let mut parser = H264Parser::new();
feed_sps_pps(&mut parser, &sps);
let cp = parser.codec_private().expect("avcC");
assert_eq!(cp.len(), sps.len() + 14, "profile {profile}");
}
}
// An IDR whose SPS changed but whose PPS did not: the re-asserted PPS must follow
// the new SPS (§7.3.2.2: a PPS is parsed against its SPS).
#[test]
+204 -10
View File
@@ -115,8 +115,8 @@ pub struct HevcParser {
/// ids are 0..=63 (H.265 §7.4.3.3); an out-of-range id is ignored.
pps_num_extra: [Option<u32>; HEVC_MAX_PPS_COUNT],
// Splice-aware CRA→BLA rewrite for a non-seamless BD clip boundary (first
// CRA_NUT -> BLA_W_LP so NoRaslOutput discards dangling RASL). Armed by
// `mark_clip_boundary` AND PTS-backstep auto-detect in `parse` — not dead code.
// CRA_NUT -> BLA_W_LP so NoRaslOutput discards dangling RASL). Armed by PTS-backstep
// auto-detect in `parse`, or by the public `mark_clip_boundary` hook (no in-tree caller).
pending_clip_boundary: bool,
// Highest PES PTS seen, on a monotonic 64-bit timeline (33-bit PTS unwrapped
// across 2^33 wraps — see `pts_wrap_offset`). Auto-detects a non-seamless clip
@@ -707,14 +707,20 @@ impl CodecParser for HevcParser {
// chromaFormat / bit depths — parse real values from the SPS RBSP; a
// hardcoded 8-bit 4:2:0 is wrong for 10-bit Main 10 UHD (nearly all UHD
// content). Fall back to it only if the SPS can't be parsed.
let chroma = parse_sps_chroma(sps).unwrap_or(SpsChroma {
chroma_format_idc: 1,
bit_depth_luma_minus8: 0,
bit_depth_chroma_minus8: 0,
max_sub_layers_minus1: 0,
temporal_id_nesting_flag: 0,
max_num_reorder_pics: None,
picture_period_ticks: None,
let chroma = parse_sps_chroma(sps).unwrap_or_else(|| {
tracing::warn!(
target: "freemkv::mux::hevc",
"HEVC SPS unparseable; hvcC declares 8-bit 4:2:0"
);
SpsChroma {
chroma_format_idc: 1,
bit_depth_luma_minus8: 0,
bit_depth_chroma_minus8: 0,
max_sub_layers_minus1: 0,
temporal_id_nesting_flag: 0,
max_num_reorder_pics: None,
picture_period_ticks: None,
}
});
// chromaFormat (6 reserved bits set + 2-bit chroma_format_idc)
record.push(0xFC | (chroma.chroma_format_idc & 0x03));
@@ -782,6 +788,7 @@ struct SpsChroma {
/// Reorder depth R of an HEVC SPS NAL (2-byte header included):
/// `sps_max_num_reorder_pics[sps_max_sub_layers_minus1]` (H.265 §7.3.2.2 \[I\]).
#[cfg(test)]
pub(crate) fn parse_sps_reorder(sps: &[u8]) -> Option<u32> {
parse_sps_chroma(sps)?.max_num_reorder_pics
}
@@ -3287,6 +3294,193 @@ mod tests {
assert_eq!(cp[18], 0xF8 | 2);
}
// PTL and the SPS fields are read off the emulation-prevention-STRIPPED RBSP; a real
// UHD SPS carries `00 00 03` inside general_constraint_indicator_flags.
#[test]
fn hvcc_reads_ptl_and_chroma_through_emulation_prevention_in_the_ptl() {
let sps = make_sps_with_chroma(1, 2, 2);
let mut rbsp = sps[2..].to_vec();
rbsp[1] = 0x21; // profile
rbsp[2] = 0x60; // compatibility flags 0x60000000
rbsp[6] = 0x90; // constraint flags 0x900000000000
rbsp[12] = 0x7B; // level_idc
let escaped = emulation_prevent(&rbsp);
assert!(escaped.len() > rbsp.len(), "the zero runs were escaped");
let mut stored = sps[..2].to_vec();
stored.extend_from_slice(&escaped);
assert_eq!(parse_sps_chroma(&stored).unwrap().chroma_format_idc, 1);
let cp = codec_private_from_sps(&stored);
assert_eq!(cp[1], 0x21);
assert_eq!(&cp[2..6], &[0x60, 0, 0, 0]);
assert_eq!(&cp[6..12], &[0x90, 0, 0, 0, 0, 0]);
assert_eq!(cp[12], 0x7B);
assert_eq!(cp[16], 0xFC | 1);
assert_eq!(cp[17], 0xF8 | 2);
assert_eq!(cp[18], 0xF8 | 2);
}
// The gap flag on a PES must reach the frame (both emit paths), or the resync gate never
// arms for HEVC.
#[test]
fn pes_discontinuity_propagates_to_frame() {
for reorder in [false, true] {
for flag in [true, false] {
let mut pes = make_pes(cra_au(&[0x10]), Some(0));
pes.discontinuity = flag;
let mut parser = HevcParser::new().with_ps_reorder(reorder);
let mut frames = parser.parse(&pes);
frames.extend(parser.flush());
assert_eq!(frames.len(), 1, "reorder {reorder}");
assert_eq!(frames[0].discontinuity, flag, "reorder {reorder}");
}
}
}
// Other SEI messages sharing the NAL (or an HDR10 message already captured) are skipped by
// their declared size, so the wanted message behind them is still found.
#[test]
fn hdr10_sei_is_found_behind_other_and_already_captured_messages() {
let idr = nal_bytes(19, &[0xEC]);
let pps = nal_bytes(NAL_PPS, &[0xC0]);
let mastering = |max_lum| {
sei_message(
SEI_MASTERING_DISPLAY_COLOUR_VOLUME,
&mastering_payload([1, 2, 3], [4, 5, 6], 7, 8, max_lum, 10),
)
};
let cll = sei_message(SEI_CONTENT_LIGHT_LEVEL_INFO, &cll_payload(1000, 400));
let other = |n: usize| sei_message(1, &vec![0x5A; n]);
let mut one_nal = HevcParser::new();
let mut au = pps.clone();
au.extend_from_slice(&sei_nal(&[
other(5),
mastering(10_000_000),
other(3),
cll.clone(),
]));
au.extend_from_slice(&idr);
one_nal.parse(&make_pes(au, Some(0)));
assert_eq!(
one_nal
.sei_mastering
.map(|m| m.max_display_mastering_luminance),
Some(10_000_000)
);
assert!(one_nal.sei_content_light.is_some());
let mut later = HevcParser::new();
let mut first = pps.clone();
first.extend_from_slice(&sei_nal(&[mastering(10_000_000)]));
first.extend_from_slice(&idr);
later.parse(&make_pes(first, Some(0)));
assert!(later.sei_content_light.is_none());
let mut second = pps.clone();
second.extend_from_slice(&sei_nal(&[mastering(1), other(4), cll.clone()]));
second.extend_from_slice(&idr);
later.parse(&make_pes(second, Some(3750)));
assert_eq!(
later
.sei_mastering
.map(|m| m.max_display_mastering_luminance),
Some(10_000_000),
"the repeat is skipped, not adopted"
);
assert!(
later.sei_content_light.is_some(),
"content light behind a skipped repeat"
);
}
#[test]
fn hdr10_sei_in_a_suffix_nal_is_captured() {
let mut sei = sei_nal(&[sei_message(
SEI_CONTENT_LIGHT_LEVEL_INFO,
&cll_payload(1000, 400),
)]);
sei[3] = NAL_SEI_SUFFIX << 1;
let mut au = nal_bytes(NAL_PPS, &[0xC0]);
au.extend_from_slice(&nal_bytes(19, &[0xEC]));
au.extend_from_slice(&sei);
let mut parser = HevcParser::new();
parser.parse(&make_pes(au, Some(0)));
assert!(parser.sei_content_light.is_some());
}
// Only the first slice segment of a picture carries the slice_type this reads; a later
// segment's bits would otherwise be taken for one.
#[test]
fn a_non_first_slice_segment_reports_no_coding_type() {
for nal_type in [1u8, 19] {
let nal = nal_bytes(nal_type, &[0x6C, 0x00]);
// 0x6C = first_slice_segment_in_pic_flag 0, then bits that would decode as an
// I slice (no_output_of_prior_pics 1, pps id 0, slice_type 2) if the flag were ignored.
assert_eq!(
hevc_first_slice_coding_type(&nal[3..], nal_type, |_| Some(0)),
None,
"NAL {nal_type}"
);
}
}
#[test]
fn hvcc_oversized_vps_or_pps_returns_none() {
for oversized in [32u8, 34] {
let mut data = Vec::new();
for t in [32u8, 33, 34] {
data.extend_from_slice(&[0x00, 0x00, 0x01]);
data.extend_from_slice(&hevc_nal_header(t));
let n = if t == oversized { 70_000 } else { 2 };
data.extend_from_slice(&vec![0x11u8; n]);
}
let mut parser = HevcParser::new();
parser.parse(&make_pes(data, Some(0)));
assert!(
parser.codec_private().is_none(),
"oversized NAL type {oversized} must not produce a truncated hvcC"
);
}
}
// A bare IRAP re-asserts the ACTIVE parameter sets, not the first-seen hvcC copy: a player
// re-applies hvcC at every keyframe, so reverting would undo a mid-title redefinition.
#[test]
fn a_bare_keyframe_reasserts_the_active_param_sets_not_the_hvcc_copy() {
let au = |sets: Option<u8>, slice: Vec<u8>| {
let mut d = Vec::new();
if let Some(b) = sets {
d.extend_from_slice(&nal_bytes(32, &[0xA0, b]));
d.extend_from_slice(&nal_bytes(33, &[0xB0, b]));
d.extend_from_slice(&nal_bytes(NAL_PPS, &[0xC0 | b]));
}
d.extend_from_slice(&slice);
d
};
let bodies = |fd: &[u8]| -> Vec<(u8, u8)> {
let mut out = Vec::new();
let mut off = 0;
while off + 4 <= fd.len() {
let len =
u32::from_be_bytes([fd[off], fd[off + 1], fd[off + 2], fd[off + 3]]) as usize;
out.push(((fd[off + 4] >> 1) & 0x3F, fd[off + 4 + len - 1]));
off += 4 + len;
}
out
};
let mut parser = HevcParser::new();
parser.parse(&make_pes(au(Some(1), nal_bytes(19, &[0xEC])), Some(0)));
// Redefinition of all three sets on a non-keyframe access unit.
let f = parser.parse(&make_pes(au(Some(2), nal_bytes(1, &[0xD0])), Some(3000)));
assert_eq!(bodies(&f[0].data).len(), 4);
let f = parser.parse(&make_pes(au(None, nal_bytes(19, &[0xEC])), Some(6000)));
assert_eq!(
bodies(&f[0].data),
vec![(32, 2), (33, 2), (34, 0xC2), (19, 0xEC)],
"VPS, SPS, PPS are the redefined ones, in hierarchy order"
);
}
// --- BitReader unit tests (exp-Golomb + bit reads) ---
#[test]
+104
View File
@@ -966,6 +966,30 @@ mod tests {
assert_eq!(got, pcm);
}
// Odd counts are coded with a pad channel: the payload_size field and the layout must say so,
// and the parser must read the bytes back.
#[test]
fn bd_payloads_pad_odd_channel_counts_and_round_trip() {
for channels in 1u8..=8 {
let h = bd_header(channels, 48_000, None, 24).unwrap();
let n = usize::from(channels);
let pcm: Vec<u8> = (0..5 * n * 3).map(|i| (i % 251) as u8 + 1).collect();
let parts = bd_payloads(&pcm, h);
assert_eq!(parts.len(), 1);
let coded = (n + n % 2) * 3;
let p = &parts[0].1;
assert_eq!(p.len(), 4 + 5 * coded, "{channels} ch");
assert_eq!(
usize::from(u16::from_be_bytes([p[0], p[1]])),
5 * coded,
"payload_size of {channels} ch"
);
assert_eq!(&p[2..4], &h);
let got = LpcmParser::new().parse(&make_pes(p.clone(), Some(0)));
assert_eq!(got[0].data, pcm, "{channels} ch round trip");
}
}
#[test]
fn bd_payloads_are_5ms_pes_like_ffmpeg_blurayenc() {
// 20000 stereo samples @ 48 kHz -> 83 full 240-sample PES + one of 80.
@@ -1091,6 +1115,86 @@ mod tests {
}
}
// The channel_assignment table: count and the reorder into WAVE channel order, by value.
#[test]
fn bd_channel_assignments_map_to_wave_order() {
// (assignment, source channel values in BD order, expected WAVE-order values)
let cases: [(u8, &[u8], &[u8]); 10] = [
(1, &[1], &[1]),
(3, &[1, 2], &[1, 2]),
(4, &[1, 2, 3], &[1, 2, 3]),
(5, &[1, 2, 3], &[1, 2, 3]),
(6, &[1, 2, 3, 4], &[1, 2, 3, 4]),
(7, &[1, 2, 3, 4], &[1, 2, 3, 4]),
(8, &[1, 2, 3, 4, 5], &[1, 2, 3, 4, 5]),
(9, &[1, 2, 3, 4, 5, 6], &[1, 2, 3, 6, 4, 5]),
(10, &[1, 2, 3, 4, 5, 6, 7], &[1, 2, 3, 5, 6, 4, 7]),
(11, &[1, 2, 3, 4, 5, 6, 7, 8], &[1, 2, 3, 8, 5, 6, 4, 7]),
];
for (assign, src, want) in cases {
// 16-bit, coded channels padded to even.
let mut pcm: Vec<u8> = src.iter().flat_map(|&c| [c, c]).collect();
if src.len() % 2 == 1 {
pcm.extend_from_slice(&[0, 0]);
}
let mut p = LpcmParser::new();
let f = p.parse(&make_pes(bd(assign, 1, &pcm), Some(0)));
let expect: Vec<u8> = want.iter().flat_map(|&c| [c, c]).collect();
assert_eq!(f[0].data, expect, "assignment {assign}");
}
}
// Timeline advance across a PTS-less PES uses the header's sample rate (10 ms of stereo
// 16-bit audio per packet).
#[test]
fn a_ptsless_packet_advances_by_the_headers_sample_rate() {
for (code, hz) in [(1u8, 48_000usize), (4, 96_000), (5, 192_000)] {
let mut pes = vec![0x00, 0x00, (3 << 4) | code, 1 << 6];
pes.extend(vec![0u8; hz / 100 * 4]);
let mut p = LpcmParser::new();
p.parse(&make_pes(pes.clone(), Some(0)));
let f = p.parse(&make_pes(pes, None));
assert_eq!(f[0].pts_ns, 10_000_000, "BD rate code {code} = {hz} Hz");
}
for (freq, hz) in [(0u8, 48_000usize), (1, 96_000), (2, 44_100), (3, 32_000)] {
let mut pes = vec![0x00, (freq << 4) | 1, 0x80];
pes.extend(vec![0u8; hz / 100 * 4]);
let mut p = LpcmParser::new_dvd();
p.parse(&make_pes(pes.clone(), Some(0)));
let f = p.parse(&make_pes(pes, None));
assert_eq!(f[0].pts_ns, 10_000_000, "DVD rate code {freq} = {hz} Hz");
}
}
// The M2TS re-mux header, against the pcm-blurayenc.c tables: assignment for each count,
// rate nibble, and the bits field.
#[test]
fn bd_header_encodes_assignment_rate_and_depth() {
for (channels, assign) in [
(1u8, 1u8),
(2, 3),
(3, 4),
(4, 6),
(5, 8),
(6, 9),
(7, 10),
(8, 11),
] {
for (hz, code) in [(48_000u32, 1u8), (96_000, 4), (192_000, 5)] {
assert_eq!(
bd_header(channels, hz, None, 24),
Some([(assign << 4) | code, 0xC0]),
"{channels} ch {hz} Hz"
);
}
}
assert_eq!(bd_header(2, 48_000, None, 16).unwrap()[1], 0x40);
assert_eq!(bd_header(9, 48_000, None, 24), None);
// A source layout byte is reused only when it agrees with the count and rate.
assert_eq!(bd_header(6, 48_000, Some(0x71), 24), Some([0x91, 0xC0]));
assert_eq!(bd_header(4, 48_000, Some(0x71), 24), Some([0x71, 0xC0]));
}
#[test]
fn dvd_bits_needed_is_the_smallest_lossless_depth() {
assert_eq!(dvd_bits_needed(&[1, 2, 0, 3, 4, 0]), 16);
+12 -12
View File
@@ -541,7 +541,7 @@ struct Composite {
/// What one stored frame says about its channels.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Frame {
pub(crate) enum ChannelFrame {
/// No valid 11172-3 header: not a frame this module can judge.
NoHeader,
/// A clean base frame whose multichannel extension verified (§2.5.3.1).
@@ -553,22 +553,22 @@ pub(crate) enum Frame {
}
/// Judges one stored frame against 11172-3 and 13818-3.
pub(crate) fn inspect(frame: &[u8]) -> Frame {
pub(crate) fn inspect(frame: &[u8]) -> ChannelFrame {
inspect_within(frame, frame.len() * 8)
}
// As `inspect`, reading no further than bit `end` (tests cut at exact field bits).
fn inspect_within(frame: &[u8], end: usize) -> Frame {
fn inspect_within(frame: &[u8], end: usize) -> ChannelFrame {
let Some(h) = Header::parse(frame) else {
return Frame::NoHeader;
return ChannelFrame::NoHeader;
};
let nch = h.nch();
let base = match walk_base(frame, &h, end) {
Ok(base) => base,
Err(why @ (Fallback::FrameCrc | Fallback::Truncated(_))) => {
return Frame::Damaged { nch, why };
return ChannelFrame::Damaged { nch, why };
}
Err(why) => return Frame::Base { nch, why },
Err(why) => return ChannelFrame::Base { nch, why },
};
// The frame's own length bounds mc_extension_data_part1 (11172-3 §2.4.3.1 slots).
let frame_end = h
@@ -576,8 +576,8 @@ fn inspect_within(frame: &[u8], end: usize) -> Frame {
.map_or(end, |n| (n * 8).min(end))
.min(frame.len() * 8);
match mc_extension(frame, &h, &base, frame_end) {
Ok(mc) => Frame::Multichannel { nch, mc },
Err(why) => Frame::Base { nch, why },
Ok(mc) => ChannelFrame::Multichannel { nch, mc },
Err(why) => ChannelFrame::Base { nch, why },
}
}
@@ -737,7 +737,7 @@ impl ChannelTracker {
}
self.frames += 1;
match inspect(frame) {
Frame::Multichannel { nch, mc } => {
ChannelFrame::Multichannel { nch, mc } => {
self.base = Some(nch);
// §2.5.2.13: "'1' extension bit stream present" - the rest is not in the track.
let count = if mc.ext_bit_stream_present {
@@ -757,18 +757,18 @@ impl ChannelTracker {
}
}
// A clean frame without a verified mc_header (§2.5.3.1) breaks the run.
Frame::Base { nch, why } => {
ChannelFrame::Base { nch, why } => {
self.base = Some(nch);
self.last = Some(why);
self.run = None;
}
// A failed frame CRC covers the header too, so its nch is not trusted; like a
// header-less frame it breaks the run, which must be consecutive.
Frame::Damaged { why, .. } => {
ChannelFrame::Damaged { why, .. } => {
self.last = Some(why);
self.run = None;
}
Frame::NoHeader => self.run = None,
ChannelFrame::NoHeader => self.run = None,
}
if self.frames >= MAX_FRAMES && self.run.is_none() {
self.settled = true;
+15 -12
View File
@@ -383,7 +383,7 @@ fn mc(centre: u8, surround: u8, lfe: bool) -> Mc {
fn multichannel(f: &[u8]) -> Option<McHeader> {
match inspect(f) {
Frame::Multichannel { mc, .. } => Some(mc),
ChannelFrame::Multichannel { mc, .. } => Some(mc),
_ => None,
}
}
@@ -730,7 +730,7 @@ fn ffmpeg_encoded_frames_walk_to_the_end_of_every_frame() {
f.len() * 8 - base.mc_start
);
// §2.5.3.1: no valid mc_crc_check in MPEG-1 frames, so no multichannel.
assert!(matches!(inspect(f), Frame::Base { nch: c, .. } if c == nch));
assert!(matches!(inspect(f), ChannelFrame::Base { nch: c, .. } if c == nch));
n += 1;
}
}
@@ -753,7 +753,10 @@ fn frame_crc_is_checked_when_protection_bit_is_zero() {
};
// §2.4.3.1: header bits "starting with bit_rate_index and ending with emphasis" plus the
// Table 3-B.5 audio_data bits (allocation and scfsi for Layer II).
assert!(matches!(inspect(&frame(crc)), Frame::Base { nch: 2, .. }));
assert!(matches!(
inspect(&frame(crc)),
ChannelFrame::Base { nch: 2, .. }
));
let bad = Spec {
bad_frame_crc: true,
..crc
@@ -761,7 +764,7 @@ fn frame_crc_is_checked_when_protection_bit_is_zero() {
// "If the words are not identical, a transmission error has occured in the protected field".
assert_eq!(
inspect(&frame(bad)),
Frame::Damaged {
ChannelFrame::Damaged {
nch: 2,
why: Fallback::FrameCrc
}
@@ -770,7 +773,7 @@ fn frame_crc_is_checked_when_protection_bit_is_zero() {
flipped[3] ^= 0x01; // emphasis bit: inside the protected header bits
assert_eq!(
inspect(&flipped),
Frame::Damaged {
ChannelFrame::Damaged {
nch: 2,
why: Fallback::FrameCrc
}
@@ -792,7 +795,7 @@ fn multichannel_needs_a_valid_mc_crc_check() {
};
assert_eq!(
inspect(&frame(bad)),
Frame::Base {
ChannelFrame::Base {
nch: 2,
why: Fallback::McCrc
}
@@ -877,7 +880,7 @@ fn centre_10_is_not_defined_and_falls_back() {
// §2.5.2.13 centre: "'10' not defined".
assert_eq!(
inspect(&f),
Frame::Base {
ChannelFrame::Base {
nch: 2,
why: Fallback::UndefinedCentre
}
@@ -929,7 +932,7 @@ fn ext_one_mc_data_beyond_the_base_part_cannot_be_verified() {
let f = frame(with_mc(STEREO_256, huge));
assert!(matches!(
inspect(&f),
Frame::Base {
ChannelFrame::Base {
why: Fallback::Truncated(_),
..
}
@@ -1198,7 +1201,7 @@ fn unsupported_frames_give_the_header_count() {
),
];
for (s, why) in cases {
assert_eq!(inspect(&frame(s)), Frame::Base { nch: 2, why });
assert_eq!(inspect(&frame(s)), ChannelFrame::Base { nch: 2, why });
}
}
@@ -1256,13 +1259,13 @@ fn truncation_at_each_field_boundary_names_the_field() {
f => f,
});
let want = if base_field {
Frame::Damaged { nch: 2, why }
ChannelFrame::Damaged { nch: 2, why }
} else {
Frame::Base { nch: 2, why }
ChannelFrame::Base { nch: 2, why }
};
assert_eq!(got, want, "{field}");
}
assert_eq!(inspect(&f[..3]), Frame::NoHeader);
assert_eq!(inspect(&f[..3]), ChannelFrame::NoHeader);
}
/// Opus defect 1: a failed first frame must not lock in the base count.
+54 -6
View File
@@ -33,13 +33,12 @@ const PICTURE_TYPE_I: u8 = 1;
#[cfg(test)]
const MAX_AU_BUFFER: usize = 8 * 1024 * 1024;
/// Cap on frames held awaiting the first PES PTS anchor. A DVD stamps a PTS in
/// the first VOBU (~0.5 s ≈ 15 frames); this leaves generous slack. If no PTS
/// ever arrives within the cap, buffered frames are released on a 0 base.
/// Cap on frames buffered in one GOP. A DVD GOP is ~15 frames; a run reaching the
/// cap is force-flushed as its own GOP, and each split re-locks its origin and
/// display-order PTS from the frames it holds.
const MAX_PENDING_FRAMES: usize = 600;
// Byte cap on frames held awaiting the first PES PTS anchor; mirrors the AC-3/DTS/PGS byte
// caps.
// Byte cap on one buffered GOP; mirrors the AC-3/DTS/PGS byte caps.
const MAX_PENDING_BYTES: usize = 8 * 1024 * 1024;
/// Frame rate table (index from sequence header frame_rate_code).
@@ -451,7 +450,7 @@ fn picture_coding_flags(au: &[u8]) -> (bool, bool, bool, bool) {
/// codec-agnostic [`CodingType`]: 1 → I, 3 → B, else (2 = P, 4 = D) → P.
fn coding_type_from_raw(raw: u8) -> CodingType {
match raw {
1 => CodingType::I,
PICTURE_TYPE_I => CodingType::I,
3 => CodingType::B,
_ => CodingType::P,
}
@@ -541,6 +540,55 @@ mod tests {
}
}
#[test]
fn a_second_top_field_starts_a_new_pair_instead_of_partnering_the_first() {
use crate::mux::codec::coding::FieldOrder;
let mut p = Mpeg2Parser::new();
let mut frames = Vec::new();
// top, top, bottom: the bottom field pairs with the SECOND top field.
for (i, (ct, e2)) in [(1u8, 0x01u8), (2, 0x01), (2, 0x02)]
.into_iter()
.enumerate()
{
let mut au = Vec::new();
if i == 0 {
au.extend_from_slice(&make_seq_header(720, 576, 3, 3));
}
au.extend_from_slice(&make_picture_header(ct));
let mut ext = pic_coding_ext(0, 0, 0, false);
ext[6] = e2;
au.extend_from_slice(&ext);
frames.extend(p.parse(&make_pes(au, None)));
}
frames.extend(p.flush());
assert_eq!(frames.len(), 3);
for f in &frames {
assert_eq!(f.coding.unwrap().field_order(), Some(FieldOrder::Tff));
}
}
#[test]
fn a_pts_jump_in_a_later_gop_relocks_the_timeline_origin() {
// 25 fps = 40 ms. GOP 1 anchors at 0; GOP 2 carries a PES PTS of 10 s
// (a clip boundary), so its frames follow it, not GOP 1's extrapolation.
let mut p = Mpeg2Parser::new();
let mut g1 = make_seq_header(720, 480, 3, 3);
g1.extend_from_slice(&gop());
g1.extend_from_slice(&make_picture_header_tr(1, 0));
g1.extend_from_slice(&[0xAA; 10]);
g1.extend_from_slice(&make_picture_header_tr(2, 1));
g1.extend_from_slice(&[0xBB; 10]);
let mut frames = p.parse(&make_pes(g1, Some(0)));
let mut g2 = gop();
g2.extend_from_slice(&make_picture_header_tr(1, 0));
g2.extend_from_slice(&[0xCC; 10]);
frames.extend(p.parse(&make_pes(g2, Some(10 * 90_000))));
frames.extend(p.flush());
assert_eq!(frames.len(), 3);
assert_eq!(frames[1].pts_ns, 40_000_000);
assert_eq!(frames[2].pts_ns, 10_000_000_000);
}
#[test]
fn gop_header_resets_stale_lone_field() {
use crate::mux::codec::coding::FieldOrder;
+25
View File
@@ -421,6 +421,31 @@ mod tests {
}
}
// A learned free-format size excludes padding: a padded frame (padding bit set) is one slot
// longer, so the next header is looked for one byte later.
#[test]
fn free_format_frames_with_alternating_padding_keep_their_exact_bytes() {
let plain = free_frame();
let mut padded = free_frame();
padded[2] |= 0x02; // padding bit
padded.push(0xAA); // Layer III padding is one slot
let (plain, padded) = (plain.as_slice(), padded.as_slice());
// Starting on a padded frame learns the size from a padded spacing.
for frames in [
[plain, padded, plain, padded, plain, padded],
[padded, plain, padded, plain, padded, plain],
] {
let mut p = MpegAudioParser::new();
let mut f = p.parse(&make_pes(frames.concat(), Some(0)));
f.extend(p.flush());
assert_eq!(f.len(), 6);
for (got, want) in f.iter().zip(frames) {
assert_eq!(got.data, want);
}
assert_eq!(p.dropped_frames(), 0);
}
}
#[test]
fn free_format_frame_split_across_pes_reassembles() {
let data = free_frame().repeat(2);
+86
View File
@@ -333,6 +333,7 @@ impl CodecParser for PgsParser {
// let it fall to the non-PCS arm (would pollute the pending set): close
// any pending set with a fallback and drop the header to resync on next PCS.
if is_pcs && pes.data.len() <= PCS_NUM_OBJECTS_OFFSET {
self.skip_to_pcs = true;
return self.emit_pending(None).into_iter().collect();
}
@@ -350,6 +351,17 @@ impl CodecParser for PgsParser {
Some(_) => match pts {
Some(start) => {
self.skip_to_pcs = false;
// A gap before this PCS cut the held set off from its END;
// closing it would emit a malformed display set.
if pes.discontinuity
&& !self
.pending
.as_ref()
.is_some_and(|(_, d)| Self::ends_with_end(d))
{
self.pending = None;
}
self.clear_scan_offset = 0;
out.extend(self.emit_pending(Some(start)));
// The set's facts are THIS packet's — the one that opened
// it. `start` is that packet's PTS by construction.
@@ -359,6 +371,7 @@ impl CodecParser for PgsParser {
// store it with a 0 sentinel (wrong start, absurd duration).
// Flush any prior pending with a fallback and skip this one.
None => {
self.skip_to_pcs = true;
out.extend(self.emit_pending(None));
}
},
@@ -377,6 +390,7 @@ impl CodecParser for PgsParser {
out.extend(self.emit_pending(None));
}
self.pending = None;
self.clear_scan_offset = 0;
self.skip_to_pcs = true;
}
if self.skip_to_pcs {
@@ -634,6 +648,66 @@ mod tests {
assert!(parser.flush().is_empty());
}
#[test]
fn pcs_after_gap_drops_an_incomplete_pending_set() {
let mut parser = PgsParser::new();
let _ = parser.parse(&make_pes(pcs_bytes(1), Some(90000)));
let mut next = make_pes(pcs_bytes(1), Some(180000));
next.discontinuity = true;
assert!(
parser.parse(&next).is_empty(),
"a set cut off before its END is not emitted"
);
let tail = parser.flush();
assert_eq!(tail.len(), 1, "the post-gap PCS opens a fresh set");
assert_eq!(tail[0].pts_ns, 2_000_000_000);
}
#[test]
fn pcs_after_gap_emits_a_complete_pending_set() {
let mut parser = PgsParser::new();
let _ = parser.parse(&make_pes(pcs_bytes(1), Some(90000)));
let _ = parser.parse(&make_pes(vec![0x80, 0x00, 0x00], Some(90000)));
let mut next = make_pes(pcs_bytes(1), Some(180000));
next.discontinuity = true;
let out = parser.parse(&next);
assert_eq!(out.len(), 1);
assert_eq!(out[0].duration_ns, Some(1_000_000_000));
}
#[test]
fn segments_after_a_dropped_pcs_are_skipped_until_the_next_pcs() {
let mut short_pcs = pcs_bytes(1);
short_pcs.truncate(PCS_NUM_OBJECTS_OFFSET);
for (dropped, pts) in [(short_pcs, Some(90000)), (pcs_bytes(1), None)] {
let mut parser = PgsParser::new();
assert!(parser.parse(&make_pes(dropped, pts)).is_empty());
for seg in [
vec![0x17, 0x00, 0x00],
vec![0x15, 0x00, 0x02, 0xAA, 0xBB],
vec![0x80, 0x00, 0x00],
] {
assert!(
parser.parse(&make_pes(seg, Some(90000))).is_empty(),
"orphan segments of a lost PCS are not emitted"
);
}
let _ = parser.parse(&make_pes(pcs_bytes(1), Some(180000)));
assert_eq!(parser.flush().len(), 1, "the next PCS resyncs");
}
}
#[test]
fn a_gap_resets_the_clear_scan_offset() {
let mut parser = PgsParser::new();
let _ = parser.parse(&make_pes(pcs_bytes(0), Some(90000)));
assert_ne!(parser.clear_scan_offset, 0, "the clear PCS was walked");
let mut gap = make_pes(vec![0x17, 0x00, 0x00], None);
gap.discontinuity = true;
let _ = parser.parse(&gap);
assert_eq!(parser.clear_scan_offset, 0);
}
#[test]
fn pending_buffer_is_capped() {
let mut parser = PgsParser::new();
@@ -976,6 +1050,18 @@ mod tests {
}
}
// The share test multiplies a disc-derived count; it must not wrap at the top of u32.
#[test]
fn the_share_test_does_not_overflow_on_huge_counts() {
assert!(demotable(facts(u32::MAX, 1), true, u32::MAX));
assert!(demotable(facts(u32::MAX / 2, 1), true, u32::MAX));
assert!(!demotable(
facts(DEMOTE_MIN_DISPLAY_SETS, 1),
true,
u32::MAX
));
}
// A track that mixes forced and non-forced sets needs no sibling corroboration: the flag is
// in use ON THIS TRACK.
#[test]
+21
View File
@@ -357,6 +357,27 @@ mod tests {
assert_eq!(&got[..4], &[0, dur, 2 * dur, 3 * dur]);
}
// The slots between two anchors exclude the held GOP's frames displayed before its own anchor.
#[test]
fn calibration_discounts_the_held_gops_leading_display_offset() {
use CodingType::*;
let dur = 40_000_000i64;
let mut r = SparsePtsReorder::new();
let mut got: Vec<i64> = Vec::new();
// Open first GOP, decode I B B P: the I displays third (slot 2), the P fourth.
for (k, ct) in [I, B, B, P].into_iter().enumerate() {
let p = (k == 0).then_some(2 * dur);
got.extend(r.push(p, frame(ct, k == 0)).iter().map(|f| f.pts_ns));
}
// The next GOP's I is anchored two slots after the first anchor.
for (k, ct) in [I, P].into_iter().enumerate() {
let p = (k == 0).then_some(4 * dur);
got.extend(r.push(p, frame(ct, k == 0)).iter().map(|f| f.pts_ns));
}
got.extend(r.flush().iter().map(|f| f.pts_ns));
assert_eq!(&got[..4], &[2 * dur, 0, dur, 3 * dur]);
}
#[test]
fn no_pts_collisions_within_a_gop() {
use CodingType::*;
+5 -3
View File
@@ -36,9 +36,9 @@ const AU_DURATION_NS: i64 = 833_333;
// kHz constant would run ~8.95% fast on these (rare) streams.
const AU_DURATION_NS_441: i64 = 907_029;
// Hard cap on the reassembly buffer: a valid AU is well under 32 KiB, so
// past this the stream is malformed — drop and resync. Parity with the
// AC-3/DTS/PGS caps.
// Backstop on the reassembly buffer, parity with the AC-3/DTS/PGS caps. The parse loop
// already drains every complete AU, so the residue never exceeds one AU (8190 bytes) and
// this does not fire today; it bounds memory if that loop changes.
const MAX_TRUEHD_BUF: usize = 256 * 1024;
pub struct TrueHdParser {
@@ -1495,6 +1495,8 @@ mod tests {
assert_eq!(truehd_lfe(0x4B), 0, "7.0 has no LFE");
assert_eq!(truehd_lfe(0xF << 15), 1, "6ch LFE bit when 8ch is empty");
assert_eq!(truehd_lfe(0x03 | (0xF << 15)), 0, "8ch wins over 6ch");
assert_eq!(truehd_lfe(0x1B << 15), 0, "6ch mask with every bit but LFE");
assert_eq!(truehd_lfe(0x04 << 15), 1, "6ch LFE bit alone");
assert_eq!(truehd_lfe(0), 0);
}
+57
View File
@@ -486,6 +486,63 @@ mod tests {
assert!(raw_during.iter().filter(|f| f.pts_ns == 0).count() >= 4);
}
// The PES PTS anchors the reconstructed display timeline: with anchors 5 frames apart the
// frame duration calibrates to a fifth of the spacing, and each GOP's frames follow.
#[test]
fn vc1_ps_reorder_stamps_display_order_pts_from_the_pes_anchors() {
let seq = [0x00, 0x00, 0x01, SC_SEQUENCE_HEADER, 0xC0, 0, 0, 0, 0, 0];
let pic = |ptype: u8| vec![0x00, 0x00, 0x01, SC_FRAME, ptype];
let mut p = Vc1Parser::new().with_ps_reorder(true);
let mut frames = Vec::new();
for anchor in [0i64, 18_750] {
for (k, ptype) in [0xC0u8, 0x00, 0x80, 0x00, 0x80].into_iter().enumerate() {
let data = if k == 0 {
[&seq[..], &pic(ptype)].concat()
} else {
pic(ptype)
};
frames.extend(p.parse(&make_pes(data, (k == 0).then_some(anchor))));
}
}
frames.extend(p.flush());
let d = 208_333_333 / 5;
let gop = |origin: i64| [0, 2, 1, 4, 3].map(|i| origin + i * d);
let want: Vec<i64> = gop(0).into_iter().chain(gop(208_333_333)).collect();
let got: Vec<i64> = frames.iter().map(|f| f.pts_ns).collect();
assert_eq!(got, want);
}
// An advanced-profile sequence header may carry `00 00 03` emulation prevention; the
// resolution and INTERLACE bit are read from the de-escaped bytes.
#[test]
fn sequence_header_fields_are_read_through_emulation_prevention() {
// De-escaped: PROFILE 3 byte, 00, then W-1 = 0 (12 bits), H-1 = 31 (12 bits),
// then INTERLACE set at bit 41. Bytes 1..=3 are 00 00 00, so an encoder escapes them.
let sh = [
0x00,
0x00,
0x01,
SC_SEQUENCE_HEADER,
0xC0,
0x00,
0x00,
0x03,
0x00,
0x1F,
0x40,
];
assert_eq!(parse_vc1_resolution(&sh), Some((2, 64)));
assert_eq!(parse_vc1_interlace(&sh), Some(true));
}
#[test]
fn a_pes_ending_in_a_bare_start_code_prefix_passes_through_without_panicking() {
let data = vec![0xAA, 0x00, 0x00, 0x01];
let f = Vc1Parser::new().parse(&make_pes(data.clone(), Some(0)));
assert_eq!(f.len(), 1);
assert_eq!(f[0].data, data);
}
/// Build a VC-1 PES with sequence header + entry point + frame start code.
fn build_vc1_iframe_pes() -> Vec<u8> {
let mut data = Vec::new();