Mux decrypt/verify redesign, HD DVD first-class, MVC 3D
decrypt: - decrypt_sectors is now a pure decrypt (apply key, leave plaintext, report unverified bytes); TS-structure is a separate primitive (is_clean_ts/ps) used only for key selection and read-verify. The mux passes decrypted bytes through (the demuxer drops non-conforming packets), ending the NULL-TS conceal loop and the per-unit key-server refetch storm. Key-proof floor replaces the 75% supermajority. recovery: - Removed the post-read decrypt-verify gate (verify.rs) that mis-aligned the disc-absolute unit grid against clip-anchored AACS units and false-failed good clips (e.g. Dunkirk's orphan-CPS clip). Bad sectors are marked by physical read result; decryptability is proven at scan + mux time. HD DVD (first-class AACS): - Role-based candidate-list file sourcing so an HD DVD's /ANY!/ files (MKBROM.AACS, VTKF000.AACS, CONTENT_CERT.AACS) are found with no disc-type branch. parse_vtkf parses VTKF000.AACS into the same UnitKeyFile as a BD Unit_Key_RO.inf, so the shared VUK unwrap applies unchanged. set_unit_base clip-anchoring. Two decrypt-axis assumptions remain UNVERIFIED-HDDVD-DECRYPT (no encrypted disc to test). mux: - MVC (Blu-ray 3D) track signals unified into one MVCDecoderConfigurationRecord; release-safe track_vint (3-byte VINT) and pid_index (i32) guards. hardening: - Container-aware is_clean / encryption detection; bytes_bad_in_title fail-safe on a corrupt mapfile; CSS crack gated on DiscFormat::Dvd (HD DVD excluded); non-vacuous CSS tests; patch NOT_READY/HARDWARE/ILLEGAL_REQUEST/ABORTED sense-path tests.
This commit is contained in:
+17
-5
@@ -241,11 +241,23 @@ impl EsWriter for AnnexBWriter {
|
||||
/// Delegates to the canonical hvcC/avcC → Annex-B converters in
|
||||
/// [`crate::mux::hevc`] — the single source of truth across all muxers.
|
||||
fn annexb_param_sets(codec: Codec, record: &[u8]) -> Vec<u8> {
|
||||
match codec {
|
||||
Codec::Hevc => hvcc_to_annex_b(record).unwrap_or_default(),
|
||||
Codec::H264 => avcc_to_annex_b(record).unwrap_or_default(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
let converted = match codec {
|
||||
Codec::Hevc => hvcc_to_annex_b(record),
|
||||
Codec::H264 => avcc_to_annex_b(record),
|
||||
_ => return Vec::new(),
|
||||
};
|
||||
converted.unwrap_or_else(|| {
|
||||
// A malformed hvcC/avcC record yields no parameter sets. Returning empty
|
||||
// means keyframes ship WITHOUT in-band SPS/PPS — playable from the first
|
||||
// keyframe but broken for seek-to-arbitrary-point and hardware decoders.
|
||||
// Surface it rather than silently degrading the output.
|
||||
tracing::warn!(
|
||||
target: "mux",
|
||||
?codec,
|
||||
"codec-private (hvcC/avcC) parse failed; keyframes will lack in-band SPS/PPS"
|
||||
);
|
||||
Vec::new()
|
||||
})
|
||||
}
|
||||
|
||||
/// PGS `.sup` writer: rebuilds the HDMV segment framing the parser stripped.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! With [`crate::sector::PrefetchedSectorSource`] alone, read+decrypt
|
||||
//! already runs on a producer thread; the *consumer* (main) thread
|
||||
//! still serialises `ts_demuxer.feed` (M2TS parsing) with the codec
|
||||
//! parsers. Profiling on the rip1 testbed showed feed at ~37 % and
|
||||
//! parsers. Profiling showed feed at ~37 % and
|
||||
//! codec parse at ~44 % of consumer wall time — i.e. feed is heavy
|
||||
//! enough that pipelining it with parse pays for itself.
|
||||
//!
|
||||
|
||||
+7
-18
@@ -108,11 +108,6 @@ pub struct DiscStream {
|
||||
/// inline `decrypt::decrypt_sectors` step. `DecryptKeys::None`
|
||||
/// (raw / unencrypted disc) makes the decorator a pass-through.
|
||||
reader: DecryptingSectorSource<Box<dyn SectorSource>>,
|
||||
/// Shared decrypt-loss counter, cloned once at construction from
|
||||
/// `reader.decrypt_loss()`. `lost_bytes()` loads it directly so the
|
||||
/// per-frame hot path performs no per-call `Arc::clone` (matching the
|
||||
/// `PipelinedPesStream` pattern).
|
||||
decrypt_loss: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||
title: DiscTitle,
|
||||
/// Mirror of the keys handed in at construction. The decorator
|
||||
/// owns the cryptographic state; this field is kept for
|
||||
@@ -242,8 +237,7 @@ impl DiscStream {
|
||||
// concern, never conceal / re-fetch / count as loss (fail loud only on a
|
||||
// genuine can't-decrypt). DiscStream is a decode/mux stream (live-drive
|
||||
// single-pass / direct), never the ciphertext-preserving sweep.
|
||||
let mut reader =
|
||||
DecryptingSectorSource::new(reader, decrypt_keys.clone()).tolerate_decrypt_loss();
|
||||
let mut reader = DecryptingSectorSource::new(reader, decrypt_keys.clone());
|
||||
|
||||
// Wrong-substream fix (Silence-of-the-Lambs): re-route the title's
|
||||
// declared AC-3 audio onto the physically-correct `0x8x` sub-streams by
|
||||
@@ -294,9 +288,6 @@ impl DiscStream {
|
||||
// the decorator is a pass-through). Reset the unit base the probe read
|
||||
// advanced so the first fill_extents read starts cleanly.
|
||||
reader.set_unit_base(0);
|
||||
// Clone the shared loss counter once here so `lost_bytes()` never
|
||||
// clones an Arc per frame on the mux hot path.
|
||||
let decrypt_loss = reader.decrypt_loss();
|
||||
|
||||
// B1 resync gates: one per stream, video flagged so the gate only
|
||||
// drop-to-keyframes video (audio/subtitle always admit). Computed before
|
||||
@@ -312,7 +303,6 @@ impl DiscStream {
|
||||
|
||||
Self {
|
||||
reader,
|
||||
decrypt_loss,
|
||||
title,
|
||||
decrypt_keys,
|
||||
unit_align,
|
||||
@@ -1004,14 +994,12 @@ impl crate::pes::Stream for DiscStream {
|
||||
}
|
||||
|
||||
fn lost_bytes(&self) -> u64 {
|
||||
// Read-error zero-fill loss (counted in fill_extents) PLUS decrypt-time
|
||||
// loss — bytes of scrambled AACS units the decorator could not decrypt
|
||||
// and passed through still encrypted (the TS assembler silently drops
|
||||
// them). Both are real missing content the abort gate must see; without
|
||||
// the decrypt term a partial key failure reports lost_bytes=0 and a rip
|
||||
// missing segments passes even under abort_on_lost_secs=0.
|
||||
// Read-error zero-fill loss (counted in fill_extents) — real missing
|
||||
// content the abort gate must see. There is no decrypt-loss term: the
|
||||
// decrypt path passes bad-encoded/undecryptable units through (a broken-TS
|
||||
// unit is the muxer's concern, and a missing key is indistinguishable from
|
||||
// bad authoring here), so only physical read loss is reported.
|
||||
self.lost_bytes
|
||||
.saturating_add(self.decrypt_loss.load(std::sync::atomic::Ordering::Relaxed))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1490,6 +1478,7 @@ mod tests {
|
||||
let keys = crate::decrypt::DecryptKeys::Aacs {
|
||||
unit_keys: vec![(0, [0u8; 16])],
|
||||
read_data_key: None,
|
||||
format: crate::disc::ContentFormat::BdTs,
|
||||
};
|
||||
let mut stream = DiscStream::new(Box::new(reader), title, keys, 8, ContentFormat::BdTs);
|
||||
stream.skip_errors = true;
|
||||
|
||||
+31
-13
@@ -785,23 +785,35 @@ fn block_ts(is_video: bool, prev: Option<i64>, pts_ticks: i64) -> i64 {
|
||||
|
||||
/// Encode a Matroska track number as an EBML VINT into a stack buffer,
|
||||
/// returning the buffer and the used length. Track numbers are small (1-based,
|
||||
/// a handful of tracks), so 1 byte covers `< 0x80` and 2 bytes covers the rest;
|
||||
/// no heap allocation, called once per block on the mux hot path.
|
||||
/// a handful of tracks), so 1 byte covers `< 0x80`, 2 bytes covers `< 0x4000`,
|
||||
/// and 3 bytes covers `< 0x20_0000`; no heap allocation, called once per block
|
||||
/// on the mux hot path.
|
||||
///
|
||||
/// The 2-byte form holds 14 payload bits (max 0x3FFF). The `debug_assert`
|
||||
/// guards the 0x4000 bound: at or above it, `(track_num >> 8)` is >= 0x40 and
|
||||
/// OR-ing the 0x40 length marker would clobber it, corrupting the track
|
||||
/// number. Not reachable today (track numbers are `i+1` over a few streams),
|
||||
/// so this documents the bound rather than handling 3-byte VINTs.
|
||||
fn track_vint(track_num: usize) -> ([u8; 2], usize) {
|
||||
/// Each width uses a marker bit that must NOT collide with the payload's top
|
||||
/// byte: the 1-byte marker is 0x80 (7 payload bits), the 2-byte marker 0x40
|
||||
/// (14 payload bits), the 3-byte marker 0x20 (21 payload bits). Handling all
|
||||
/// three in RELEASE (not just `debug_assert`) means an out-of-2-byte-range
|
||||
/// track number can never silently clobber the marker bit and corrupt the
|
||||
/// block. Real discs never approach even the 2-byte range; the 21-bit ceiling
|
||||
/// is an absurd upper bound kept as a `debug_assert`.
|
||||
fn track_vint(track_num: usize) -> ([u8; 3], usize) {
|
||||
if track_num < 0x80 {
|
||||
([(track_num as u8) | 0x80, 0], 1)
|
||||
([(track_num as u8) | 0x80, 0, 0], 1)
|
||||
} else if track_num < 0x4000 {
|
||||
([0x40 | ((track_num >> 8) as u8), track_num as u8, 0], 2)
|
||||
} else {
|
||||
debug_assert!(
|
||||
track_num < 0x4000,
|
||||
"track number {track_num} exceeds the 14-bit 2-byte EBML VINT range"
|
||||
track_num < 0x20_0000,
|
||||
"track number {track_num} exceeds the 21-bit 3-byte EBML VINT range"
|
||||
);
|
||||
([0x40 | ((track_num >> 8) as u8), track_num as u8], 2)
|
||||
(
|
||||
[
|
||||
0x20 | ((track_num >> 16) as u8),
|
||||
(track_num >> 8) as u8,
|
||||
track_num as u8,
|
||||
],
|
||||
3,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3507,7 +3519,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn track_vint_encodes_one_and_two_byte_forms() {
|
||||
fn track_vint_encodes_one_two_and_three_byte_forms() {
|
||||
// 1-byte form for track numbers < 0x80, high bit set.
|
||||
let (b, n) = track_vint(1);
|
||||
assert_eq!(&b[..n], &[0x81]);
|
||||
@@ -3518,6 +3530,12 @@ mod tests {
|
||||
assert_eq!(&b[..n], &[0x40, 0x80]);
|
||||
let (b, n) = track_vint(0x3FFF);
|
||||
assert_eq!(&b[..n], &[0x7F, 0xFF]);
|
||||
// 3-byte form at/above 0x4000, 0x20 length marker in the top byte —
|
||||
// handled in RELEASE (no silent marker-bit clobber), not just debug.
|
||||
let (b, n) = track_vint(0x4000);
|
||||
assert_eq!(&b[..n], &[0x20, 0x40, 0x00]);
|
||||
let (b, n) = track_vint(0x1F_FFFF);
|
||||
assert_eq!(&b[..n], &[0x3F, 0xFF, 0xFF]);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -1016,7 +1016,11 @@ fn parse_track(
|
||||
arem = arem.saturating_sub(ahlen as u64 + as_);
|
||||
match aid {
|
||||
ebml::SAMPLING_FREQUENCY => sr = ebml::read_float_val(r, as_ as usize)?,
|
||||
ebml::CHANNELS => ch = read_uint_bounded(r, as_)? as u8,
|
||||
// Clamp instead of `as u8`: a foreign/corrupt MKV with a
|
||||
// CHANNELS value that is a multiple of 256 would truncate to
|
||||
// 0 (an invalid channel count) on a bare cast. Saturate to
|
||||
// u8::MAX so an absurd count degrades to "many", never to 0.
|
||||
ebml::CHANNELS => ch = read_uint_bounded(r, as_)?.min(u8::MAX as u64) as u8,
|
||||
_ => {
|
||||
skip_bytes(r, as_)?;
|
||||
}
|
||||
|
||||
@@ -56,14 +56,6 @@ pub struct PipelinedPesStream {
|
||||
/// `std::env::var_os` takes a process-wide lock, so the per-batch /
|
||||
/// per-poll reads it replaces were needless hot-path overhead.
|
||||
skip_parse: bool,
|
||||
/// Cumulative bytes of scrambled AACS units the producer's decrypt step
|
||||
/// could not decrypt — silent decrypt loss the demux drops without a sync.
|
||||
/// Shared with the producer thread's [`DecryptingSectorSource`]
|
||||
/// (`crate::sector::DecryptingSectorSource::decrypt_loss`). Surfaced through
|
||||
/// [`Stream::lost_bytes`] so the file-backed mux abort gate sees a partial
|
||||
/// decrypt failure instead of reporting a perfect rip. `None` for pipelines
|
||||
/// with no AACS decrypt step (e.g. the M2TS byte-stream path).
|
||||
decrypt_loss: Option<std::sync::Arc<std::sync::atomic::AtomicU64>>,
|
||||
/// Count of dropped DVD navigation packets (private_stream_2, 0xBF). These
|
||||
/// are expected on every disc; instead of a per-packet WARN they're tallied
|
||||
/// and summarised once at EOF.
|
||||
@@ -134,7 +126,6 @@ impl PipelinedPesStream {
|
||||
pending_frames: std::collections::VecDeque::new(),
|
||||
eof: false,
|
||||
skip_parse: std::env::var_os("FREEMKV_SKIP_PARSE").is_some(),
|
||||
decrypt_loss: None,
|
||||
dropped_nav_packets: 0,
|
||||
resync,
|
||||
is_video,
|
||||
@@ -142,20 +133,6 @@ impl PipelinedPesStream {
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach the producer's decrypt-loss counter so [`Stream::lost_bytes`]
|
||||
/// reports bytes of scrambled AACS units that could not be decrypted (and
|
||||
/// were therefore silently dropped downstream). Obtained from the
|
||||
/// producer's `DecryptingSectorSource::decrypt_loss()` before it is moved
|
||||
/// into the prefetch thread. The M2TS / no-decrypt pipelines leave this
|
||||
/// unset.
|
||||
pub(crate) fn with_decrypt_loss(
|
||||
mut self,
|
||||
loss: std::sync::Arc<std::sync::atomic::AtomicU64>,
|
||||
) -> Self {
|
||||
self.decrypt_loss = Some(loss);
|
||||
self
|
||||
}
|
||||
|
||||
/// Pull one batch of `PesPacket`s from the demux thread, run
|
||||
/// codec parse on each, enqueue resulting `PesFrame`s on
|
||||
/// `pending_frames`. Returns Ok(true) on success, Ok(false) on
|
||||
@@ -464,17 +441,9 @@ impl Stream for PipelinedPesStream {
|
||||
.and_then(|(_, parser)| parser.codec_private())
|
||||
}
|
||||
|
||||
fn lost_bytes(&self) -> u64 {
|
||||
// The file-backed highway has no read-error zero-fill term (resolve
|
||||
// tracks read loss separately), but the producer's decrypt step can
|
||||
// pass scrambled units through undecrypted — silent loss the demux
|
||||
// drops. Surface that so the mux abort gate sees a partial AACS/CSS
|
||||
// decrypt failure rather than reporting a perfect rip.
|
||||
self.decrypt_loss
|
||||
.as_ref()
|
||||
.map(|c| c.load(std::sync::atomic::Ordering::Relaxed))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
// `lost_bytes` uses the trait default (0): the file-backed highway has no
|
||||
// read-error zero-fill term (resolve/mapfile tracks physical read loss
|
||||
// separately) and the decrypt path no longer reports a decrypt-loss term.
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+32
-22
@@ -391,11 +391,11 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
|
||||
let title = disc.titles[idx].clone();
|
||||
let format = disc.content_format;
|
||||
// ISO file: 8192-sector batch (16 MiB at 2048 B/sector) —
|
||||
// sequential read from fast storage, no bad sectors. Measured
|
||||
// optimum on the rip1 testbed; bumping to 16384 sectors (32 MiB)
|
||||
// regressed (more cache pressure, longer per-batch latency starves
|
||||
// the consumer between iterations). Physical drives keep smaller
|
||||
// batches for adaptive error handling.
|
||||
// sequential read from fast storage, no bad sectors. Empirically
|
||||
// optimal; bumping to 16384 sectors (32 MiB) regressed (more cache
|
||||
// pressure, longer per-batch latency starves the consumer between
|
||||
// iterations). Physical drives keep smaller batches for adaptive
|
||||
// error handling.
|
||||
const ISO_MUX_BATCH_SECTORS: u16 = 8192;
|
||||
|
||||
// Pass `DecryptKeys::None` to the decrypt decorator when
|
||||
@@ -621,7 +621,8 @@ fn build_demux_state(title: &DiscTitle, format: ContentFormat) -> DemuxState {
|
||||
/// - `fetch`: optional fresh-key-on-failure callback (see
|
||||
/// [`crate::sector::KeyFetch`]). When a unit no held key decrypts, the
|
||||
/// decrypt decorator hands that ciphertext to `fetch` and adds any key it
|
||||
/// returns. `None` keeps the prior behaviour (the unit is counted as loss).
|
||||
/// returns, then re-decrypts. `None` means no mid-stream key recovery — the
|
||||
/// unit's best-effort bytes pass through to the muxer as-is.
|
||||
// Eight reader/title/keys/tuning/callback params is inherent to the mux entry
|
||||
// point; grouping them into a struct would only move the same fields around.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -647,19 +648,25 @@ pub fn build_iso_pipeline<S: SectorSource + Send + 'static>(
|
||||
};
|
||||
// MUX path: read > decrypt > mux. The decrypt seam applies the CPS unit key and
|
||||
// passes the bytes to the muxer; a unit that decrypts to broken TS is the
|
||||
// muxer's problem, not a decrypt failure, so the mux never conceals, re-fetches
|
||||
// a key, or counts it as loss — it fails only when it genuinely can't decrypt
|
||||
// (no key / misaligned unit). The `fetch` key-recovery seam is a rip/verify
|
||||
// concern (Disc::sweep / Disc::patch), deliberately NOT installed on the mux:
|
||||
// key recovery happens up front, and the mux never re-asks mid-stream.
|
||||
// muxer's problem, not a decrypt failure, so the mux never conceals a unit or
|
||||
// counts it as loss.
|
||||
let mut decrypting =
|
||||
crate::sector::DecryptingSectorSource::new(Box::new(reader) as Box<dyn SectorSource>, keys)
|
||||
.tolerate_decrypt_loss();
|
||||
let _ = &fetch; // rip/verify key-recovery seam; the mux does not consume it
|
||||
// Loss counter: the mux does not tally broken-TS units (the muxer handles them),
|
||||
// so for a keyed disc this stays 0; it still surfaces via `lost_bytes()` for the
|
||||
// abort gate, which now reflects only a genuine can't-decrypt.
|
||||
let decrypt_loss = decrypting.decrypt_loss();
|
||||
crate::sector::DecryptingSectorSource::new(Box::new(reader) as Box<dyn SectorSource>, keys);
|
||||
// Install the fresh-key-on-failure callback (if the app supplied one). This is
|
||||
// how multi-CPS is muxed: each CPS unit's key is fetched when the mux reaches a
|
||||
// unit no held key opens — "get the key when we need it." It fires only on a
|
||||
// genuine miss: now that key selection is accurate (`is_clean_ts`), a unit that
|
||||
// decrypted correctly but has bad-encoded TS is NOT a miss, so this no longer
|
||||
// storms the key source the way the old TS supermajority gate did.
|
||||
if let Some(cb) = fetch {
|
||||
decrypting = decrypting.with_key_fetch(cb);
|
||||
}
|
||||
// Loss-counter handle. The mux does NOT tally decrypt-quality misses: a
|
||||
// broken-TS unit is the muxer's concern, and a missing key is an up-front
|
||||
// resolve failure — indistinguishable from bad authoring at this seam, so
|
||||
// counting it would false-abort a bad-encoded-but-decryptable disc. A genuine
|
||||
// can't-decrypt surfaces as `Err`; `lost_bytes()` reflects physical read loss
|
||||
// only (there is no decrypt-loss term to fold in).
|
||||
|
||||
// Wrong-substream fix (Silence-of-the-Lambs): before the prefetcher takes
|
||||
// the reader, probe the feature head through the (plaintext) decrypting
|
||||
@@ -685,10 +692,13 @@ pub fn build_iso_pipeline<S: SectorSource + Send + 'static>(
|
||||
let (demux_thread, demux_rx) =
|
||||
super::demux_thread::DemuxThread::spawn_zero_copy(rx, recycle_tx, shell, halt, ts, ps)
|
||||
.map_err(|e| -> io::Error { e.into() })?;
|
||||
Ok(
|
||||
PipelinedPesStream::new(demux_thread, demux_rx, title, parsers, pid_to_track)
|
||||
.with_decrypt_loss(decrypt_loss),
|
||||
)
|
||||
Ok(PipelinedPesStream::new(
|
||||
demux_thread,
|
||||
demux_rx,
|
||||
title,
|
||||
parsers,
|
||||
pid_to_track,
|
||||
))
|
||||
}
|
||||
|
||||
/// Assemble the M2TS file mux pipeline (read → demux → parse) for a
|
||||
|
||||
+25
-21
@@ -12,9 +12,11 @@ use crate::consts::TS_PACKET_BYTES;
|
||||
|
||||
/// TS sync byte.
|
||||
const SYNC_BYTE: u8 = 0x47;
|
||||
/// MPEG-TS null-packet PID (0x1FFF). Carries no elementary stream; the P3
|
||||
/// concealment fill emits null packets on this PID, tagged with an
|
||||
/// adaptation-field discontinuity_indicator to signal a concealed gap.
|
||||
/// MPEG-TS null-packet PID (0x1FFF). Carries no elementary stream. The demuxer
|
||||
/// still recognises a `0x1FFF` packet with an adaptation-field
|
||||
/// discontinuity_indicator as a concealed-gap loss signal, but the in-tree WRITER
|
||||
/// that emitted these (the removed NULL-TS concealment fill) is gone — the mux no
|
||||
/// longer conceals; only externally-authored markers reach this path now.
|
||||
const NULL_PID: u16 = 0x1FFF;
|
||||
|
||||
/// A reassembled PES packet with timestamp info.
|
||||
@@ -34,8 +36,10 @@ pub struct PesPacket {
|
||||
pub source: Option<crate::pes::SourcePos>,
|
||||
/// True when one or more packets for this stream were lost before this PES —
|
||||
/// a continuity break (CC gap or adaptation-field discontinuity_indicator) on
|
||||
/// a tracked PID, or the CC-independent concealment marker the mux emits when
|
||||
/// it replaces an undecryptable unit with NULL-TS packets (P3/A2). This PES is
|
||||
/// a tracked PID, or a CC-independent NULL-TS concealment marker (P3/B1). NOTE:
|
||||
/// the mux no longer emits such markers (the concealment writer was removed);
|
||||
/// this now flags only real discontinuities and externally-authored markers.
|
||||
/// This PES is
|
||||
/// the FIRST whose data is entirely after the gap: a mid-frame loss drops the
|
||||
/// truncated partial and flags the next complete PES; a loss landing on a PES
|
||||
/// boundary flags the PES STARTING after it (never the one just flushed). So
|
||||
@@ -200,7 +204,7 @@ impl PesAssembler {
|
||||
/// BD Transport Stream demuxer.
|
||||
pub struct TsDemuxer {
|
||||
assemblers: Vec<PesAssembler>,
|
||||
pid_index: Vec<i16>, // PID → index into assemblers, -1 = not tracked
|
||||
pid_index: Vec<i32>, // PID → index into assemblers, -1 = not tracked
|
||||
remainder: Vec<u8>, // leftover bytes from previous feed() call
|
||||
/// Absolute source byte offset of the NEXT byte to be fed — the running
|
||||
/// base that turns an in-buffer packet offset into a source position.
|
||||
@@ -224,20 +228,17 @@ impl TsDemuxer {
|
||||
/// limits. Empty `pids` yields max_pid 0; the floor still produces a
|
||||
/// valid (wholly-unused) table.
|
||||
pub fn new(pids: &[u16]) -> Self {
|
||||
// The PID→assembler index is stored as i16 (-1 = untracked), so a
|
||||
// 32768th+ tracked PID would truncate to a negative value and be
|
||||
// silently treated as untracked. Callers pass a handful of PIDs
|
||||
// (BD-TS has at most ~8192), so this is a programmer-error guard.
|
||||
debug_assert!(
|
||||
pids.len() <= i16::MAX as usize,
|
||||
"TsDemuxer: too many PIDs for an i16 index table"
|
||||
);
|
||||
// The PID→assembler index is stored as i32 (-1 = untracked). PIDs are
|
||||
// u16 (≤ 65535) and the assembler index `i` is bounded by the number of
|
||||
// distinct PIDs (≤ 65536), both far below i32::MAX, so `i as i32` can
|
||||
// never truncate to a negative value and be mis-read as untracked —
|
||||
// unlike an i16 table, this is safe in RELEASE, not just under debug.
|
||||
let max_pid = pids.iter().copied().max().unwrap_or(0) as usize;
|
||||
let table_size = (max_pid + 1).max(8192);
|
||||
let mut pid_index = vec![-1i16; table_size];
|
||||
let mut pid_index = vec![-1i32; table_size];
|
||||
let mut assemblers = Vec::with_capacity(pids.len());
|
||||
for (i, &pid) in pids.iter().enumerate() {
|
||||
pid_index[pid as usize] = i as i16;
|
||||
pid_index[pid as usize] = i as i32;
|
||||
assemblers.push(PesAssembler::new(pid));
|
||||
}
|
||||
Self {
|
||||
@@ -368,10 +369,13 @@ impl TsDemuxer {
|
||||
let pusi = ts[1] & 0x40 != 0; // Payload Unit Start Indicator
|
||||
let adaptation = (ts[3] >> 4) & 0x03;
|
||||
|
||||
// P3/B1 CONCEALMENT MARKER. The decrypt layer fills an undecryptable
|
||||
// aligned unit with NULL-TS packets (PID 0x1FFF) that carry an
|
||||
// adaptation-field discontinuity_indicator (see `aacs::content::fill_null_ts_unit`).
|
||||
// This is the authoritative loss signal — unlike a tracked PID's 4-bit
|
||||
// P3/B1 CONCEALMENT MARKER: a NULL-TS packet (PID 0x1FFF) carrying an
|
||||
// adaptation-field discontinuity_indicator. NOTE: the in-tree writer that
|
||||
// laid these down on an undecryptable unit was removed with the pure-decrypt
|
||||
// passthrough change (the mux no longer conceals), so this recognition now
|
||||
// only fires on externally-authored markers — a candidate for removal with
|
||||
// the rest of the retired concealment path.
|
||||
// As a loss signal it is CC-INDEPENDENT — unlike a tracked PID's 4-bit
|
||||
// continuity_counter it is CC-INDEPENDENT, so it survives a loss that is
|
||||
// an exact multiple of 16 packets and a loss at the very start of a PID
|
||||
// (no prior CC to diff against). The decrypt layer cannot know which
|
||||
@@ -1057,7 +1061,7 @@ mod tests {
|
||||
|
||||
/// One 192-byte BD source packet that is a B1 concealment marker: a PID-0x1FFF
|
||||
/// null packet carrying the adaptation-field discontinuity_indicator (the byte
|
||||
/// shape `fill_null_ts_unit` writes for every packet of a concealed unit).
|
||||
/// shape of a concealed-unit packet).
|
||||
fn null_marker_packet() -> Vec<u8> {
|
||||
let mut pkt = vec![0u8; BD_SOURCE_PACKET_BYTES];
|
||||
pkt[4] = SYNC_BYTE; // 0x47
|
||||
|
||||
Reference in New Issue
Block a user