mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
Merge branch 'audit-lib-G5' into audit-fix-lib
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(¬_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();
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user