0.31.0: hardening and correctness pass across mux, codec, AACS/CSS, UDF/MPLS/CLPI, recovery, drive/SCSI, labels, and I/O
Library-wide review-and-fix pass: tightened AACS keydb/handshake/variant handling and trailing-partial-unit policy, corrected MPLS mark offset and added UDF allocation bounds, hardened the mux/codec framing and M2TS paths, guarded SCSI READ CAPACITY short transfers and unified error mapping, added overflow guards on untrusted disc input, and made prefetch shutdown deterministic. Release profile now builds with thin LTO + single codegen unit.
This commit is contained in:
+89
-20
@@ -7,10 +7,22 @@
|
||||
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
|
||||
|
||||
/// Sample rates indexed by fscod (0=48kHz, 1=44.1kHz, 2=32kHz). fscod=3 is
|
||||
/// reserved in AC-3 and signals "fscod2" (reduced rates) in E-AC-3; we treat
|
||||
/// the base rate as 48 kHz in that case for duration purposes.
|
||||
/// reserved in AC-3; in E-AC-3 it signals "fscod2" (reduced rates: 24/22.05/16
|
||||
/// kHz, selected by byte-4 bits [5:4]). `frame_sample_rate` decodes fscod2 in
|
||||
/// the E-AC-3 case; this table's index-3 entry (48 kHz) is only the fallback
|
||||
/// when the header is too short to read fscod2.
|
||||
const SAMPLE_RATES: [u32; 4] = [48_000, 44_100, 32_000, 48_000];
|
||||
|
||||
/// E-AC-3 reduced sample rates indexed by fscod2 (byte-4 bits [5:4]), used when
|
||||
/// fscod==3. Index 3 is reserved; we fall back to 48 kHz for it.
|
||||
const EAC3_REDUCED_RATES: [u32; 4] = [24_000, 22_050, 16_000, 48_000];
|
||||
|
||||
/// Minimum byte length of a valid (E-)AC-3 frame. A real E-AC-3 frame must carry
|
||||
/// at least the syncword (2) + BSI header (~4) before any audio. `eac3_frame_size`
|
||||
/// returns `(frmsiz + 1) * 2`, so frmsiz=0/1 yield 2/4-byte "frames" that are
|
||||
/// sub-header junk; rejecting anything below this guards against emitting them.
|
||||
const MIN_FRAME_BYTES: usize = 6;
|
||||
|
||||
/// AC-3 (legacy) always carries 6 audio blocks × 256 samples = 1536 samples.
|
||||
const AC3_SAMPLES_PER_FRAME: u32 = 1536;
|
||||
|
||||
@@ -89,8 +101,9 @@ impl CodecParser for Ac3Parser {
|
||||
ac3_frame_size(remaining)
|
||||
};
|
||||
|
||||
if frame_size == 0 || frame_size > 8192 {
|
||||
// Invalid frame size — skip this sync word
|
||||
if !(MIN_FRAME_BYTES..=8192).contains(&frame_size) {
|
||||
// Invalid/sub-header frame size (e.g. an E-AC-3 frmsiz of 0/1
|
||||
// sizing to a 2/4-byte fragment) — skip this sync word.
|
||||
pos = start + 2;
|
||||
continue;
|
||||
}
|
||||
@@ -112,13 +125,15 @@ impl CodecParser for Ac3Parser {
|
||||
}
|
||||
|
||||
// Keep unconsumed data for the next call. `pos` is the start of the
|
||||
// unconsumed region: either a partial frame that straddles this PES
|
||||
// boundary — which, by construction, begins at a syncword (every byte
|
||||
// before `pos` was emitted as a frame or skipped as pre-sync junk) — or
|
||||
// trailing bytes too short to size/complete a frame. Carry from `pos`,
|
||||
// NOT from the next syncword: discarding bytes between `pos` and the
|
||||
// next sync would drop the partial frame we are deliberately keeping
|
||||
// across the boundary.
|
||||
// last unprocessed search region. On the `start + frame_size > len`
|
||||
// break it sits exactly at the straddling frame's syncword; on the
|
||||
// `remaining.len() < 6` break it is the value from the top of that
|
||||
// iteration, with the syncword possibly sitting after some pre-sync
|
||||
// junk — so the re-scan below (from `pos`, NOT a recomputed sync) is
|
||||
// required to locate the carry-over syncword. Carry from `pos`, NOT
|
||||
// from the next syncword: discarding bytes between `pos` and the next
|
||||
// sync would drop the partial frame we are deliberately keeping across
|
||||
// the boundary.
|
||||
let keep_from = if pos < data.len() {
|
||||
// A syncword at/after `pos` marks the carry-over start (anything
|
||||
// before it is junk with no sync). With no full sync, retain the
|
||||
@@ -139,6 +154,11 @@ impl CodecParser for Ac3Parser {
|
||||
// No frame could be parsed out of a buffer this large — this is
|
||||
// not valid AC-3 here. Drop it and resync on the next PES rather
|
||||
// than grow without bound on pathological input.
|
||||
tracing::debug!(
|
||||
target: "mux",
|
||||
"ac3: carry-over buffer exceeded {} bytes without a frame; dropping and resyncing",
|
||||
MAX_AC3_BUF
|
||||
);
|
||||
self.buf.clear();
|
||||
} else {
|
||||
self.buf = tail.to_vec();
|
||||
@@ -174,7 +194,7 @@ impl CodecParser for Ac3Parser {
|
||||
} else {
|
||||
ac3_frame_size(frame)
|
||||
};
|
||||
if frame_size == 0 || frame_size > 8192 || off + frame_size > buf.len() {
|
||||
if !(MIN_FRAME_BYTES..=8192).contains(&frame_size) || off + frame_size > buf.len() {
|
||||
return Vec::new();
|
||||
}
|
||||
let duration_ns = frame_duration_ns(frame, bsid);
|
||||
@@ -212,12 +232,20 @@ fn eac3_samples_per_frame(data: &[u8]) -> u32 {
|
||||
numblks * 256
|
||||
}
|
||||
|
||||
/// Sample rate (Hz) of an AC-3/E-AC-3 frame from its fscod field (byte 4 bits 7-6).
|
||||
fn frame_sample_rate(data: &[u8]) -> u32 {
|
||||
/// Sample rate (Hz) of an AC-3/E-AC-3 frame from its fscod field (byte 4 bits
|
||||
/// 7-6). For E-AC-3 (`bsid >= 11`) an fscod of 3 selects a reduced rate via
|
||||
/// fscod2 (byte 4 bits [5:4]); decoding it keeps the frame duration correct
|
||||
/// instead of mistiming reduced-rate frames at 48 kHz (A/V drift).
|
||||
fn frame_sample_rate(data: &[u8], bsid: u8) -> u32 {
|
||||
if data.len() < 5 {
|
||||
return SAMPLE_RATES[0];
|
||||
}
|
||||
SAMPLE_RATES[((data[4] >> 6) & 0x03) as usize]
|
||||
let fscod = (data[4] >> 6) & 0x03;
|
||||
if fscod == 0x03 && bsid >= 11 {
|
||||
let fscod2 = (data[4] >> 4) & 0x03;
|
||||
return EAC3_REDUCED_RATES[fscod2 as usize];
|
||||
}
|
||||
SAMPLE_RATES[fscod as usize]
|
||||
}
|
||||
|
||||
/// Duration of one AC-3/E-AC-3 frame in nanoseconds: samples_per_frame /
|
||||
@@ -228,7 +256,7 @@ fn frame_duration_ns(data: &[u8], bsid: u8) -> u64 {
|
||||
} else {
|
||||
AC3_SAMPLES_PER_FRAME
|
||||
} as u64;
|
||||
let rate = frame_sample_rate(data) as u64;
|
||||
let rate = frame_sample_rate(data, bsid) as u64;
|
||||
// samples / rate seconds → ns, rounded to nearest.
|
||||
(samples * 1_000_000_000 + rate / 2) / rate
|
||||
}
|
||||
@@ -240,7 +268,7 @@ fn find_ac3_sync(data: &[u8]) -> Option<usize> {
|
||||
|
||||
/// Extract bsid from an AC-3/E-AC-3 frame starting at the syncword.
|
||||
/// bsid is at byte 5, bits 7..3.
|
||||
pub fn get_bsid(data: &[u8]) -> u8 {
|
||||
fn get_bsid(data: &[u8]) -> u8 {
|
||||
if data.len() < 6 {
|
||||
return 0;
|
||||
}
|
||||
@@ -256,8 +284,11 @@ fn eac3_frame_size(data: &[u8]) -> usize {
|
||||
(frmsiz + 1) * 2
|
||||
}
|
||||
|
||||
/// Calculate AC-3 frame size in bytes from fscod and frmsizecod.
|
||||
fn ac3_frame_size(data: &[u8]) -> usize {
|
||||
/// Calculate AC-3 frame size in bytes from fscod and frmsizecod. Returns 0 for
|
||||
/// an unmappable header (reserved fscod==3, or frmsizecod out of table range).
|
||||
/// `pub(crate)` so the TrueHD parser can reuse it when skipping interleaved AC-3
|
||||
/// frames instead of duplicating the size table.
|
||||
pub(crate) fn ac3_frame_size(data: &[u8]) -> usize {
|
||||
if data.len() < 5 {
|
||||
return 0;
|
||||
}
|
||||
@@ -439,7 +470,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn buffer_stays_bounded_across_many_garbage_pes() {
|
||||
// Finding 14: the carry-over buffer must never grow without bound. Feed
|
||||
// The carry-over buffer must never grow without bound. Feed
|
||||
// many large PES packets that contain no usable frame and assert the
|
||||
// retained buffer stays tiny — carry-from-`pos` drops all pre-sync junk,
|
||||
// and a never-completing frame is bounded by the 8192-byte frame cap and
|
||||
@@ -573,6 +604,44 @@ mod tests {
|
||||
assert_eq!(frame_duration_ns(&frame, bsid), 32_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eac3_subheader_sized_frame_is_rejected() {
|
||||
// An E-AC-3 sync with frmsiz=0 sizes to a 2-byte "frame"; frmsiz=1 to
|
||||
// 4 bytes. Both are sub-header junk that must NOT be emitted as audio.
|
||||
// bsid must be >= 11 for the E-AC-3 sizing path. Byte 5 bits 7..3 = bsid.
|
||||
let mut parser = Ac3Parser::new();
|
||||
// Build an E-AC-3 sync: 0x0B 0x77, frmsiz=0 (bytes 2-3 low bits = 0),
|
||||
// bsid=16 (>=11) at byte 5. Pad to a few bytes so find_ac3_sync + sizing
|
||||
// run. eac3_frame_size = (0 + 1) * 2 = 2 < MIN_FRAME_BYTES.
|
||||
let mut data = vec![0x0B, 0x77, 0x00, 0x00, 0x00, 16 << 3, 0x00, 0x00];
|
||||
// Append a real AC-3 frame after the junk so we can confirm the parser
|
||||
// resyncs past the junk and still emits the valid frame.
|
||||
let good = make_ac3_frame(0, 2);
|
||||
data.extend_from_slice(&good);
|
||||
let pes = PesPacket {
|
||||
pid: 0,
|
||||
pts: Some(90000),
|
||||
dts: None,
|
||||
data,
|
||||
};
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1, "only the real AC-3 frame is emitted");
|
||||
assert_eq!(frames[0].data.len(), 160);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eac3_fscod2_reduced_rate_duration() {
|
||||
// E-AC-3 with fscod==3 (reduced rate) and fscod2==0 → 24 kHz, not 48.
|
||||
// bsid>=11 selects the E-AC-3 path. When fscod==3 the block count is
|
||||
// fixed at 6 → 1536 samples. Byte 4 layout: fscod(2)|fscod2(2)|...
|
||||
// fscod=3 (0b11), fscod2=0 (0b00) → byte4 = 0b1100_0000 = 0xC0.
|
||||
let data = [0x0B, 0x77, 0x00, 0x00, 0xC0, 16 << 3];
|
||||
let bsid = get_bsid(&data);
|
||||
assert!(bsid >= 11, "test frame is E-AC-3");
|
||||
// 1536 samples / 24000 Hz = 64 ms.
|
||||
assert_eq!(frame_duration_ns(&data, bsid), 64_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ac3_frame_size_table() {
|
||||
// fscod=0 (48kHz), frmsizecod=0: 64 words = 128 bytes
|
||||
|
||||
+87
-21
@@ -15,6 +15,11 @@ const DTS_CORE_SYNC: [u8; 4] = [0x7F, 0xFE, 0x80, 0x01];
|
||||
#[cfg(test)]
|
||||
const DTS_HD_EXT_SYNC: [u8; 4] = [0x64, 0x58, 0x20, 0x25];
|
||||
|
||||
/// DTS / DTS-HD elementary-stream parser. Buffers DTS across PES boundaries so
|
||||
/// a core frame plus all of its trailing DTS-HD extension substreams are
|
||||
/// emitted together as one access unit, delimited by the next valid core sync.
|
||||
/// This preserves the lossless extension data instead of downgrading to lossy
|
||||
/// core (the Dunkirk / Fight Club lossy-core bug).
|
||||
pub struct DtsParser {
|
||||
buf: Vec<u8>,
|
||||
/// PTS of the access unit currently being assembled in `buf` (the unit
|
||||
@@ -92,11 +97,22 @@ impl DtsParser {
|
||||
/// this without a clean boundary we resync rather than stall or balloon.
|
||||
const MAX_AU_BYTES: usize = 65536;
|
||||
|
||||
/// Minimum plausible DTS core frame size. The core header alone is ~10-14
|
||||
/// bytes; a decoded `core_size` below this means we matched a false/corrupt
|
||||
/// core sync (the 14-bit `fsize` field decoded to a tiny value) rather than a
|
||||
/// real frame, so we resync instead of emitting a junk access unit.
|
||||
const MIN_CORE_FRAME_BYTES: usize = 10;
|
||||
/// Number of leading bytes that must be buffered before the core `fsize` field
|
||||
/// (bytes 5-7) can be decoded. This is a HEADER-LAYOUT minimum — "enough bytes
|
||||
/// to read the size field" — and is deliberately distinct from
|
||||
/// `MIN_CORE_FRAME_BYTES` (the decoded-frame-size validity floor). They must not
|
||||
/// be conflated: this one gates buffer reads of the header, the other rejects
|
||||
/// implausible decoded sizes.
|
||||
const CORE_HEADER_MIN_BYTES: usize = 10;
|
||||
|
||||
/// Minimum plausible decoded DTS core frame size, per ETSI TS 102 114: the
|
||||
/// on-wire FSIZE floor is 95, so a real core frame is at least 96 bytes. A
|
||||
/// decoded `core_size` below this means we matched a false/corrupt core sync
|
||||
/// (a lucky 0x7FFE8001 in extension-substream payload whose 14-bit `fsize`
|
||||
/// decoded to a tiny value) rather than a real frame, so we resync instead of
|
||||
/// closing an access unit at a junk boundary and dropping the DTS-HD extension
|
||||
/// tail.
|
||||
const MIN_CORE_FRAME_BYTES: usize = 96;
|
||||
|
||||
/// Sentinel for "no valid PTS base captured yet". Real PTS-in-ns values are
|
||||
/// non-negative (derived from the unsigned 90 kHz PES timestamp), so a negative
|
||||
@@ -156,25 +172,30 @@ impl CodecParser for DtsParser {
|
||||
};
|
||||
if start > 0 {
|
||||
self.drain_front(start);
|
||||
if find_sync(&self.buf, &DTS_CORE_SYNC) != Some(0) {
|
||||
// Shouldn't happen, but never loop forever.
|
||||
break;
|
||||
}
|
||||
// The sync `find_sync` located at offset `start` is now at
|
||||
// offset 0 by construction, so a re-scan would be a redundant
|
||||
// O(buf_len) walk per iteration; assert the invariant instead.
|
||||
debug_assert_eq!(
|
||||
find_sync(&self.buf, &DTS_CORE_SYNC),
|
||||
Some(0),
|
||||
"drain_front(start) must leave the core sync at offset 0"
|
||||
);
|
||||
}
|
||||
|
||||
// Need the core header to size the core frame.
|
||||
if self.buf.len() < 10 {
|
||||
if self.buf.len() < CORE_HEADER_MIN_BYTES {
|
||||
break;
|
||||
}
|
||||
let core_size = dts_core_frame_size(&self.buf);
|
||||
// `dts_core_frame_size` returns a 14-bit `fsize + 1`, so it is
|
||||
// always in [1, 16384]; the bare `== 0` / `> MAX_AU_BYTES` checks
|
||||
// can never fire. A real DTS core header is at least ~10-14 bytes,
|
||||
// so any decoded size below that came from a false/corrupt sync.
|
||||
// Reject it (drain the 4 syncword bytes and resync) instead of
|
||||
// letting a tiny bogus size close the current access unit at a junk
|
||||
// boundary and drop the trailing extension substreams. The
|
||||
// `> MAX_AU_BYTES` upper bound is kept as a harmless guard.
|
||||
// can never fire. A real DTS core frame is at least
|
||||
// MIN_CORE_FRAME_BYTES (96, the ETSI spec floor), so any decoded
|
||||
// size below that came from a false/corrupt sync. Reject it (drain
|
||||
// the 4 syncword bytes and resync) instead of letting a tiny bogus
|
||||
// size close the current access unit at a junk boundary and drop the
|
||||
// trailing extension substreams. The `> MAX_AU_BYTES` upper bound is
|
||||
// kept as a harmless guard.
|
||||
if !(MIN_CORE_FRAME_BYTES..=MAX_AU_BYTES).contains(&core_size) {
|
||||
// Bogus core sync — skip past it and resync.
|
||||
self.drain_front(4);
|
||||
@@ -257,7 +278,8 @@ impl CodecParser for DtsParser {
|
||||
// core + its extension substreams, which had no following core sync to
|
||||
// close it during streaming). Require a complete core frame; drop a
|
||||
// bare partial sync tail.
|
||||
if find_sync(&self.buf, &DTS_CORE_SYNC) != Some(0) || self.buf.len() < 10 {
|
||||
if find_sync(&self.buf, &DTS_CORE_SYNC) != Some(0) || self.buf.len() < CORE_HEADER_MIN_BYTES
|
||||
{
|
||||
self.buf.clear();
|
||||
return Vec::new();
|
||||
}
|
||||
@@ -315,7 +337,7 @@ fn next_core_boundary(buf: &[u8], core_size: usize) -> NextCore {
|
||||
while let Some(rel) = find_sync(&buf[from..], &DTS_CORE_SYNC) {
|
||||
let pos = from + rel;
|
||||
// Need the candidate's core header to judge it.
|
||||
if buf.len() - pos < 10 {
|
||||
if buf.len() - pos < CORE_HEADER_MIN_BYTES {
|
||||
return NextCore::NeedMore;
|
||||
}
|
||||
let sz = dts_core_frame_size(&buf[pos..]);
|
||||
@@ -328,10 +350,17 @@ fn next_core_boundary(buf: &[u8], core_size: usize) -> NextCore {
|
||||
NextCore::None
|
||||
}
|
||||
|
||||
/// DTS core frame size from header bits.
|
||||
/// fsize is at bits 46-59 (14 bits) of the header: bytes 5-7.
|
||||
/// DTS core frame size from header bits. `fsize` is the 14-bit field at bits
|
||||
/// 46-59 of the header (bytes 5-7). On the wire `fsize` is the frame length
|
||||
/// minus one, so this returns `fsize + 1`, i.e. the core frame length in bytes
|
||||
/// (range 1..=16384). Callers treat the result as the actual byte length and
|
||||
/// the MIN..=MAX range checks assume so.
|
||||
///
|
||||
/// Returns `0` when `data` is shorter than `CORE_HEADER_MIN_BYTES` — every call
|
||||
/// site rejects that via the minimum-frame lower bound, so a `0` is never
|
||||
/// mistaken for a valid tiny frame.
|
||||
fn dts_core_frame_size(data: &[u8]) -> usize {
|
||||
if data.len() < 10 {
|
||||
if data.len() < CORE_HEADER_MIN_BYTES {
|
||||
return 0;
|
||||
}
|
||||
// fsize field: 14 bits starting at bit 46
|
||||
@@ -629,6 +658,43 @@ mod tests {
|
||||
assert_eq!(tail[0].data.len(), 640);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub_spec_core_size_is_rejected_as_false_sync() {
|
||||
// A core sync whose decoded fsize+1 lands in [CORE_HEADER_MIN_BYTES,
|
||||
// MIN_CORE_FRAME_BYTES) — i.e. a "frame" smaller than the 96-byte ETSI
|
||||
// spec minimum — is a false sync inside extension payload and must NOT
|
||||
// close an access unit. Pick a decoded size of 64 (well inside the old
|
||||
// 10..96 false-positive window the raised floor now rejects).
|
||||
let false_size = 64usize;
|
||||
assert!(
|
||||
(CORE_HEADER_MIN_BYTES..MIN_CORE_FRAME_BYTES).contains(&false_size),
|
||||
"test fixture must sit in the widened reject window"
|
||||
);
|
||||
let mut parser = DtsParser::new();
|
||||
|
||||
// Frame 1: real core(512) + extension that embeds a sub-spec "core sync"
|
||||
// whose fsize decodes to 64 bytes.
|
||||
let mut ext = make_dts_ext(256);
|
||||
let bogus = make_dts_core(false_size); // valid-looking sync, size 64
|
||||
ext[64..64 + bogus.len()].copy_from_slice(&bogus);
|
||||
let mut frame1 = make_dts_core(512);
|
||||
frame1.extend_from_slice(&ext);
|
||||
|
||||
assert!(
|
||||
parser.parse(&make_pes(frame1, Some(90000))).is_empty(),
|
||||
"sub-spec core size must not close the AU"
|
||||
);
|
||||
|
||||
// Real next core closes frame 1 as core + full extension.
|
||||
let f = parser.parse(&make_pes(make_dts_core(640), Some(93000)));
|
||||
assert_eq!(f.len(), 1);
|
||||
assert_eq!(
|
||||
f[0].data.len(),
|
||||
512 + 256,
|
||||
"AU must not be split at the sub-spec false sync"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forced_emit_does_not_corrupt_next_au_pts() {
|
||||
// When the buffer exceeds MAX_AU_BYTES with no next core sync, the
|
||||
|
||||
+130
-22
@@ -2,10 +2,14 @@
|
||||
//!
|
||||
//! DVD subtitles are carried in PS private stream 1 with sub-stream IDs 0x20-0x3F.
|
||||
//! A single subpicture unit (SPU — one displayed bitmap) may span multiple PES
|
||||
//! packets: only the first PES carries a PTS, continuations carry PTS=0. The SPU
|
||||
//! begins with a 2-byte big-endian `SPU_size` giving the total byte length of the
|
||||
//! whole unit. We reassemble across PES boundaries into one Frame so large
|
||||
//! subtitles aren't split/garbled, inheriting the head PES's PTS.
|
||||
//! packets: only the first PES carries a PTS; continuation PES packets have no
|
||||
//! PTS field (the PS demuxer leaves `pts` as `None`). The SPU begins with a
|
||||
//! 2-byte big-endian `SPU_size` giving the total byte length of the whole unit.
|
||||
//! We reassemble across PES boundaries into one Frame so large subtitles aren't
|
||||
//! split/garbled, inheriting the head PES's PTS. The presence of a PTS — not
|
||||
//! merely an open `pending` — is the authoritative SPU-boundary signal, so a
|
||||
//! lost continuation or a corrupt SPU_size can't merge the next subtitle into
|
||||
//! the stuck unit.
|
||||
//!
|
||||
//! For MKV: codec ID "S_VOBSUB".
|
||||
//! All frames are keyframes (each is a complete bitmap).
|
||||
@@ -35,7 +39,7 @@ impl DvdSubParser {
|
||||
/// Emit `pending` as a Frame if it is complete (or `force` at EOF),
|
||||
/// returning it and clearing the buffer. Returns None if nothing to emit.
|
||||
fn take_if_complete(&mut self, force: bool) -> Option<Frame> {
|
||||
let (pts_ns, size, buf) = self.pending.as_ref()?;
|
||||
let (_, size, buf) = self.pending.as_ref()?;
|
||||
if force || buf.len() >= *size {
|
||||
let (pts_ns, _, data) = self.pending.take().unwrap();
|
||||
return Some(Frame {
|
||||
@@ -45,7 +49,6 @@ impl DvdSubParser {
|
||||
duration_ns: None,
|
||||
});
|
||||
}
|
||||
let _ = pts_ns;
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -58,32 +61,63 @@ impl CodecParser for DvdSubParser {
|
||||
|
||||
let mut out = Vec::new();
|
||||
|
||||
if self.pending.is_some() {
|
||||
// Continuation of an in-progress SPU (PTS=0 on these). Append,
|
||||
// bounded by MAX_SPU_BYTES.
|
||||
if let Some((_, _, buf)) = self.pending.as_mut() {
|
||||
let room = MAX_SPU_BYTES.saturating_sub(buf.len());
|
||||
let take = room.min(pes.data.len());
|
||||
buf.extend_from_slice(&pes.data[..take]);
|
||||
// A PES carrying a real PTS is the START of a new SPU; continuations of
|
||||
// an in-progress SPU carry no PTS (the PS demuxer leaves `pts` None when
|
||||
// the PES has no PTS field — see the module doc). PTS is therefore the
|
||||
// authoritative SPU-boundary signal, NOT merely `pending.is_some()`.
|
||||
//
|
||||
// Append-as-continuation ONLY when this PES has no PTS. When it has a
|
||||
// PTS but a stale `pending` is still open (a lost continuation, or a
|
||||
// corrupt/oversized declared SPU_size that real data never reaches),
|
||||
// force-emit the stuck unit truncated and fall through to start a fresh
|
||||
// SPU from this PES. Without this, one bad SPU_size would swallow every
|
||||
// later subtitle until EOF — exactly the damaged-disc case we target.
|
||||
if pes.pts.is_none() {
|
||||
if self.pending.is_some() {
|
||||
// Continuation: append, bounded by MAX_SPU_BYTES.
|
||||
if let Some((_, _, buf)) = self.pending.as_mut() {
|
||||
let room = MAX_SPU_BYTES.saturating_sub(buf.len());
|
||||
let take = room.min(pes.data.len());
|
||||
buf.extend_from_slice(&pes.data[..take]);
|
||||
}
|
||||
if let Some(frame) = self.take_if_complete(false) {
|
||||
out.push(frame);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
if let Some(frame) = self.take_if_complete(false) {
|
||||
out.push(frame);
|
||||
}
|
||||
return out;
|
||||
// No pending and no PTS: nothing to attach this to. Pass it through
|
||||
// as a lone frame (PTS unknown → 0) rather than drop it.
|
||||
} else if let Some(frame) = self.take_if_complete(true) {
|
||||
// New SPU starting while a previous one is still open → flush stale.
|
||||
out.push(frame);
|
||||
}
|
||||
|
||||
// Start of a new SPU. The first 2 bytes are the big-endian total size.
|
||||
let pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0);
|
||||
let declared = if pes.data.len() >= 2 {
|
||||
((pes.data[0] as usize) << 8) | pes.data[1] as usize
|
||||
// SPU_size includes the 2-byte header, so a declared size < 2 is
|
||||
// always malformed; treat it like the too-short path (lone frame)
|
||||
// rather than emit an immediate oversized unit.
|
||||
let d = ((pes.data[0] as usize) << 8) | pes.data[1] as usize;
|
||||
if d < 2 {
|
||||
out.push(Frame {
|
||||
pts_ns,
|
||||
keyframe: true,
|
||||
data: pes.data.clone(),
|
||||
duration_ns: None,
|
||||
});
|
||||
return out;
|
||||
}
|
||||
d
|
||||
} else {
|
||||
// Too short to carry SPU_size — pass through as a lone frame.
|
||||
return vec![Frame {
|
||||
out.push(Frame {
|
||||
pts_ns,
|
||||
keyframe: true,
|
||||
data: pes.data.clone(),
|
||||
duration_ns: None,
|
||||
}];
|
||||
});
|
||||
return out;
|
||||
};
|
||||
|
||||
let mut buf = pes.data.clone();
|
||||
@@ -114,6 +148,18 @@ impl CodecParser for DvdSubParser {
|
||||
///
|
||||
/// Input: `[padding, Y, Cb, Cr]` (as stored in DVD IFO PGC data).
|
||||
/// Returns `[R, G, B]`.
|
||||
///
|
||||
/// Range convention (deliberate): this uses the **full-range (JFIF) BT.601**
|
||||
/// coefficients with no 16/235 luma scaling. DVD IFO palette YCbCr is nominally
|
||||
/// studio-swing BT.601, so studio-swing math would be more colorimetrically
|
||||
/// "correct" in isolation. But the output here is a VobSub `.idx` `palette:`
|
||||
/// line, and the entire VobSub ecosystem (the original tooling, mkvtoolnix,
|
||||
/// players that read the .idx palette) is built around this full-range formula —
|
||||
/// it is the de-facto on-disk convention. Emitting studio-swing-scaled RGB here
|
||||
/// would make freemkv's palettes inconsistent with every other tool and wrong in
|
||||
/// players that assume the VobSub convention. We therefore intentionally keep
|
||||
/// full-range; do NOT "fix" this to studio-swing without changing the consuming
|
||||
/// side in lockstep.
|
||||
pub fn ycbcr_to_rgb(color: &[u8; 4]) -> [u8; 3] {
|
||||
let y = color[1] as f64;
|
||||
let cb = color[2] as f64;
|
||||
@@ -240,9 +286,10 @@ mod tests {
|
||||
|
||||
let f = parser.parse(&make_pes(head.clone(), Some(90000)));
|
||||
assert!(f.is_empty(), "incomplete SPU should not emit yet");
|
||||
let f = parser.parse(&make_pes(cont1.clone(), Some(0)));
|
||||
// Continuations carry NO PTS (None), per the PS demuxer.
|
||||
let f = parser.parse(&make_pes(cont1.clone(), None));
|
||||
assert!(f.is_empty(), "still incomplete");
|
||||
let frames = parser.parse(&make_pes(cont2.clone(), Some(0)));
|
||||
let frames = parser.parse(&make_pes(cont2.clone(), None));
|
||||
assert_eq!(frames.len(), 1, "completed SPU emits exactly one frame");
|
||||
|
||||
// Reassembled bytes = head + cont1 + cont2, in order.
|
||||
@@ -268,6 +315,67 @@ mod tests {
|
||||
assert_eq!(frames[0].pts_ns, 1_000_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_pts_pes_force_emits_stale_pending_and_starts_new_spu() {
|
||||
// A lost continuation leaves an incomplete pending SPU. The NEXT real
|
||||
// subtitle arrives with its own PTS — it must force-emit the stuck unit
|
||||
// (truncated) and begin a fresh SPU, not be appended as a continuation.
|
||||
let mut parser = DvdSubParser::new(None);
|
||||
|
||||
// SPU 1 declares 100 bytes but only 6 arrive; the continuation is lost.
|
||||
let head1 = vec![0x00, 0x64, 0xDE, 0xAD, 0xBE, 0xEF];
|
||||
assert!(
|
||||
parser
|
||||
.parse(&make_pes(head1.clone(), Some(90000)))
|
||||
.is_empty(),
|
||||
"SPU 1 incomplete, held pending"
|
||||
);
|
||||
|
||||
// SPU 2 arrives with a real PTS — declares 4 bytes, fully present.
|
||||
let head2 = vec![0x00, 0x04, 0x11, 0x22];
|
||||
let frames = parser.parse(&make_pes(head2.clone(), Some(180000)));
|
||||
// First the truncated stale SPU 1, then complete SPU 2.
|
||||
assert_eq!(frames.len(), 2, "stale flushed + new emitted");
|
||||
assert_eq!(frames[0].data, head1, "stale SPU 1 emitted truncated");
|
||||
assert_eq!(frames[0].pts_ns, 1_000_000_000, "SPU 1 keeps its PTS");
|
||||
assert_eq!(frames[1].data, head2, "SPU 2 emitted fresh");
|
||||
assert_eq!(frames[1].pts_ns, 2_000_000_000, "SPU 2 keeps its own PTS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrupt_oversized_size_recovers_on_next_real_pts() {
|
||||
// A corrupt SPU_size that real data never reaches must not swallow every
|
||||
// later subtitle. The next real-PTS PES resets pending and recovers the
|
||||
// track.
|
||||
let mut parser = DvdSubParser::new(None);
|
||||
|
||||
// Declares 0xFFFF but only a few bytes ever arrive (corrupt size).
|
||||
let bad = vec![0xFF, 0xFF, 0x01, 0x02, 0x03];
|
||||
assert!(parser.parse(&make_pes(bad.clone(), Some(90000))).is_empty());
|
||||
// A no-PTS stray continuation appends (still stuck under the bad size).
|
||||
assert!(parser.parse(&make_pes(vec![0x04, 0x05], None)).is_empty());
|
||||
|
||||
// Next real subtitle (PTS present) recovers: stale flushed + new SPU.
|
||||
let good = vec![0x00, 0x04, 0xAA, 0xBB];
|
||||
let frames = parser.parse(&make_pes(good.clone(), Some(270000)));
|
||||
assert_eq!(frames.len(), 2, "track recovers, not swallowed to EOF");
|
||||
assert_eq!(frames[1].data, good);
|
||||
assert_eq!(frames[1].pts_ns, 3_000_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declared_size_below_two_passes_through_as_lone_frame() {
|
||||
// SPU_size includes its own 2-byte header, so a declared size < 2 is
|
||||
// malformed. It must pass through as a lone frame, not emit an oversized
|
||||
// unit or get stuck pending.
|
||||
let mut parser = DvdSubParser::new(None);
|
||||
let data = vec![0x00, 0x00, 0xAB, 0xCD]; // declared = 0
|
||||
let frames = parser.parse(&make_pes(data.clone(), Some(90000)));
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].data, data, "passed through whole");
|
||||
assert!(parser.pending.is_none(), "no pending left open");
|
||||
}
|
||||
|
||||
// ── YCbCr → RGB conversion tests ──────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
+79
-83
@@ -4,6 +4,7 @@
|
||||
//! Detects keyframes (IDR slices).
|
||||
//! Each PES packet = one access unit = one frame.
|
||||
|
||||
use super::startcode::{find_start_code, skip_start_code};
|
||||
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
|
||||
|
||||
/// H.264 NAL unit types we care about.
|
||||
@@ -12,6 +13,9 @@ const NAL_SPS: u8 = 7;
|
||||
const NAL_PPS: u8 = 8;
|
||||
const NAL_AUD: u8 = 9;
|
||||
|
||||
/// H.264 (AVC) Annex B → MKV codec parser: extracts SPS/PPS for the avcC
|
||||
/// codecPrivate, detects IDR keyframes, and converts each PES access unit into
|
||||
/// length-prefixed NAL units. Implements [`CodecParser`].
|
||||
pub struct H264Parser {
|
||||
// First-seen SPS/PPS seed the MKV codecPrivate (avcC) — the only out-of-band
|
||||
// copy the player gets. BD H.264 repeats the parameter sets at every IDR;
|
||||
@@ -32,6 +36,7 @@ impl Default for H264Parser {
|
||||
}
|
||||
|
||||
impl H264Parser {
|
||||
/// Create a fresh H.264 parser with no parameter sets captured yet.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
sps: None,
|
||||
@@ -56,7 +61,14 @@ fn handle_param_set(first: &mut Option<Vec<u8>>, nal: &[u8], frame_data: &mut Ve
|
||||
Some(f) if f.as_slice() == nal => {} // == codecPrivate → player has it
|
||||
Some(_) => {
|
||||
// Differs from codecPrivate → emit in-band so it wins at this AU.
|
||||
frame_data.extend_from_slice(&(nal.len() as u32).to_be_bytes());
|
||||
// A NAL longer than u32::MAX cannot be length-prefixed in the
|
||||
// 4-byte field; skip it rather than emit a truncated length over
|
||||
// the full body (mis-framed NALU). Unreachable in practice — no
|
||||
// real access unit is >4 GiB.
|
||||
let Ok(len) = u32::try_from(nal.len()) else {
|
||||
return;
|
||||
};
|
||||
frame_data.extend_from_slice(&len.to_be_bytes());
|
||||
frame_data.extend_from_slice(nal);
|
||||
}
|
||||
}
|
||||
@@ -78,7 +90,10 @@ impl CodecParser for H264Parser {
|
||||
// Annex B (start-code prefixed) NALUs to length-prefixed NALUs (MKV with
|
||||
// AVCDecoderConfigurationRecord expects a 4-byte length prefix per NAL).
|
||||
let mut keyframe = false;
|
||||
let mut frame_data = Vec::new();
|
||||
// Pre-size: output is ~input bytes plus a few 4-byte NAL length prefixes.
|
||||
// The unsized Vec growth chain otherwise reallocs several times per
|
||||
// frame in the mux hot path (mirrors the HEVC parser).
|
||||
let mut frame_data = Vec::with_capacity(pes.data.len() + 64);
|
||||
|
||||
for nal in NalIterator::new(&pes.data) {
|
||||
let nal_type = nal[0] & 0x1F;
|
||||
@@ -88,13 +103,21 @@ impl CodecParser for H264Parser {
|
||||
// mid-title redefinition differs from the avcC copy.
|
||||
NAL_SPS => handle_param_set(&mut self.sps, nal, &mut frame_data),
|
||||
NAL_PPS => handle_param_set(&mut self.pps, nal, &mut frame_data),
|
||||
// Access unit delimiters: drop.
|
||||
// Access unit delimiters: drop. Intentional and spec-correct —
|
||||
// Matroska H.264 frame data omits AUDs (the container delimits
|
||||
// access units), so keeping them in-band is redundant. Mirrors
|
||||
// the HEVC parser.
|
||||
NAL_AUD => {}
|
||||
_ => {
|
||||
if nal_type == NAL_SLICE_IDR {
|
||||
keyframe = true;
|
||||
}
|
||||
let len = nal.len() as u32;
|
||||
// A NAL longer than u32::MAX can't be length-prefixed in the
|
||||
// 4-byte field; skip it rather than mis-frame the output.
|
||||
// Unreachable in practice (no real AU is >4 GiB).
|
||||
let Ok(len) = u32::try_from(nal.len()) else {
|
||||
continue;
|
||||
};
|
||||
frame_data.extend_from_slice(&len.to_be_bytes());
|
||||
frame_data.extend_from_slice(nal);
|
||||
}
|
||||
@@ -182,61 +205,42 @@ impl<'a> Iterator for NalIterator<'a> {
|
||||
type Item = &'a [u8];
|
||||
|
||||
fn next(&mut self) -> Option<&'a [u8]> {
|
||||
if self.pos >= self.data.len() {
|
||||
return None;
|
||||
}
|
||||
// Loop (not tail-recursion) over empty NALs: a crafted/garbled Annex B
|
||||
// stream with many adjacent start codes (e.g. 00 00 01 00 00 01 ...)
|
||||
// yields empty NALs back-to-back; recursing once per empty NAL would
|
||||
// overflow the stack. `self.pos` advances to `nal_end` each iteration,
|
||||
// so the loop always terminates. Mirrors the HEVC parser's while-scan.
|
||||
loop {
|
||||
if self.pos >= self.data.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Skip the start code at current position
|
||||
let nal_start = skip_start_code(self.data, self.pos)?;
|
||||
// Skip the start code at current position
|
||||
let nal_start = skip_start_code(self.data, self.pos)?;
|
||||
|
||||
// Find next start code (or end of data)
|
||||
let nal_end = find_start_code(self.data, nal_start).unwrap_or(self.data.len());
|
||||
// Find next start code (or end of data)
|
||||
let nal_end = find_start_code(self.data, nal_start).unwrap_or(self.data.len());
|
||||
|
||||
// Remove trailing zeros (part of next start code's zero prefix)
|
||||
let mut end = nal_end;
|
||||
while end > nal_start && self.data[end - 1] == 0x00 {
|
||||
end -= 1;
|
||||
}
|
||||
// Strip the leading zeros of the following start code. For a
|
||||
// conforming bitstream this is lossless: rbsp_trailing_bits() sets a
|
||||
// stop-one bit, so the final byte of any RBSP is never 0x00 — the only
|
||||
// trailing zeros here belong to the next 00 00 (00) 01 prefix, never to
|
||||
// the NAL's RBSP payload. (Mirrors the HEVC parser.)
|
||||
let mut end = nal_end;
|
||||
while end > nal_start && self.data[end - 1] == 0x00 {
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
self.pos = nal_end;
|
||||
self.pos = nal_end;
|
||||
|
||||
if end > nal_start {
|
||||
Some(&self.data[nal_start..end])
|
||||
} else {
|
||||
self.next()
|
||||
if end > nal_start {
|
||||
return Some(&self.data[nal_start..end]);
|
||||
}
|
||||
// Empty NAL — continue scanning instead of recursing.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the position of the next start code (00 00 01) at or after `from`.
|
||||
///
|
||||
/// Backed by `memchr::memmem::find` for SIMD-accelerated bytestring
|
||||
/// search. On AVX2-capable x86_64 this runs ~5–10× the byte-by-byte
|
||||
/// scan that preceded it; on a 200 KB UHD HEVC frame the saving is
|
||||
/// in the hundreds of microseconds per call.
|
||||
pub fn find_start_code(data: &[u8], from: usize) -> Option<usize> {
|
||||
if data.len() < from + 3 {
|
||||
return None;
|
||||
}
|
||||
memchr::memmem::find(&data[from..], b"\x00\x00\x01").map(|rel| from + rel)
|
||||
}
|
||||
|
||||
/// Skip past the start code at position `pos`, returning the first byte after it.
|
||||
pub fn skip_start_code(data: &[u8], pos: usize) -> Option<usize> {
|
||||
if pos + 2 >= data.len() {
|
||||
return None;
|
||||
}
|
||||
if data[pos] == 0x00 && data[pos + 1] == 0x00 {
|
||||
if pos + 3 < data.len() && data[pos + 2] == 0x00 && data[pos + 3] == 0x01 {
|
||||
return Some(pos + 4); // 4-byte start code
|
||||
}
|
||||
if data[pos + 2] == 0x01 {
|
||||
return Some(pos + 3); // 3-byte start code
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -251,39 +255,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// --- find_start_code tests ---
|
||||
|
||||
#[test]
|
||||
fn find_start_code_3byte() {
|
||||
let data = [0x00, 0x00, 0x01, 0x65];
|
||||
assert_eq!(find_start_code(&data, 0), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_start_code_4byte() {
|
||||
let data = [0x00, 0x00, 0x00, 0x01, 0x65];
|
||||
// find_start_code looks for 00 00 01 pattern, which starts at offset 1 in a 4-byte start code
|
||||
assert_eq!(find_start_code(&data, 0), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_start_code_offset() {
|
||||
let data = [0xFF, 0xFF, 0x00, 0x00, 0x01, 0x09];
|
||||
assert_eq!(find_start_code(&data, 0), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_start_code_none() {
|
||||
let data = [0x00, 0x00, 0x00, 0x00];
|
||||
assert_eq!(find_start_code(&data, 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_start_code_too_short() {
|
||||
let data = [0x00, 0x00];
|
||||
assert_eq!(find_start_code(&data, 0), None);
|
||||
}
|
||||
|
||||
// --- parse SPS+PPS → codec_private ---
|
||||
|
||||
#[test]
|
||||
@@ -590,6 +561,31 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_empty_nals_do_not_overflow_stack() {
|
||||
// Regression: NalIterator::next must iterate, not recurse, over empty
|
||||
// NALs. A crafted Annex B stream of tens of thousands of adjacent start
|
||||
// codes (each producing an empty NAL) would blow the stack under the old
|
||||
// tail-recursive implementation. Iterating handles it in bounded stack.
|
||||
let mut data = Vec::new();
|
||||
// 50_000 back-to-back 3-byte start codes → 50_000 empty NALs.
|
||||
for _ in 0..50_000 {
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
}
|
||||
// One real NAL at the end so the iterator yields something.
|
||||
data.extend_from_slice(&[0x41, 0xAA, 0xBB]);
|
||||
|
||||
let mut parser = H264Parser::new();
|
||||
let frames = parser.parse(&make_pes(data, Some(0)));
|
||||
// Exactly one populated frame; the empty NALs are skipped without
|
||||
// overflowing.
|
||||
assert_eq!(frames.len(), 1);
|
||||
let fd = &frames[0].data;
|
||||
let len = u32::from_be_bytes([fd[0], fd[1], fd[2], fd[3]]) as usize;
|
||||
assert_eq!(len, 3, "the single real NAL is length-prefixed");
|
||||
assert_eq!(fd[4], 0x41);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn avcc_oversized_param_set_returns_none() {
|
||||
// A param set > 65535 bytes can't be length-encoded in avcC's 16-bit
|
||||
|
||||
+103
-24
@@ -4,7 +4,7 @@
|
||||
//! Detects keyframes (IRAP pictures: IDR, CRA, BLA).
|
||||
//! Each PES packet = one access unit = one frame.
|
||||
|
||||
use super::h264::{find_start_code, skip_start_code};
|
||||
use super::startcode::{find_start_code, skip_start_code};
|
||||
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
|
||||
|
||||
// HEVC NAL unit types
|
||||
@@ -20,6 +20,9 @@ const _NAL_UNSPEC62_DV_RPU: u8 = 62;
|
||||
const NAL_BLA_W_LP: u8 = 16;
|
||||
const NAL_RSV_IRAP_VCL23: u8 = 23;
|
||||
|
||||
/// HEVC (H.265) Annex B → MKV codec parser: extracts VPS/SPS/PPS for the hvcC
|
||||
/// codecPrivate, detects IRAP keyframes, and converts each PES access unit into
|
||||
/// length-prefixed NAL units. Implements [`CodecParser`].
|
||||
pub struct HevcParser {
|
||||
// First-seen parameter set of each type → seeds the MKV codecPrivate (hvcC).
|
||||
// This is the ONLY copy the player gets out-of-band, and a player re-applies
|
||||
@@ -42,6 +45,7 @@ impl Default for HevcParser {
|
||||
}
|
||||
|
||||
impl HevcParser {
|
||||
/// Create a fresh HEVC parser with no parameter sets captured yet.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
vps: None,
|
||||
@@ -71,12 +75,30 @@ fn handle_param_set(first: &mut Option<Vec<u8>>, nal: &[u8], frame_data: &mut Ve
|
||||
Some(f) if f.as_slice() == nal => {} // == codecPrivate → player has it
|
||||
Some(_) => {
|
||||
// Differs from codecPrivate → emit in-band so it wins at this AU.
|
||||
frame_data.extend_from_slice(&(nal.len() as u32).to_be_bytes());
|
||||
// A NAL longer than u32::MAX can't be length-prefixed in the 4-byte
|
||||
// field; skip it rather than mis-frame the output. Unreachable in
|
||||
// practice (no real access unit is >4 GiB).
|
||||
let Ok(len) = u32::try_from(nal.len()) else {
|
||||
return;
|
||||
};
|
||||
frame_data.extend_from_slice(&len.to_be_bytes());
|
||||
frame_data.extend_from_slice(nal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Append `nal` to `out` as a 4-byte big-endian length prefix followed by the
|
||||
/// NAL body. A NAL longer than `u32::MAX` can't be length-prefixed in the
|
||||
/// 4-byte field, so it is skipped rather than mis-framed. Unreachable in
|
||||
/// practice (no real access unit is >4 GiB).
|
||||
fn push_length_prefixed(out: &mut Vec<u8>, nal: &[u8]) {
|
||||
let Ok(len) = u32::try_from(nal.len()) else {
|
||||
return;
|
||||
};
|
||||
out.extend_from_slice(&len.to_be_bytes());
|
||||
out.extend_from_slice(nal);
|
||||
}
|
||||
|
||||
impl CodecParser for HevcParser {
|
||||
fn parse(&mut self, pes: &PesPacket) -> Vec<Frame> {
|
||||
if pes.data.is_empty() {
|
||||
@@ -112,7 +134,12 @@ impl CodecParser for HevcParser {
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
if nal_start < data.len() {
|
||||
// Skip empty NALs entirely. When the trailing-zero strip reduces
|
||||
// `end` back to `nal_start` (e.g. `00 00 01 00 00 01`, or a
|
||||
// zero-filled bad sector between two start codes), the slice is
|
||||
// empty; emitting a 4-byte 0x00000000 length prefix with no NAL
|
||||
// body produces a structurally invalid NALU a decoder rejects.
|
||||
if nal_start < data.len() && end > nal_start {
|
||||
// HEVC NAL header: 2 bytes. Type is bits 1-6 of first byte.
|
||||
let nal_type = (data[nal_start] >> 1) & 0x3F;
|
||||
|
||||
@@ -126,18 +153,18 @@ impl CodecParser for HevcParser {
|
||||
NAL_PPS => {
|
||||
handle_param_set(&mut self.pps, &data[nal_start..end], &mut frame_data)
|
||||
}
|
||||
NAL_AUD => {} // Skip access unit delimiters
|
||||
// Drop Access Unit Delimiters. This is intentional and
|
||||
// spec-correct: Matroska HEVC frame data omits AUDs
|
||||
// (the container delimits access units), so carrying
|
||||
// them in-band is redundant. H.264 does the same below.
|
||||
NAL_AUD => {}
|
||||
t if (NAL_BLA_W_LP..=NAL_RSV_IRAP_VCL23).contains(&t) => {
|
||||
keyframe = true;
|
||||
let nal = &data[nal_start..end];
|
||||
frame_data.extend_from_slice(&(nal.len() as u32).to_be_bytes());
|
||||
frame_data.extend_from_slice(nal);
|
||||
push_length_prefixed(&mut frame_data, &data[nal_start..end]);
|
||||
}
|
||||
_ => {
|
||||
// All other NAL types (slices, SEI, DV RPU, etc.) pass through
|
||||
let nal = &data[nal_start..end];
|
||||
frame_data.extend_from_slice(&(nal.len() as u32).to_be_bytes());
|
||||
frame_data.extend_from_slice(nal);
|
||||
push_length_prefixed(&mut frame_data, &data[nal_start..end]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,8 +200,9 @@ impl CodecParser for HevcParser {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Simplified: store as arrays in Annex B format
|
||||
// Full HEVCDecoderConfigurationRecord is complex — for now, concatenate
|
||||
// Build a conforming HEVCDecoderConfigurationRecord: fixed header
|
||||
// (configurationVersion, profile_tier_level fields, parallelism, parsed
|
||||
// chroma/bit depths) followed by numOfArrays length-prefixed NAL arrays.
|
||||
let mut record = Vec::new();
|
||||
|
||||
// Minimal HEVCDecoderConfigurationRecord header.
|
||||
@@ -198,17 +226,17 @@ impl CodecParser for HevcParser {
|
||||
if sps.len() > 7 {
|
||||
record.extend_from_slice(&sps[4..8]);
|
||||
} else {
|
||||
let avail = sps.len().saturating_sub(4).min(4);
|
||||
let target = record.len() + 4;
|
||||
record.extend_from_slice(&sps[sps.len().min(4)..sps.len().min(8)]);
|
||||
record.extend_from_slice(&vec![0u8; 4 - avail]);
|
||||
record.resize(target, 0u8); // zero-pad the missing bytes in place
|
||||
}
|
||||
// general_constraint_indicator_flags (6 bytes) — SPS bytes 8..14
|
||||
if sps.len() > 13 {
|
||||
record.extend_from_slice(&sps[8..14]);
|
||||
} else {
|
||||
let avail = sps.len().saturating_sub(8).min(6);
|
||||
let target = record.len() + 6;
|
||||
record.extend_from_slice(&sps[sps.len().min(8)..sps.len().min(14)]);
|
||||
record.extend_from_slice(&vec![0u8; 6 - avail]);
|
||||
record.resize(target, 0u8); // zero-pad the missing bytes in place
|
||||
}
|
||||
// general_level_idc — SPS byte 14
|
||||
record.push(if sps.len() > 14 { sps[14] } else { 0 });
|
||||
@@ -224,6 +252,8 @@ impl CodecParser for HevcParser {
|
||||
chroma_format_idc: 1,
|
||||
bit_depth_luma_minus8: 0,
|
||||
bit_depth_chroma_minus8: 0,
|
||||
max_sub_layers_minus1: 0,
|
||||
temporal_id_nesting_flag: 0,
|
||||
});
|
||||
// chromaFormat (6 reserved bits set + 2-bit chroma_format_idc)
|
||||
record.push(0xFC | (chroma.chroma_format_idc & 0x03));
|
||||
@@ -233,8 +263,14 @@ impl CodecParser for HevcParser {
|
||||
record.push(0xF8 | (chroma.bit_depth_chroma_minus8 & 0x07));
|
||||
// avgFrameRate
|
||||
record.extend_from_slice(&[0, 0]);
|
||||
// constantFrameRate + numTemporalLayers + temporalIdNested + lengthSizeMinusOne
|
||||
record.push(0x03); // lengthSizeMinusOne = 3 (4 bytes)
|
||||
// Byte 21 packs four fields (ISO/IEC 14496-15):
|
||||
// constantFrameRate u(2) = 0 (unknown / not constant)
|
||||
// numTemporalLayers u(3) = sps_max_sub_layers_minus1 + 1
|
||||
// temporalIdNested u(1) = sps_temporal_id_nesting_flag
|
||||
// lengthSizeMinusOne u(2) = 3 (4-byte length prefix)
|
||||
let num_temporal_layers = (chroma.max_sub_layers_minus1 + 1) & 0x07;
|
||||
let temporal_id_nested = chroma.temporal_id_nesting_flag & 0x01;
|
||||
record.push((num_temporal_layers << 3) | (temporal_id_nested << 2) | 0x03);
|
||||
// numOfArrays
|
||||
record.push(3); // VPS, SPS, PPS
|
||||
|
||||
@@ -271,6 +307,10 @@ struct SpsChroma {
|
||||
chroma_format_idc: u8,
|
||||
bit_depth_luma_minus8: u8,
|
||||
bit_depth_chroma_minus8: u8,
|
||||
/// sps_max_sub_layers_minus1 (u3): numTemporalLayers = this + 1 for hvcC.
|
||||
max_sub_layers_minus1: u8,
|
||||
/// sps_temporal_id_nesting_flag (u1) for hvcC temporalIdNested.
|
||||
temporal_id_nesting_flag: u8,
|
||||
}
|
||||
|
||||
/// Minimal MSB-first bit reader over a byte slice.
|
||||
@@ -365,7 +405,7 @@ fn parse_sps_chroma(sps: &[u8]) -> Option<SpsChroma> {
|
||||
// sps_max_sub_layers_minus1 u(3)
|
||||
let max_sub_layers_minus1 = r.read_bits(3)?;
|
||||
// sps_temporal_id_nesting_flag u(1)
|
||||
r.skip_bits(1)?;
|
||||
let temporal_id_nesting_flag = r.read_bit()?;
|
||||
|
||||
// profile_tier_level( 1, sps_max_sub_layers_minus1 )
|
||||
parse_profile_tier_level(&mut r, max_sub_layers_minus1)?;
|
||||
@@ -396,17 +436,18 @@ fn parse_sps_chroma(sps: &[u8]) -> Option<SpsChroma> {
|
||||
chroma_format_idc,
|
||||
bit_depth_luma_minus8,
|
||||
bit_depth_chroma_minus8,
|
||||
max_sub_layers_minus1: max_sub_layers_minus1 as u8,
|
||||
temporal_id_nesting_flag: temporal_id_nesting_flag as u8,
|
||||
})
|
||||
}
|
||||
|
||||
/// Consume a profile_tier_level(profilePresentFlag=1, maxNumSubLayersMinus1)
|
||||
/// structure from the bit reader (HEVC 7.3.3).
|
||||
fn parse_profile_tier_level(r: &mut BitReader, max_sub_layers_minus1: u32) -> Option<()> {
|
||||
// general: profile_space u(2) + tier u(1) + profile_idc u(5) = 8 bits,
|
||||
// profile_compatibility_flags u(32), 4× constraint/flags + reserved = 44
|
||||
// bits, general_inbld/reserved = 1 bit (total constraint area 48 bits),
|
||||
// general_level_idc u(8). 88 bits = 11 bytes... but the spec packs the
|
||||
// general PTL as 8 + 32 + 48 + 8 = 96 bits = 12 bytes. Skip 96 bits.
|
||||
// general PTL fixed layout (HEVC 7.3.3): profile_space u(2) + tier u(1) +
|
||||
// profile_idc u(5) = 8, general_profile_compatibility_flags u(32),
|
||||
// constraint-flags/reserved area = 48, general_level_idc u(8).
|
||||
// Total = 8 + 32 + 48 + 8 = 96 bits = 12 bytes. Skip 96 bits.
|
||||
r.skip_bits(96)?;
|
||||
|
||||
if max_sub_layers_minus1 > 0 {
|
||||
@@ -859,6 +900,30 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// --- empty NAL between adjacent start codes is skipped ---
|
||||
|
||||
#[test]
|
||||
fn empty_nal_between_start_codes_emits_no_bare_prefix() {
|
||||
// `00 00 01 00 00 01 <real NAL>`: the first start code is immediately
|
||||
// followed by another, so the in-between NAL is empty after the
|
||||
// trailing-zero strip. It must be skipped, NOT written as a bare
|
||||
// 0x00000000 length prefix (which a decoder treats as malformed).
|
||||
let mut parser = HevcParser::new();
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]); // start code, empty NAL
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]); // next start code
|
||||
data.extend_from_slice(&hevc_nal_header(1)); // TRAIL_R
|
||||
data.extend_from_slice(&[0x10, 0x20]);
|
||||
|
||||
let frames = parser.parse(&make_pes(data, Some(0)));
|
||||
assert_eq!(frames.len(), 1);
|
||||
let fd = &frames[0].data;
|
||||
// Exactly one length-prefixed NAL — no zero-length entry.
|
||||
let len = u32::from_be_bytes([fd[0], fd[1], fd[2], fd[3]]) as usize;
|
||||
assert!(len > 0, "no bare zero-length prefix emitted");
|
||||
assert_eq!(len + 4, fd.len(), "exactly one NAL in frame data");
|
||||
}
|
||||
|
||||
// --- empty PES ---
|
||||
|
||||
#[test]
|
||||
@@ -1129,6 +1194,20 @@ mod tests {
|
||||
assert_eq!(cp[18], 0xF8 | 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hvcc_byte21_from_sps_temporal_layers() {
|
||||
// make_sps_with_chroma sets sps_max_sub_layers_minus1 = 0 and
|
||||
// sps_temporal_id_nesting_flag = 1, so byte 21 must encode
|
||||
// numTemporalLayers = 1, temporalIdNested = 1, lengthSizeMinusOne = 3:
|
||||
// (1 << 3) | (1 << 2) | 3 = 0x0F.
|
||||
let sps = make_sps_with_chroma(1, 2, 2);
|
||||
let cp = codec_private_from_sps(&sps);
|
||||
assert_eq!(
|
||||
cp[21], 0x0F,
|
||||
"byte 21: numTemporalLayers=1, temporalIdNested=1, lengthSizeMinusOne=3"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hvcc_handles_emulation_prevention_in_sps() {
|
||||
// Insert an emulation-prevention byte (00 00 03) into the SPS RBSP and
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
//! DVD = leave intact. The raw PCM data is otherwise one complete audio frame
|
||||
//! per PES; no framing is needed.
|
||||
//!
|
||||
//! For MKV: codec ID "A_PCM/INT/BIG" (BD) or "A_PCM/INT/LIT" (DVD).
|
||||
//! For MKV: both BD and DVD LPCM map to codec ID "A_PCM/INT/BIG" (big-endian).
|
||||
//! DVD-Video LPCM is big-endian per the DVD-Video spec, and `mkv.rs` emits
|
||||
//! "A_PCM/INT/BIG" unconditionally for `Codec::Lpcm` — there is no DVD/BD branch
|
||||
//! and no "A_PCM/INT/LIT" path, so no byte-swap or alternate codec ID applies.
|
||||
//! All frames are keyframes (uncompressed audio).
|
||||
|
||||
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
|
||||
@@ -131,8 +134,9 @@ mod tests {
|
||||
fn dvd_lpcm_preserves_all_pcm_bytes() {
|
||||
// DVD-PS LPCM: PsDemuxer already removed the 7-byte private sub-header,
|
||||
// so the payload handed to this parser is raw PCM. The DVD parser must
|
||||
// NOT strip any further bytes (the round-2 audit Finding 3 bug: the BD
|
||||
// 4-byte strip dropped one sample pair per PES, drifting the audio).
|
||||
// NOT strip any further bytes — applying the BD 4-byte strip to a DVD
|
||||
// payload would drop one sample pair per PES and progressively drift
|
||||
// the audio.
|
||||
let mut parser = LpcmParser::new_dvd();
|
||||
let pcm = vec![0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0x01, 0x02];
|
||||
let frames = parser.parse(&make_pes(pcm.clone(), Some(90000)));
|
||||
|
||||
+85
-2
@@ -7,15 +7,27 @@
|
||||
//! - Determine keyframe status
|
||||
//! - Convert PTS from 90kHz to nanoseconds
|
||||
|
||||
/// AC-3 / E-AC-3 (Dolby Digital / Digital Plus) elementary-stream parser.
|
||||
pub mod ac3;
|
||||
/// DTS / DTS-HD elementary-stream parser.
|
||||
pub mod dts;
|
||||
/// DVD bitmap subtitle (VobSub) parser.
|
||||
pub mod dvdsub;
|
||||
/// H.264 (AVC) Annex-B elementary-stream parser.
|
||||
pub mod h264;
|
||||
/// HEVC (H.265) Annex-B elementary-stream parser.
|
||||
pub mod hevc;
|
||||
/// BD/DVD LPCM (Linear PCM) audio parser.
|
||||
pub mod lpcm;
|
||||
/// MPEG-2 Video elementary-stream parser.
|
||||
pub mod mpeg2;
|
||||
/// HDMV PGS (Presentation Graphics Stream) subtitle parser.
|
||||
pub mod pgs;
|
||||
/// Shared MPEG/Annex-B start-code scanning helpers.
|
||||
pub(crate) mod startcode;
|
||||
/// Dolby TrueHD / Atmos elementary-stream parser.
|
||||
pub mod truehd;
|
||||
/// VC-1 (SMPTE 421M) elementary-stream parser.
|
||||
pub mod vc1;
|
||||
|
||||
use super::ts::PesPacket;
|
||||
@@ -70,12 +82,20 @@ pub trait CodecParser: Send {
|
||||
}
|
||||
|
||||
/// Passthrough parser — treats each PES as one frame, no parsing.
|
||||
/// Used for codecs where PES = frame (AC3, DTS, PGS).
|
||||
///
|
||||
/// Used for the audio codecs that have no dedicated parser and whose PES
|
||||
/// boundaries already line up with frame boundaries (Aac, Mp2, Mp3, Flac,
|
||||
/// Opus). AC3/DTS/TrueHD have their own parsers; PGS/DvdSub have their own
|
||||
/// subtitle parsers. Video codecs must NOT use the all-keyframe form of this
|
||||
/// parser — see `parser_for_codec`.
|
||||
pub struct PassthroughParser {
|
||||
keyframe: bool,
|
||||
}
|
||||
|
||||
impl PassthroughParser {
|
||||
/// Create a passthrough parser. Pass `true` for codecs where every PES is
|
||||
/// independently decodable (audio / subtitle keyframes), `false` for the
|
||||
/// video fallback where no frame-boundary or keyframe detection occurs.
|
||||
pub fn new(always_keyframe: bool) -> Self {
|
||||
Self {
|
||||
keyframe: always_keyframe,
|
||||
@@ -124,6 +144,69 @@ pub fn parser_for_codec(
|
||||
Codec::Lpcm if is_dvd_ps => Box::new(lpcm::LpcmParser::new_dvd()),
|
||||
Codec::Lpcm => Box::new(lpcm::LpcmParser::new()),
|
||||
Codec::DvdSub => Box::new(dvdsub::DvdSubParser::new(codec_data)),
|
||||
_ => Box::new(PassthroughParser::new(true)),
|
||||
// Video codecs with no dedicated parser. There is no frame-boundary
|
||||
// detection here, so a PES carrying multiple access units is emitted as
|
||||
// one oversized block — but marking every frame a keyframe (as the
|
||||
// audio passthrough does) would explode Cues density and mislead
|
||||
// seeking. Use the non-keyframe passthrough and warn that framing is
|
||||
// approximate. Mpeg1/Av1 are real Codec variants without a parser yet.
|
||||
Codec::Mpeg1 | Codec::Av1 => {
|
||||
tracing::warn!(
|
||||
target: "mux",
|
||||
"no dedicated parser for video codec {:?}; using non-keyframe passthrough (frame boundaries/keyframes not detected)",
|
||||
codec
|
||||
);
|
||||
Box::new(PassthroughParser::new(false))
|
||||
}
|
||||
// Remaining audio-only codecs (Aac, Mp2, Mp3, Flac, Opus) where PES =
|
||||
// frame: all-keyframe passthrough is correct. Subtitle/Unknown also land
|
||||
// here; keyframe flag is irrelevant for them.
|
||||
Codec::Aac | Codec::Mp2 | Codec::Mp3 | Codec::Flac | Codec::Opus => {
|
||||
Box::new(PassthroughParser::new(true))
|
||||
}
|
||||
Codec::Srt | Codec::Ssa | Codec::Unknown(_) => Box::new(PassthroughParser::new(true)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn pes(pts: Option<i64>, data: Vec<u8>) -> PesPacket {
|
||||
PesPacket {
|
||||
pid: 0x1011,
|
||||
pts,
|
||||
dts: None,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unhandled_video_codecs_use_non_keyframe_passthrough() {
|
||||
// Mpeg1/Av1 have no dedicated parser. They must NOT be marked
|
||||
// all-keyframe (that would explode Cues density and mislead seeking);
|
||||
// the non-keyframe passthrough is the safe fallback.
|
||||
for codec in [Codec::Mpeg1, Codec::Av1] {
|
||||
let mut parser = parser_for_codec(codec, None, false);
|
||||
let frames = parser.parse(&pes(Some(9000), vec![0xDE, 0xAD, 0xBE, 0xEF]));
|
||||
assert_eq!(frames.len(), 1, "{codec:?}");
|
||||
assert!(
|
||||
!frames[0].keyframe,
|
||||
"{codec:?} must not be flagged keyframe by the fallback parser"
|
||||
);
|
||||
assert_eq!(frames[0].data, vec![0xDE, 0xAD, 0xBE, 0xEF]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unhandled_audio_codecs_use_keyframe_passthrough() {
|
||||
// PES = frame audio codecs: every frame is independently decodable, so
|
||||
// all-keyframe passthrough is correct.
|
||||
for codec in [Codec::Aac, Codec::Mp2, Codec::Mp3, Codec::Flac, Codec::Opus] {
|
||||
let mut parser = parser_for_codec(codec, None, false);
|
||||
let frames = parser.parse(&pes(Some(0), vec![0x01, 0x02]));
|
||||
assert_eq!(frames.len(), 1, "{codec:?}");
|
||||
assert!(frames[0].keyframe, "{codec:?} should be keyframe");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+96
-28
@@ -9,6 +9,7 @@
|
||||
//! - Sequence extension: 00 00 01 B5
|
||||
//! - Picture header: 00 00 01 00
|
||||
|
||||
use super::startcode::find_start_code;
|
||||
use super::{CodecParser, Frame, pts_to_ns};
|
||||
use crate::mux::ts::PesPacket;
|
||||
|
||||
@@ -61,6 +62,7 @@ impl Default for Mpeg2Parser {
|
||||
}
|
||||
|
||||
impl Mpeg2Parser {
|
||||
/// Create a new MPEG-2 parser with no captured sequence-header state.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
seq_header: None,
|
||||
@@ -102,8 +104,13 @@ impl CodecParser for Mpeg2Parser {
|
||||
// PTS-based seeking. Fall back to DTS only if PTS is absent.
|
||||
let pts_ns = pes.pts.or(pes.dts).map(pts_to_ns).unwrap_or(0);
|
||||
let data = &pes.data;
|
||||
let mut keyframe = false;
|
||||
// Keyframe-ness is a property of the coded PICTURE, not of a sequence
|
||||
// header. A PES may carry a sequence header followed by a P/B-frame
|
||||
// (open-GOP / re-encoded MPEG-2); the picture, not the seq header,
|
||||
// decides the cue point. Set this only from the PICTURE_CODE arm.
|
||||
let mut picture_is_keyframe = false;
|
||||
let mut has_picture = false;
|
||||
let mut saw_seq_header = false;
|
||||
|
||||
// Scan for start codes in the elementary stream data.
|
||||
let mut pos = 0;
|
||||
@@ -165,7 +172,19 @@ impl CodecParser for Mpeg2Parser {
|
||||
};
|
||||
|
||||
self.seq_header = Some(data[hdr_start..hdr_end].to_vec());
|
||||
keyframe = true;
|
||||
// A NEW sequence header replaces the stored one, so its B5
|
||||
// sequence extension must be re-captured. Reset the flag the
|
||||
// SEQ_EXT_CODE arm guards on; otherwise, once the first
|
||||
// header's B3+B5 pair was seen, every later header (channel
|
||||
// change, title boundary, parser reuse) would be stored
|
||||
// without its extension bytes — corrupting codecPrivate
|
||||
// (interlace, chroma format, progressive-sequence flags).
|
||||
self.has_extension = false;
|
||||
// NOTE: a sequence header does NOT make the access unit a
|
||||
// keyframe — that is decided solely by the PICTURE_CODE arm
|
||||
// (picture_is_keyframe). Setting it here would mis-cue a
|
||||
// seq-header-followed-by-P/B-frame PES.
|
||||
saw_seq_header = true;
|
||||
pos = if next_sc.is_some() { hdr_end } else { sc + 4 };
|
||||
}
|
||||
SEQ_EXT_CODE if self.seq_header.is_some() && !self.has_extension => {
|
||||
@@ -185,7 +204,7 @@ impl CodecParser for Mpeg2Parser {
|
||||
if sc + 5 < data.len() {
|
||||
let picture_coding_type = (data[sc + 5] >> 3) & 0x07;
|
||||
if picture_coding_type == PICTURE_TYPE_I {
|
||||
keyframe = true;
|
||||
picture_is_keyframe = true;
|
||||
}
|
||||
}
|
||||
pos = sc + 4;
|
||||
@@ -209,13 +228,15 @@ impl CodecParser for Mpeg2Parser {
|
||||
// header and no picture. A PES with neither (e.g. a slice
|
||||
// continuation) still passes through unchanged, preserving real
|
||||
// keyframe detection.
|
||||
if !has_picture && contains_seq_header(data) {
|
||||
// `saw_seq_header` is set by the scan loop's SEQ_HEADER_CODE arm above,
|
||||
// so this reuses that single pass instead of re-scanning the PES bytes.
|
||||
if !has_picture && saw_seq_header {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
vec![Frame {
|
||||
pts_ns,
|
||||
keyframe,
|
||||
keyframe: picture_is_keyframe,
|
||||
data: pes.data.clone(),
|
||||
duration_ns: None,
|
||||
}]
|
||||
@@ -264,29 +285,6 @@ fn parse_aspect_ratio(hdr: &[u8]) -> Option<(u8, u8)> {
|
||||
Some(ASPECT_RATIOS[ar_code])
|
||||
}
|
||||
|
||||
/// Returns true if `data` contains a sequence-header start code (00 00 01 B3).
|
||||
fn contains_seq_header(data: &[u8]) -> bool {
|
||||
let mut pos = 0;
|
||||
while let Some(sc) = find_start_code(data, pos) {
|
||||
if sc + 3 >= data.len() {
|
||||
break;
|
||||
}
|
||||
if data[sc + 3] == SEQ_HEADER_CODE {
|
||||
return true;
|
||||
}
|
||||
pos = sc + 4;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Find the position of the next start code (00 00 01) at or after `from`.
|
||||
fn find_start_code(data: &[u8], from: usize) -> Option<usize> {
|
||||
if data.len() < from + 3 {
|
||||
return None;
|
||||
}
|
||||
(from..data.len() - 2).find(|&i| data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x01)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -487,6 +485,33 @@ mod tests {
|
||||
assert!(parser.codec_private().is_some());
|
||||
}
|
||||
|
||||
// --- seq-header keyframe flag must not leak into a P/B-frame ---
|
||||
|
||||
#[test]
|
||||
fn seq_header_then_p_frame_is_not_keyframe() {
|
||||
// A PES carrying a sequence header followed by a P-frame (open-GOP /
|
||||
// re-encoded MPEG-2) must NOT be flagged a keyframe — the keyframe-ness
|
||||
// belongs to the coded picture, not the sequence header. A spurious
|
||||
// keyframe here produces a bad MKV cue point.
|
||||
let mut parser = Mpeg2Parser::new();
|
||||
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&make_seq_header(720, 480, 3, 4));
|
||||
data.extend_from_slice(&make_picture_header(2)); // P-frame
|
||||
data.extend_from_slice(&[0xFF; 16]);
|
||||
|
||||
let pes = make_pes(data, Some(0));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert!(
|
||||
!frames[0].keyframe,
|
||||
"seq-header + P-frame must not be a keyframe"
|
||||
);
|
||||
// The sequence header is still captured for codecPrivate.
|
||||
assert!(parser.codec_private().is_some());
|
||||
}
|
||||
|
||||
// --- parameter-set-only PES (seq header, no picture) emits no frame ---
|
||||
|
||||
#[test]
|
||||
@@ -522,6 +547,49 @@ mod tests {
|
||||
assert!(frames2[0].keyframe);
|
||||
}
|
||||
|
||||
// --- a SECOND sequence header re-captures its extension ---
|
||||
|
||||
#[test]
|
||||
fn new_sequence_header_recaptures_extension() {
|
||||
// Regression: has_extension was never reset when a new sequence header
|
||||
// replaced the stored one, so a second header (channel change / title
|
||||
// boundary) was stored WITHOUT its B5 sequence extension. To exercise
|
||||
// the SEQ_EXT_CODE arm (which the has_extension flag guards), each
|
||||
// header and its extension arrive in SEPARATE PES packets.
|
||||
let mut parser = Mpeg2Parser::new();
|
||||
|
||||
// Header A (no trailing start code → captured alone), then its B5
|
||||
// extension in the next PES.
|
||||
let _ = parser.parse(&make_pes(make_seq_header(1920, 1080, 3, 4), Some(0)));
|
||||
let mut ext_a = vec![0x00, 0x00, 0x01, SEQ_EXT_CODE];
|
||||
ext_a.extend_from_slice(&[0x11, 0x22, 0x33, 0x44, 0x55, 0x66]);
|
||||
let _ = parser.parse(&make_pes(ext_a, Some(0)));
|
||||
assert!(
|
||||
parser
|
||||
.codec_private()
|
||||
.unwrap()
|
||||
.windows(6)
|
||||
.any(|w| w == [0x11, 0x22, 0x33, 0x44, 0x55, 0x66]),
|
||||
"first header's extension captured (has_extension now true)"
|
||||
);
|
||||
|
||||
// A NEW header B, then ITS extension in a separate PES. With the bug,
|
||||
// has_extension stayed true and this extension would be dropped.
|
||||
let _ = parser.parse(&make_pes(make_seq_header(720, 480, 2, 4), Some(3600)));
|
||||
let mut ext_b = vec![0x00, 0x00, 0x01, SEQ_EXT_CODE];
|
||||
ext_b.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
|
||||
let _ = parser.parse(&make_pes(ext_b, Some(3600)));
|
||||
|
||||
let cp2 = parser.codec_private().unwrap();
|
||||
assert!(
|
||||
cp2.windows(6)
|
||||
.any(|w| w == [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]),
|
||||
"second header's extension must be re-captured, not dropped"
|
||||
);
|
||||
// It is header B (720x480), not stale header A.
|
||||
assert_eq!(parser.resolution(), Some((720, 480)));
|
||||
}
|
||||
|
||||
// --- PTS conversion ---
|
||||
|
||||
#[test]
|
||||
|
||||
+153
-19
@@ -31,6 +31,8 @@ const MAX_PGS_PENDING_BYTES: usize = 4 * 1024 * 1024;
|
||||
// palette_id_ref) = 13.
|
||||
const PCS_NUM_OBJECTS_OFFSET: usize = 13;
|
||||
|
||||
/// Stateful parser that collapses PGS display/clear PCS pairs into
|
||||
/// duration-bearing Matroska frames. Implements [`CodecParser`].
|
||||
pub struct PgsParser {
|
||||
pending: Option<(i64, Vec<u8>)>,
|
||||
}
|
||||
@@ -42,9 +44,25 @@ impl Default for PgsParser {
|
||||
}
|
||||
|
||||
impl PgsParser {
|
||||
/// Create a fresh PGS parser with no pending display set.
|
||||
pub fn new() -> Self {
|
||||
Self { pending: None }
|
||||
}
|
||||
|
||||
/// Take the pending display set (if any) and emit it as a Frame whose
|
||||
/// duration runs from its start PTS to `end_pts_ns` (the PTS of the PCS that
|
||||
/// closes or replaces it), clamped to >= 0. Shared by the clear-PCS and
|
||||
/// replace-PCS arms so the Frame shape stays in one place.
|
||||
fn emit_pending(&mut self, end_pts_ns: i64) -> Option<Frame> {
|
||||
let (start_pts, data) = self.pending.take()?;
|
||||
let duration = end_pts_ns.saturating_sub(start_pts).max(0) as u64;
|
||||
Some(Frame {
|
||||
pts_ns: start_pts,
|
||||
keyframe: true,
|
||||
data,
|
||||
duration_ns: Some(duration),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CodecParser for PgsParser {
|
||||
@@ -52,10 +70,36 @@ impl CodecParser for PgsParser {
|
||||
if pes.data.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0);
|
||||
// Keep PTS as Option: a PCS with no PTS has an UNKNOWN start/clear time.
|
||||
// Collapsing it to a 0 sentinel produces a frame with a wrong start time
|
||||
// and an absurd duration (the full elapsed time of the disc). PGS PCS
|
||||
// packets carry a PTS on well-formed BD streams, so a missing PTS is a
|
||||
// malformed-stream path that we skip cleanly rather than corrupt.
|
||||
let pts = pes.pts.map(pts_to_ns);
|
||||
|
||||
let is_pcs = pes.data[0] == SEGMENT_PCS;
|
||||
let pcs_num_objects = if is_pcs && pes.data.len() > PCS_NUM_OBJECTS_OFFSET {
|
||||
|
||||
// A PCS too short to carry number_of_composition_objects is malformed.
|
||||
// Don't let it fall through to the non-PCS arm (where it would pollute
|
||||
// the pending display set or pass through as a lone frame): close any
|
||||
// pending set undurated (mirroring the no-PTS display path) and drop
|
||||
// the truncated header so the parser resyncs on the next PCS.
|
||||
if is_pcs && pes.data.len() <= PCS_NUM_OBJECTS_OFFSET {
|
||||
return self
|
||||
.pending
|
||||
.take()
|
||||
.map(|(start_pts, data)| {
|
||||
vec![Frame {
|
||||
pts_ns: start_pts,
|
||||
keyframe: true,
|
||||
data,
|
||||
duration_ns: None,
|
||||
}]
|
||||
})
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
let pcs_num_objects = if is_pcs {
|
||||
Some(pes.data[PCS_NUM_OBJECTS_OFFSET])
|
||||
} else {
|
||||
None
|
||||
@@ -65,33 +109,40 @@ impl CodecParser for PgsParser {
|
||||
match pcs_num_objects {
|
||||
// Clear/empty PCS — closes any pending display. Drop the
|
||||
// clear segment itself; BlockDuration covers the screen
|
||||
// wipe.
|
||||
// wipe. A clear PCS with no PTS can't time the duration, so
|
||||
// emit the pending set with no duration (it lingers to EOF).
|
||||
Some(0) => {
|
||||
if let Some((start_pts, data)) = self.pending.take() {
|
||||
let duration = pts_ns.saturating_sub(start_pts).max(0) as u64;
|
||||
out.push(Frame {
|
||||
let frame = match pts {
|
||||
Some(end) => self.emit_pending(end),
|
||||
None => self.pending.take().map(|(start_pts, data)| Frame {
|
||||
pts_ns: start_pts,
|
||||
keyframe: true,
|
||||
data,
|
||||
duration_ns: Some(duration),
|
||||
});
|
||||
}
|
||||
duration_ns: None,
|
||||
}),
|
||||
};
|
||||
out.extend(frame);
|
||||
}
|
||||
// Display PCS — start a new pending. If a prior display
|
||||
// was never explicitly cleared (replace-without-clear),
|
||||
// emit it with the new PCS's PTS as its end.
|
||||
Some(_) => {
|
||||
if let Some((start_pts, data)) = self.pending.take() {
|
||||
let duration = pts_ns.saturating_sub(start_pts).max(0) as u64;
|
||||
out.push(Frame {
|
||||
Some(_) => match pts {
|
||||
Some(start) => {
|
||||
out.extend(self.emit_pending(start));
|
||||
self.pending = Some((start, pes.data.clone()));
|
||||
}
|
||||
// A display PCS with no PTS has an unknown start time. Don't
|
||||
// store it with a 0 sentinel (wrong start, absurd duration).
|
||||
// Flush any prior pending undurated and skip storing this one.
|
||||
None => {
|
||||
out.extend(self.pending.take().map(|(start_pts, data)| Frame {
|
||||
pts_ns: start_pts,
|
||||
keyframe: true,
|
||||
data,
|
||||
duration_ns: Some(duration),
|
||||
});
|
||||
duration_ns: None,
|
||||
}));
|
||||
}
|
||||
self.pending = Some((pts_ns, pes.data.clone()));
|
||||
}
|
||||
},
|
||||
// Non-PCS first segment — either a continuation of the
|
||||
// current display set, or non-standard layout. If we have
|
||||
// a pending display, append; otherwise emit as-is.
|
||||
@@ -103,14 +154,21 @@ impl CodecParser for PgsParser {
|
||||
if buf.len() + pes.data.len() <= MAX_PGS_PENDING_BYTES {
|
||||
buf.extend_from_slice(&pes.data);
|
||||
}
|
||||
} else {
|
||||
} else if pes.pts.is_some() {
|
||||
// A lone non-PCS segment with a real PTS — pass it through.
|
||||
// (A missing PTS falls through to the drop path below: a
|
||||
// bitmap with no timing reference would land at 00:00:00.)
|
||||
out.push(Frame {
|
||||
pts_ns,
|
||||
pts_ns: pts.unwrap_or(0),
|
||||
keyframe: true,
|
||||
data: pes.data.clone(),
|
||||
duration_ns: None,
|
||||
});
|
||||
}
|
||||
// No pending set AND no PTS: drop it. Emitting at pts_ns=0 would
|
||||
// place a stray bitmap at 00:00:00.000 with no timing reference;
|
||||
// the no-PTS PCS arms above avoid the 0 sentinel for the same
|
||||
// reason.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,6 +313,82 @@ mod tests {
|
||||
assert_eq!(frames[0].duration_ns, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_pcs_without_pts_is_not_stored_with_zero_start() {
|
||||
// A display PCS with no PTS has an unknown start time. It must NOT be
|
||||
// stored with a 0 sentinel — otherwise a later clear PCS at real PTS T
|
||||
// would emit a frame with pts_ns=0 and duration_ns=T (hours of ns for a
|
||||
// mid-disc subtitle). The malformed display PCS is skipped instead.
|
||||
let mut parser = PgsParser::new();
|
||||
let frames = parser.parse(&make_pes(pcs_bytes(1), None));
|
||||
assert!(frames.is_empty(), "no-PTS display PCS emits nothing");
|
||||
assert!(
|
||||
parser.pending.is_none(),
|
||||
"no-PTS display PCS must not be stored as pending"
|
||||
);
|
||||
|
||||
// A subsequent well-formed display + clear pair must time correctly,
|
||||
// unpolluted by the skipped no-PTS PCS.
|
||||
let _ = parser.parse(&make_pes(pcs_bytes(1), Some(90000)));
|
||||
let f = parser.parse(&make_pes(pcs_bytes(0), Some(270000)));
|
||||
assert_eq!(f.len(), 1);
|
||||
assert_eq!(f[0].pts_ns, 1_000_000_000);
|
||||
assert_eq!(f[0].duration_ns, Some(2_000_000_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_pcs_without_pts_emits_pending_undurated() {
|
||||
// A clear PCS that lacks a PTS can't compute a duration; the pending
|
||||
// display is still emitted, but with no duration (lingers to EOF)
|
||||
// instead of a bogus absurd one.
|
||||
let mut parser = PgsParser::new();
|
||||
let _ = parser.parse(&make_pes(pcs_bytes(1), Some(90000)));
|
||||
let f = parser.parse(&make_pes(pcs_bytes(0), None));
|
||||
assert_eq!(f.len(), 1);
|
||||
assert_eq!(f[0].pts_ns, 1_000_000_000, "pending keeps its real start");
|
||||
assert_eq!(f[0].duration_ns, None, "no duration without a clear PTS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_pcs_flushes_pending_and_resyncs() {
|
||||
// A PCS too short to carry number_of_composition_objects arriving with a
|
||||
// pending display must close that display (undurated) and drop the
|
||||
// truncated header, not append its bytes into the pending bitmap.
|
||||
let mut parser = PgsParser::new();
|
||||
let display = pcs_bytes(1);
|
||||
assert!(
|
||||
parser
|
||||
.parse(&make_pes(display.clone(), Some(90000)))
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
// A 13-byte (<= PCS_NUM_OBJECTS_OFFSET) PCS: truncated.
|
||||
let truncated = vec![SEGMENT_PCS; PCS_NUM_OBJECTS_OFFSET];
|
||||
let frames = parser.parse(&make_pes(truncated, Some(180000)));
|
||||
assert_eq!(frames.len(), 1, "pending display flushed on truncated PCS");
|
||||
assert_eq!(frames[0].data, display, "pending bitmap not polluted");
|
||||
assert_eq!(frames[0].duration_ns, None, "flushed undurated");
|
||||
assert!(parser.pending.is_none(), "parser resynced");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lone_non_pcs_without_pts_is_dropped() {
|
||||
// A non-PCS segment with no pending set and no PTS must be dropped, not
|
||||
// emitted at pts_ns = 0 (which would land a stray bitmap at time zero).
|
||||
let mut parser = PgsParser::new();
|
||||
let frames = parser.parse(&make_pes(vec![0x15, 0x00, 0x02, 0xAA], None));
|
||||
assert!(frames.is_empty(), "no pending + no PTS → dropped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lone_non_pcs_with_pts_passes_through() {
|
||||
// A lone non-PCS segment WITH a PTS still passes through.
|
||||
let mut parser = PgsParser::new();
|
||||
let frames = parser.parse(&make_pes(vec![0x15, 0x00, 0x02, 0xAA], Some(90000)));
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].pts_ns, 1_000_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flush_with_nothing_pending_is_empty() {
|
||||
let mut parser = PgsParser::new();
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
//! Shared MPEG/Annex-B start-code scanning helpers.
|
||||
//!
|
||||
//! H.264, HEVC, MPEG-2 and the MPEG-2 Program Stream demuxer all locate the
|
||||
//! 3-byte `00 00 01` start-code prefix to delimit NAL units / PES units. A
|
||||
//! single memchr-backed implementation lives here so every caller gets the
|
||||
//! same SIMD-accelerated scan instead of a hand-rolled byte-by-byte loop.
|
||||
|
||||
/// Find the position of the next start code (`00 00 01`) at or after `from`.
|
||||
///
|
||||
/// Backed by `memchr::memmem::find` for SIMD-accelerated bytestring search. On
|
||||
/// AVX2-capable x86_64 this runs several times faster than a byte-by-byte scan;
|
||||
/// on a 200 KB UHD HEVC frame the saving is in the hundreds of microseconds per
|
||||
/// call. The reported offset is the start of the `00 00 01` triple, so a 4-byte
|
||||
/// `00 00 00 01` start code is reported at the second `00`.
|
||||
pub fn find_start_code(data: &[u8], from: usize) -> Option<usize> {
|
||||
if data.len() < from + 3 {
|
||||
return None;
|
||||
}
|
||||
memchr::memmem::find(&data[from..], b"\x00\x00\x01").map(|rel| from + rel)
|
||||
}
|
||||
|
||||
/// Skip past the start code at position `pos`, returning the first byte after
|
||||
/// it. Handles both the 3-byte (`00 00 01`) and 4-byte (`00 00 00 01`) forms.
|
||||
/// Returns `None` if `pos` does not begin a start code or the buffer is too
|
||||
/// short to contain one.
|
||||
pub fn skip_start_code(data: &[u8], pos: usize) -> Option<usize> {
|
||||
if pos + 2 >= data.len() {
|
||||
return None;
|
||||
}
|
||||
if data[pos] == 0x00 && data[pos + 1] == 0x00 {
|
||||
if pos + 3 < data.len() && data[pos + 2] == 0x00 && data[pos + 3] == 0x01 {
|
||||
return Some(pos + 4); // 4-byte start code
|
||||
}
|
||||
if data[pos + 2] == 0x01 {
|
||||
return Some(pos + 3); // 3-byte start code
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn find_start_code_3byte() {
|
||||
let data = [0x00, 0x00, 0x01, 0x65];
|
||||
assert_eq!(find_start_code(&data, 0), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_start_code_4byte() {
|
||||
let data = [0x00, 0x00, 0x00, 0x01, 0x65];
|
||||
// The 00 00 01 triple starts at offset 1 in a 4-byte start code.
|
||||
assert_eq!(find_start_code(&data, 0), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_start_code_offset() {
|
||||
let data = [0xFF, 0xFF, 0x00, 0x00, 0x01, 0x09];
|
||||
assert_eq!(find_start_code(&data, 0), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_start_code_none() {
|
||||
let data = [0x00, 0x00, 0x00, 0x00];
|
||||
assert_eq!(find_start_code(&data, 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_start_code_too_short() {
|
||||
let data = [0x00, 0x00];
|
||||
assert_eq!(find_start_code(&data, 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skip_3byte() {
|
||||
let data = [0x00, 0x00, 0x01, 0x65];
|
||||
assert_eq!(skip_start_code(&data, 0), Some(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skip_4byte() {
|
||||
let data = [0x00, 0x00, 0x00, 0x01, 0x65];
|
||||
assert_eq!(skip_start_code(&data, 0), Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skip_not_a_start_code() {
|
||||
let data = [0xFF, 0x00, 0x01, 0x65];
|
||||
assert_eq!(skip_start_code(&data, 0), None);
|
||||
}
|
||||
}
|
||||
+155
-59
@@ -4,9 +4,10 @@
|
||||
//! Access units span PES boundaries — must buffer and reassemble.
|
||||
//!
|
||||
//! TrueHD access unit header (4 bytes):
|
||||
//! [0..1] upper 4 bits = parity, lower 12 bits = length in 2-byte words
|
||||
//! [2..3] timing value
|
||||
//! [4..] substream data (major sync 0xF8726FBA may appear at offset 4)
|
||||
//! bytes 0-1: top nibble = MLP check/access-unit nibble, lower 12 bits =
|
||||
//! access-unit length in 2-byte words
|
||||
//! bytes 2-3: timing value
|
||||
//! bytes 4..: substream data (major sync 0xF8726FBA may appear at offset 4)
|
||||
//!
|
||||
//! AC-3 frames (interleaved, same PID): start with sync word 0x0B77.
|
||||
//! We skip AC-3 frames and only emit TrueHD access units.
|
||||
@@ -41,55 +42,70 @@ impl TrueHdParser {
|
||||
}
|
||||
}
|
||||
|
||||
/// Skip an AC-3 frame starting at the current buffer position.
|
||||
/// Returns number of bytes consumed, or 0 if not enough data.
|
||||
fn skip_ac3_frame(&self) -> usize {
|
||||
/// Size (bytes) of the AC-3 frame at the buffer head.
|
||||
///
|
||||
/// Distinguishes three cases the caller must treat differently:
|
||||
/// - `Unmappable`: the header's fscod/frmsizecod don't map to a real frame
|
||||
/// size (reserved fscod==3, or frmsizecod >= 38). The caller must drain
|
||||
/// and resync, NOT wait for more data — waiting would stall forever.
|
||||
/// - `NeedMore`: a valid size, but the frame isn't fully buffered yet.
|
||||
/// - `Frame(n)`: a complete `n`-byte AC-3 frame is buffered.
|
||||
///
|
||||
/// Frame sizing reuses `ac3::ac3_frame_size` so the AC-3 size table has a
|
||||
/// single source of truth shared with the AC-3 parser; a returned `0` there
|
||||
/// (reserved fscod or out-of-range frmsizecod) is the unmappable case.
|
||||
fn ac3_frame_at_head(&self) -> Ac3Size {
|
||||
if self.buf.len() < 6 {
|
||||
return 0;
|
||||
return Ac3Size::NeedMore;
|
||||
}
|
||||
// AC-3 frame size from frmsizcod + fscod
|
||||
// Byte 4: [fscod:2][frmsizecod:6]
|
||||
let fscod = (self.buf[4] >> 6) & 0x03;
|
||||
let frmsizecod = (self.buf[4] & 0x3F) as usize;
|
||||
// Frame size in 16-bit words per fscod (simplified table for common rates)
|
||||
let frame_words = match fscod {
|
||||
0 => {
|
||||
// 48 kHz
|
||||
static SIZES: [usize; 38] = [
|
||||
64, 64, 80, 80, 96, 96, 112, 112, 128, 128, 160, 160, 192, 192, 224, 224, 256,
|
||||
256, 320, 320, 384, 384, 448, 448, 512, 512, 640, 640, 768, 768, 896, 896,
|
||||
1024, 1024, 1152, 1152, 1280, 1280,
|
||||
];
|
||||
SIZES.get(frmsizecod).copied().unwrap_or(0)
|
||||
}
|
||||
1 => {
|
||||
// 44.1 kHz
|
||||
static SIZES: [usize; 38] = [
|
||||
69, 70, 87, 88, 104, 105, 121, 122, 139, 140, 174, 175, 208, 209, 243, 244,
|
||||
278, 279, 348, 349, 417, 418, 487, 488, 557, 558, 696, 697, 835, 836, 975, 976,
|
||||
1114, 1115, 1253, 1254, 1393, 1394,
|
||||
];
|
||||
SIZES.get(frmsizecod).copied().unwrap_or(0)
|
||||
}
|
||||
2 => {
|
||||
// 32 kHz
|
||||
static SIZES: [usize; 38] = [
|
||||
96, 96, 120, 120, 144, 144, 168, 168, 192, 192, 240, 240, 288, 288, 336, 336,
|
||||
384, 384, 480, 480, 576, 576, 672, 672, 768, 768, 960, 960, 1152, 1152, 1344,
|
||||
1344, 1536, 1536, 1728, 1728, 1920, 1920,
|
||||
];
|
||||
SIZES.get(frmsizecod).copied().unwrap_or(0)
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
let frame_bytes = frame_words * 2;
|
||||
if frame_bytes == 0 || self.buf.len() < frame_bytes {
|
||||
return 0;
|
||||
let frame_bytes = super::ac3::ac3_frame_size(&self.buf);
|
||||
if frame_bytes == 0 {
|
||||
// Reserved fscod or out-of-range frmsizecod → unmappable header.
|
||||
return Ac3Size::Unmappable;
|
||||
}
|
||||
frame_bytes
|
||||
if self.buf.len() < frame_bytes {
|
||||
return Ac3Size::NeedMore;
|
||||
}
|
||||
Ac3Size::Frame(frame_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Secondary validation for an AC-3 frame of `frame_bytes` at the buffer head:
|
||||
/// is its computed end a plausible boundary? Accept when the frame fills the
|
||||
/// rest of the buffer, or the bytes that follow start another AC-3 sync
|
||||
/// (0x0B77) or a plausible TrueHD access unit (non-zero 12-bit length within
|
||||
/// the 32 KiB cap). If none holds, the leading 0x0B77 is more likely a TrueHD
|
||||
/// AU header that happens to look like AC-3, so the AC-3 reading is rejected.
|
||||
fn ac3_boundary_corroborated(buf: &[u8], frame_bytes: usize) -> bool {
|
||||
if frame_bytes >= buf.len() {
|
||||
// The AC-3 frame is fully buffered and ends the data — consistent.
|
||||
return true;
|
||||
}
|
||||
let tail = &buf[frame_bytes..];
|
||||
if tail.len() < 2 {
|
||||
// Not enough following bytes to judge; accept (the next call will see
|
||||
// the continuation).
|
||||
return true;
|
||||
}
|
||||
// Another AC-3 sync immediately after?
|
||||
if tail[0] == 0x0B && tail[1] == 0x77 {
|
||||
return true;
|
||||
}
|
||||
// A plausible TrueHD AU header after? (non-zero 12-bit length, <= 32 KiB)
|
||||
let next_words = (((tail[0] as usize) << 8) | tail[1] as usize) & 0xFFF;
|
||||
next_words != 0 && next_words * 2 <= 32768
|
||||
}
|
||||
|
||||
/// Outcome of sizing the AC-3 frame at the TrueHD buffer head.
|
||||
enum Ac3Size {
|
||||
/// fscod/frmsizecod don't map to a real frame size — resync, don't wait.
|
||||
Unmappable,
|
||||
/// A valid size, but the frame is not fully buffered yet.
|
||||
NeedMore,
|
||||
/// A complete `n`-byte AC-3 frame is buffered.
|
||||
Frame(usize),
|
||||
}
|
||||
|
||||
impl CodecParser for TrueHdParser {
|
||||
fn parse(&mut self, pes: &PesPacket) -> Vec<Frame> {
|
||||
if pes.data.is_empty() {
|
||||
@@ -118,29 +134,48 @@ impl CodecParser for TrueHdParser {
|
||||
break;
|
||||
}
|
||||
|
||||
// AC-3 frame (interleaved): starts with sync word 0x0B77
|
||||
// AC-3 frame (interleaved): starts with sync word 0x0B77.
|
||||
//
|
||||
// 0x0B 0x77 is also a legal TrueHD AU header (check-nibble 0,
|
||||
// length-high-bits 0xB → length 0xB77 words). To avoid an AC-3
|
||||
// misread stealing a real TrueHD AU, an AC-3 frame is only accepted
|
||||
// when its computed end is corroborated by what follows: end of
|
||||
// buffer (frame fills the rest), another AC-3 sync, or a plausible
|
||||
// TrueHD AU header. If none holds, this is treated as a TrueHD AU.
|
||||
if self.buf[0] == 0x0B && self.buf[1] == 0x77 {
|
||||
let skip = self.skip_ac3_frame();
|
||||
if skip == 0 {
|
||||
break; // incomplete AC-3 frame, wait for more data
|
||||
match self.ac3_frame_at_head() {
|
||||
Ac3Size::Unmappable => {
|
||||
// Permanently unmappable header at the head would stall
|
||||
// the parser forever; resync by dropping 2 bytes so one
|
||||
// bad frame costs one frame, not the whole buffer.
|
||||
self.buf.drain(..2);
|
||||
continue;
|
||||
}
|
||||
Ac3Size::NeedMore => break, // wait for the rest of the frame
|
||||
Ac3Size::Frame(skip) => {
|
||||
if ac3_boundary_corroborated(&self.buf, skip) {
|
||||
self.buf.drain(..skip);
|
||||
continue;
|
||||
}
|
||||
// Not corroborated — fall through and interpret the
|
||||
// 0x0B77 bytes as a TrueHD access unit instead.
|
||||
}
|
||||
}
|
||||
self.buf.drain(..skip);
|
||||
continue;
|
||||
}
|
||||
|
||||
// TrueHD access unit: lower 12 bits of first 2 bytes = length in words
|
||||
let unit_words = (((self.buf[0] as usize) << 8) | self.buf[1] as usize) & 0xFFF;
|
||||
if unit_words == 0 {
|
||||
self.buf.drain(..2);
|
||||
// A zero-length AU is malformed/padding. The AU header is 4 bytes
|
||||
// (length + timing); drain the whole header, not just the length
|
||||
// word, otherwise the timing bytes get misread as the next
|
||||
// length word and produce a spurious parse on the next iteration.
|
||||
self.buf.drain(..4);
|
||||
continue;
|
||||
}
|
||||
// unit_words is masked to 12 bits, so unit_bytes <= 4095 * 2 = 8190;
|
||||
// no separate oversize-resync guard is reachable.
|
||||
let unit_bytes = unit_words * 2;
|
||||
if unit_bytes > 32768 {
|
||||
// Likely misaligned — try to resync by scanning for AC-3 sync or
|
||||
// a valid TrueHD length
|
||||
self.buf.drain(..2);
|
||||
continue;
|
||||
}
|
||||
if self.buf.len() < unit_bytes {
|
||||
break; // incomplete access unit, wait for more data
|
||||
}
|
||||
@@ -352,6 +387,67 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_length_au_drains_full_header() {
|
||||
// A zero-length AU header (4 bytes: length=0 + timing) must be skipped
|
||||
// whole. If only 2 bytes were drained the timing bytes would be misread
|
||||
// as a bogus length word. Here the timing bytes are 0x01 0x90 (= 0x190 =
|
||||
// 400 words = 800 bytes) which, if misread, would stall the parser
|
||||
// waiting for 800 bytes that never come. Draining 4 lets the following
|
||||
// real unit parse.
|
||||
let mut parser = TrueHdParser::new();
|
||||
let mut data = vec![0x00, 0x00, 0x01, 0x90]; // length=0, timing=0x0190
|
||||
data.extend_from_slice(&make_truehd_unit(200));
|
||||
let frames = parser.parse(&make_pes(data, Some(90000)));
|
||||
assert_eq!(frames.len(), 1, "real unit parses after zero-length header");
|
||||
assert_eq!(frames[0].data.len(), 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmappable_ac3_header_resyncs_not_stalls() {
|
||||
// A permanently unmappable 0x0B77 header at the buffer head (reserved
|
||||
// fscod==3) must NOT stall the parser. It used to be treated as
|
||||
// "incomplete, wait" and break forever, dropping every following AU.
|
||||
// Now it resyncs (drains 2 bytes) so a clean TrueHD unit behind it is
|
||||
// eventually emitted.
|
||||
let mut parser = TrueHdParser::new();
|
||||
// Unmappable AC-3-looking head: 0x0B77, byte4 fscod=3 (0xC0).
|
||||
let mut data = vec![0x0B, 0x77, 0x00, 0x00, 0xC0, 0x00];
|
||||
// A clean TrueHD AU follows.
|
||||
data.extend_from_slice(&make_truehd_unit(200));
|
||||
let frames = parser.parse(&make_pes(data, Some(90000)));
|
||||
assert_eq!(
|
||||
frames.len(),
|
||||
1,
|
||||
"TrueHD AU behind a bad header is recovered"
|
||||
);
|
||||
assert_eq!(frames[0].data.len(), 200);
|
||||
assert!(parser.buf.is_empty(), "buffer fully consumed, no stall");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truehd_au_with_0b77_head_not_stolen_by_ac3() {
|
||||
// A TrueHD AU whose first two bytes are 0x0B 0x77 (length 0xB77 = 2935
|
||||
// words = 5870 bytes) must NOT be misrouted to the AC-3 path. The AC-3
|
||||
// size for this header (fscod from byte4) would close the boundary in
|
||||
// the wrong place; the secondary corroboration rejects it because the
|
||||
// computed AC-3 end is not followed by another AC-3 sync / TrueHD AU.
|
||||
let mut parser = TrueHdParser::new();
|
||||
// 5870-byte AU starting with 0x0B 0x77. Byte 4 = 0x00 → AC-3 would
|
||||
// size it as fscod=0, frmsizecod=0 → 128 bytes. The bytes at offset 128
|
||||
// are zeros (next_words==0) → not corroborated → kept as TrueHD.
|
||||
let mut unit = vec![0u8; 5870];
|
||||
unit[0] = 0x0B; // 0xB high nibble of the 12-bit length, check nibble 0
|
||||
unit[1] = 0x77; // low byte of length 0xB77
|
||||
let frames = parser.parse(&make_pes(unit, Some(90000)));
|
||||
assert_eq!(frames.len(), 1, "0x0B77-headed TrueHD AU kept whole");
|
||||
assert_eq!(
|
||||
frames[0].data.len(),
|
||||
5870,
|
||||
"AU sized by TrueHD length, not AC-3 frame size"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_private_none() {
|
||||
let parser = TrueHdParser::new();
|
||||
|
||||
+97
-14
@@ -3,7 +3,8 @@
|
||||
//! VC-1 uses start codes similar to MPEG-2.
|
||||
//! Sequence header (0x0F) contains codec initialization data.
|
||||
//! Frame start = Frame header start code (0x0D).
|
||||
//! I-frames (keyframes) are identified from the frame header.
|
||||
//! I-frames (keyframes) are signalled by the presence of a Sequence Header
|
||||
//! (0x0F) in the PES, per the BD VC-1 convention (see `parse`).
|
||||
|
||||
use super::{CodecParser, Frame, PesPacket, pts_to_ns};
|
||||
|
||||
@@ -164,25 +165,54 @@ fn parse_vc1_resolution(sh: &[u8]) -> Option<(u32, u32)> {
|
||||
// Simple/Main profile: resolution not in sequence header
|
||||
return None;
|
||||
}
|
||||
// Advanced profile layout (bit-level starting from sh[4]):
|
||||
// profile(2) + level(3) + chroma_format(2) + quantizer_spec(3) +
|
||||
// postproc_flag(1) + max_coded_width(12) + max_coded_height(12) ...
|
||||
// Total bits before width: 2+3+2+3+1 = 11 bits
|
||||
// We need at least 11+12+12 = 35 bits = 5 bytes from sh[4..]
|
||||
// Advanced profile sequence-header layout (SMPTE 421M, bit-level from sh[4]):
|
||||
// PROFILE(2) + LEVEL(3) + COLORDIFF_FORMAT(2) + FRMRTQ_POSTPROC(3) +
|
||||
// BITRTQ_POSTPROC(5) + POSTPROCFLAG(1) + MAX_CODED_WIDTH(12) +
|
||||
// MAX_CODED_HEIGHT(12) ...
|
||||
// Total bits before MAX_CODED_WIDTH: 2+3+2+3+5+1 = 16 bits.
|
||||
// We need 16+12+12 = 40 bits = 5 de-escaped bytes from sh[4..].
|
||||
if sh.len() < 9 {
|
||||
return None;
|
||||
}
|
||||
// Build a u64 from bytes 4..9 for easy bit extraction
|
||||
let mut bits: u64 = 0;
|
||||
for j in 0..5 {
|
||||
bits = (bits << 8) | sh[4 + j] as u64;
|
||||
// VC-1 Annex-B EBDU payload may carry emulation-prevention bytes (an
|
||||
// inserted 0x03 after a 00 00 run). De-escape the payload before bit
|
||||
// extraction so an EP byte landing within the first few bytes can't shift
|
||||
// every subsequent bit and corrupt MAX_CODED_WIDTH/HEIGHT. Collect just the
|
||||
// 5 de-escaped bytes the bit fields need.
|
||||
let payload = &sh[4..];
|
||||
let mut deesc = Vec::with_capacity(5);
|
||||
let mut zeros = 0u8;
|
||||
for &b in payload {
|
||||
if zeros >= 2 && b == 0x03 {
|
||||
zeros = 0; // drop the emulation-prevention byte
|
||||
continue;
|
||||
}
|
||||
deesc.push(b);
|
||||
if deesc.len() == 5 {
|
||||
break;
|
||||
}
|
||||
zeros = if b == 0x00 { zeros + 1 } else { 0 };
|
||||
}
|
||||
// bits has 40 bits. Skip first 11 bits, then read 12+12.
|
||||
let coded_width = ((bits >> (40 - 11 - 12)) & 0xFFF) as u32 + 1;
|
||||
let coded_height = ((bits >> (40 - 11 - 24)) & 0xFFF) as u32 + 1;
|
||||
if deesc.len() < 5 {
|
||||
return None;
|
||||
}
|
||||
// Build a u64 from the 5 de-escaped bytes for easy bit extraction.
|
||||
let mut bits: u64 = 0;
|
||||
for &b in &deesc {
|
||||
bits = (bits << 8) | b as u64;
|
||||
}
|
||||
// bits holds 40 significant bits laid out as:
|
||||
// [16 leading bits][MAX_CODED_WIDTH:12][MAX_CODED_HEIGHT:12]
|
||||
// so MAX_CODED_WIDTH starts 12 bits from the LSB end and MAX_CODED_HEIGHT
|
||||
// occupies the low 12 bits (shift 0).
|
||||
const WIDTH_SHIFT: u64 = 12; // 40 - 16 - 12
|
||||
let coded_width = ((bits >> WIDTH_SHIFT) & 0xFFF) as u32 + 1;
|
||||
let coded_height = (bits & 0xFFF) as u32 + 1;
|
||||
// coded_width/height are `(bits & 0xFFF) + 1`, so always >= 1; after the
|
||||
// ×2 both are always >= 2. Only the upper bound can fail.
|
||||
let w = coded_width * 2;
|
||||
let h = coded_height * 2;
|
||||
if w > 0 && h > 0 && w <= 8192 && h <= 8192 {
|
||||
if w <= 8192 && h <= 8192 {
|
||||
Some((w, h))
|
||||
} else {
|
||||
None
|
||||
@@ -444,6 +474,59 @@ mod tests {
|
||||
assert_eq!(frames[0].pts_ns, 2_000_000_000);
|
||||
}
|
||||
|
||||
// --- advanced-profile resolution parsing (bit-offset regression) ---
|
||||
|
||||
/// Build an advanced-profile VC-1 sequence header encoding the given
|
||||
/// width/height. Layout from sh[4]: PROFILE(2)=3, LEVEL(3), COLORDIFF(2),
|
||||
/// FRMRTQ(3), BITRTQ(5), POSTPROCFLAG(1) = 16 bits, then
|
||||
/// MAX_CODED_WIDTH(12) = width/2 - 1, MAX_CODED_HEIGHT(12) = height/2 - 1.
|
||||
fn make_ap_seq_header(width: u32, height: u32) -> Vec<u8> {
|
||||
let coded_w = (width / 2) - 1;
|
||||
let coded_h = (height / 2) - 1;
|
||||
// Accumulate 40 bits MSB-first: 16 leading bits then 12+12.
|
||||
let mut acc: u64 = 0;
|
||||
let mut nbits = 0u32;
|
||||
let put = |val: u64, n: u32, acc: &mut u64, nbits: &mut u32| {
|
||||
*acc = (*acc << n) | (val & ((1u64 << n) - 1));
|
||||
*nbits += n;
|
||||
};
|
||||
// PROFILE = 3 (advanced), then 14 more leading bits (all zero here).
|
||||
put(0b11, 2, &mut acc, &mut nbits);
|
||||
put(0, 14, &mut acc, &mut nbits); // level+colordiff+frmrtq+bitrtq+postproc
|
||||
put(coded_w as u64, 12, &mut acc, &mut nbits);
|
||||
put(coded_h as u64, 12, &mut acc, &mut nbits);
|
||||
// 40 bits → 5 bytes, MSB-first.
|
||||
let mut payload = Vec::with_capacity(5);
|
||||
for i in (0..5).rev() {
|
||||
payload.push(((acc >> (i * 8)) & 0xFF) as u8);
|
||||
}
|
||||
let mut sh = vec![0x00, 0x00, 0x01, SC_SEQUENCE_HEADER];
|
||||
sh.extend_from_slice(&payload);
|
||||
sh
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advanced_profile_resolution_uses_16bit_offset() {
|
||||
// Regression: the parser skipped 11 bits (omitting BITRTQ_POSTPROC's 5
|
||||
// bits) instead of 16, reading width/height 5 bits too early. Encode a
|
||||
// non-default 1280x720 and confirm it round-trips, proving the 16-bit
|
||||
// pre-width offset.
|
||||
let mut parser = Vc1Parser::new();
|
||||
let mut data = make_ap_seq_header(1280, 720);
|
||||
// A frame so the parser emits and stores the header.
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01, SC_FRAME]);
|
||||
data.extend_from_slice(&[0x55, 0x66]);
|
||||
parser.parse(&make_pes(data, Some(0)));
|
||||
|
||||
let cp = parser.codec_private();
|
||||
// codec_private needs an entry point too; resolution is in width/height
|
||||
// fields regardless. Read them off the parser via codec_private when
|
||||
// available, else assert the internal fields directly.
|
||||
assert_eq!(parser.width, 1280, "width parsed at the 16-bit offset");
|
||||
assert_eq!(parser.height, 720, "height parsed at the 16-bit offset");
|
||||
let _ = cp;
|
||||
}
|
||||
|
||||
// --- find_next_sc utility ---
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user