Account for decrypt-time loss so partial AACS/CSS failures can't pass as a perfect rip
When a scrambled AACS unit fails to decrypt under every available key (a missing/wrong CPS sub-key, or a marginal unit that fails the TS-sync verify), decrypt_sectors restored the original encrypted bytes and returned Ok with no signal. Those still-encrypted bytes flowed to the TS assembler, which silently dropped the non-syncing packets with no loss counter. The only loss accounting was DiscStream's read-error zero-fill path, so mux reported lost_video_secs=0 for decrypt-dropped content and the abort gate accepted the rip even under abort_on_lost_secs=0. A rip missing real video/audio segments was published as a perfect success. decrypt_sectors now returns the number of bytes in scrambled units that no key could decrypt. DecryptingSectorSource accumulates that into a shared counter exposed via decrypt_loss(); both mux pipelines fold it into lost_bytes() — the inline DiscStream path directly, and the file-backed highway via PipelinedPesStream sharing the producer's counter. Restore-to-original is unchanged, so clear nav-files are never corrupted; metadata-probe callers that don't read the counter are unaffected. Adds regression tests at the decrypt and decorator layers.
This commit is contained in:
@@ -56,6 +56,14 @@ 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>>,
|
||||
}
|
||||
|
||||
impl PipelinedPesStream {
|
||||
@@ -84,9 +92,24 @@ impl PipelinedPesStream {
|
||||
pending_frames: std::collections::VecDeque::new(),
|
||||
eof: false,
|
||||
skip_parse: std::env::var_os("FREEMKV_SKIP_PARSE").is_some(),
|
||||
decrypt_loss: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -263,6 +286,18 @@ impl Stream for PipelinedPesStream {
|
||||
.find(|(p, _)| *p == pid)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user