mp4 demuxer (untrusted input): bound every allocation sized from a box field (stsz/stco/stsc counts, stts/ctts run-lengths, per-sample and moov sizes, plus an absolute cap so a sparse file can't inflate file_len); guard the parse_stsd slice and a zero mdhd timescale; cap track count so the per-track PID can't overflow; rewrite read_moov to handle size==0 / size<8 / 64-bit largesize; parse esds/AudioSpecificConfig for AAC; write tkhd duration in the movie timescale. decrypt: resolve_mux_key_map now fails loud on an extent no key can classify instead of inheriting the previous extent's key, so a keymap never silently carries a wrong key; the sweep/patch key-fetch recovery fails loud when a unit is still unresolved after the retry. AACS: reject inverted forensic segments in both range builders; compare the forensic index in u16 space so an out-of-range value can't truncate onto a valid u8 index. RECOVERED_ERROR no longer latches the damage zone, preserving the 30s wedge cooldown for a following hard error. audio: AAC/MP2/MP3/FLAC carry the last PTS across a PES with no timestamp; the DTS-HD extension-sync search is bounded to after the core; the MP4 16.16 sample-rate field saturates. demux_sink records the video reference before the kind filter so audio:// / sub:// keep multi-clip PTS continuity and the DELAY tag. Remove a dead error variant and the AACS-unsupported-video code; codec comments cite the primary format specs; assorted doc/naming fixes and regression tests throughout.
124 lines
4.8 KiB
Rust
124 lines
4.8 KiB
Rust
//! B1 drop-to-IRAP gate — keep a muxed elementary stream decode-clean across a
|
|
//! mid-stream gap (e.g. an undecryptable unit the mux concealed as NULL TS,
|
|
//! P3/A2).
|
|
//!
|
|
//! When packets are lost, the affected access unit is already dropped at the TS
|
|
//! layer (the assembler drops the partial PES on the continuity gap). But for
|
|
//! INTER-CODED video the frames that follow reference the lost frame (and each
|
|
//! other) until the next IRAP/IDR keyframe — emitting them makes any decoder
|
|
//! fault on the "missing reference / non-existing PPS" condition and visibly
|
|
//! break decode. So after a gap on a video track we DROP FORWARD to the keyframe
|
|
//! and resume cleanly there. The gap rounds up to (at most) one GOP — the price
|
|
//! of never emitting a dangling reference; it is logged.
|
|
//!
|
|
//! Audio and subtitle frames are independent (no inter-frame references), so a
|
|
//! gap there costs only the single already-dropped frame; the gate is a no-op
|
|
//! for non-video tracks (it always admits).
|
|
|
|
/// Per-track keyframe-resync state. One gate per elementary stream; a video
|
|
/// track's gate stays "armed" from a discontinuity until the next keyframe.
|
|
#[derive(Debug, Default)]
|
|
pub(crate) struct ResyncGate {
|
|
/// True while dropping post-gap inter-coded frames until the next keyframe.
|
|
armed: bool,
|
|
/// Count of frames dropped while armed (for a single summary log on resync).
|
|
dropped: u64,
|
|
}
|
|
|
|
impl ResyncGate {
|
|
pub(crate) fn new() -> Self {
|
|
Self {
|
|
armed: false,
|
|
dropped: 0,
|
|
}
|
|
}
|
|
|
|
/// Decide whether a parsed frame should be EMITTED (`true`) or DROPPED
|
|
/// (`false`).
|
|
///
|
|
/// * `is_video` — inter-coded video track (the only kind with cross-frame
|
|
/// references); `false` for audio/subtitle, which always admit.
|
|
/// * `discontinuity` — this frame's source PES followed a TS continuity gap.
|
|
/// * `keyframe` — this frame is a self-contained IRAP/IDR.
|
|
///
|
|
/// A non-video track always admits. A video track arms on a discontinuity
|
|
/// and then drops every non-keyframe until (and excluding the drop of) the
|
|
/// next keyframe, which disarms and is emitted.
|
|
pub(crate) fn admit(&mut self, is_video: bool, discontinuity: bool, keyframe: bool) -> bool {
|
|
if !is_video {
|
|
return true;
|
|
}
|
|
if discontinuity {
|
|
self.armed = true;
|
|
}
|
|
if self.armed {
|
|
if keyframe {
|
|
self.armed = false;
|
|
self.dropped = 0;
|
|
true
|
|
} else {
|
|
self.dropped += 1;
|
|
false
|
|
}
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
|
|
/// Frames dropped so far in the CURRENT armed run (0 when not armed / just
|
|
/// resynced). Lets the consumer log the resync cost once at the keyframe.
|
|
pub(crate) fn dropped_in_run(&self) -> u64 {
|
|
self.dropped
|
|
}
|
|
|
|
/// Whether the gate is currently dropping frames (armed, awaiting keyframe).
|
|
pub(crate) fn is_armed(&self) -> bool {
|
|
self.armed
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn non_video_always_admits_even_on_discontinuity() {
|
|
let mut g = ResyncGate::new();
|
|
// Audio/subtitle: a gap drops only the (already TS-dropped) frame; every
|
|
// frame the parser still emits is independent and must pass.
|
|
assert!(g.admit(false, true, false));
|
|
assert!(g.admit(false, true, false));
|
|
assert!(!g.is_armed(), "non-video never arms");
|
|
}
|
|
|
|
#[test]
|
|
fn video_drops_inter_frames_until_next_keyframe() {
|
|
let mut g = ResyncGate::new();
|
|
// Clean run: everything admits.
|
|
assert!(g.admit(true, false, true)); // IDR
|
|
assert!(g.admit(true, false, false)); // P
|
|
// Gap arrives on the next frame (a P referencing lost data) → drop it
|
|
// and every inter frame until the next keyframe.
|
|
assert!(!g.admit(true, true, false), "post-gap P dropped");
|
|
assert!(!g.admit(true, false, false), "still dropping (no key yet)");
|
|
assert!(!g.admit(true, false, false));
|
|
assert_eq!(g.dropped_in_run(), 3);
|
|
// Next keyframe resyncs and is emitted.
|
|
assert!(g.admit(true, false, true), "keyframe resumes the stream");
|
|
assert!(!g.is_armed());
|
|
assert_eq!(g.dropped_in_run(), 0);
|
|
// Back to a clean run.
|
|
assert!(g.admit(true, false, false), "post-resync P admits");
|
|
}
|
|
|
|
#[test]
|
|
fn discontinuity_landing_on_a_keyframe_emits_immediately() {
|
|
let mut g = ResyncGate::new();
|
|
// If the first surviving frame after the gap is itself an IRAP, there is
|
|
// nothing to drop — it is self-contained.
|
|
assert!(g.admit(true, true, true), "gap+keyframe emits, no drop");
|
|
assert!(!g.is_armed());
|
|
assert_eq!(g.dropped_in_run(), 0);
|
|
}
|
|
}
|