fix(libfreemkv): rc6 hardening pass — mux timeline/colour/PCR, demux panic sentinel, parser robustness + doc accuracy
Surgical fixes (each with a regression test that fails without the change): mux/mkv.rs, mux/demux_sink.rs: drive the clip-boundary timeline epoch off the resolved PRIMARY VIDEO track, not the literal stream index 0. An M2TS/PMT title can list an audio ES before video, so streams[0] may be audio; a non-video epoch driver ratchets the frontier and inflates the timeline. mkv cluster-opening falls back to track 0 for audio-only titles so they still open clusters. mux/codec/ac3.rs: correct ACMOD_CHANNELS — acmod=5 (3/1) is 4 channels, not 3 (was undercounting a 3/1 stream); fix the A/52 Table 5.8 doc. disc/mod.rs: HDMV coding_type 0x91 (Interactive Graphics / menus) no longer maps to PGS subtitle — it falls through to Unknown so the PMT/STN walker drops it instead of surfacing a bogus subtitle track. mux/videomap.rs + mux/mkv.rs: FVI colour now mirrors the MKV muxer's CICP precedence (measured CICP authoritative; HDR-driven PQ/HLG transfer override) via a shared cicp_for_video helper, so the two sinks can't disagree (HDR10 BT.2020 no longer emits SDR transfer 14). mux/mkvstream.rs: saturating_add on cluster_ts + rel_ts so an adversarial CLUSTER_TIMESTAMP near i64::MAX can't overflow/panic before the existing saturating_mul. mux/timeline.rs: tighten the tail-straggler clamp so a normal new-epoch non-video frame leading the sparse video frontier by >3s is not demoted into the previous clip's epoch. mux/m2ts_mux/mod.rs: re-stamp PCR per video TS packet (mid-PES), not only at PES boundaries, so a large UHD I-frame can't open a multi-second PCR gap; modular 33-bit PTS rebasing so a real 90 kHz clock wrap is not collapsed to PTS 0 (pre-base frames still floor to 0). io/byte_prefetcher.rs, sector/prefetched.rs: wrap the producer feed loop in catch_unwind and emit a typed error sentinel on panic, so a mid-stream producer panic is not read as a clean EOF at the demux boundary (which would silently truncate the mux). mux/codec/h264.rs: extend HIGH_PROFILES to the full ISO/IEC 14496-15 set that mandates the avcC chroma/bit-depth extension (adds 244 et al.). Doc/comment accuracy: css/mod.rs (50000 sectors, not scrambled-sectors), aacs/decrypt.rs (decrypt_unit already-clear path), ifo.rs (TT_SRPT at 0xC4), css/lfsr.rs (LFSR0 24-bit; TAB1-then-XOR cipher; real scramble-flag predicate), disc/read_error.rs (for_sweep does bounded transient retries). Skipped: keydb.rs SSRF guard (low/latent, no live caller) — a hard loopback block breaks an existing behavioral test that exercises the header-EOF path over a loopback server; a clean fix needs a resolver test seam beyond this surgical pass. The sibling keydb_fetch.rs comment fix is out of scope (freemkv crate).
This commit is contained in:
+59
-3
@@ -781,9 +781,15 @@ impl Stream for DemuxSink {
|
||||
}
|
||||
|
||||
fn write(&mut self, frame: &PesFrame) -> io::Result<()> {
|
||||
// Track 0 (primary video) drives epoch decisions; every other track is a
|
||||
// passive rider on the same global offset — see `TimelineContinuity`.
|
||||
let pts = self.timeline.adjust(frame.pts, frame.track == 0);
|
||||
// The PRIMARY VIDEO track (`ref_video_track`, the first DiscStream::Video)
|
||||
// drives epoch decisions; every other track is a passive rider on the same
|
||||
// global offset — see `TimelineContinuity`. Drive epochs off the same
|
||||
// dynamically-resolved video reference used for the audio-delay
|
||||
// computation, NOT the literal stream index 0 — an M2TS/PMT title can list
|
||||
// an audio ES before the video ES, in which case track 0 is audio and a
|
||||
// non-video epoch driver would ratchet the frontier on sparse/lagging PTS.
|
||||
let drives = Some(frame.track) == self.ref_video_track;
|
||||
let pts = self.timeline.adjust(frame.pts, drives);
|
||||
if let Some(Some(t)) = self.tracks.get_mut(frame.track) {
|
||||
t.first_pts_ns.get_or_insert(pts);
|
||||
t.writer.write_frame(&mut t.w, frame, pts)?;
|
||||
@@ -1172,6 +1178,56 @@ mod tests {
|
||||
assert_eq!(a, out);
|
||||
}
|
||||
|
||||
/// Regression for the hardcoded `frame.track == 0` epoch driver: on an
|
||||
/// M2TS/PMT title the PMT can list an AUDIO ES before the VIDEO ES, so the
|
||||
/// video lands at stream index 1. The sink already resolves the video
|
||||
/// reference dynamically (`ref_video_track`); the epoch driver must use that
|
||||
/// SAME reference, not the literal 0. Here track 0 is audio and track 1 is
|
||||
/// video. A video-only clip boundary (track 1) must open a new epoch (bump
|
||||
/// `offset_ns`). With the bug (only `frame.track == 0` drives epochs) the
|
||||
/// video back-jump would be treated as a passive rider and `offset_ns` would
|
||||
/// stay 0 — corrupting every track's rebased timeline.
|
||||
#[test]
|
||||
fn epoch_driver_follows_ref_video_not_track_zero() {
|
||||
let dir = tempdir();
|
||||
// Audio FIRST (index 0), video SECOND (index 1).
|
||||
let title = title_with(
|
||||
vec![audio_stream(Codec::Ac3, "eng"), video_stream(Codec::H264)],
|
||||
vec![None, None],
|
||||
);
|
||||
let mut sink = DemuxSink::create(&dir, &title, &DemuxOptions::default()).unwrap();
|
||||
assert_eq!(
|
||||
sink.ref_video_track,
|
||||
Some(1),
|
||||
"video reference must be the first VIDEO stream (index 1), not 0"
|
||||
);
|
||||
|
||||
let vid = |pts: i64, data: u8| PesFrame {
|
||||
coding: None,
|
||||
source: None,
|
||||
track: 1, // VIDEO is track 1 here
|
||||
pts,
|
||||
keyframe: true,
|
||||
data: vec![0x00, 0x00, 0x00, 0x01, data],
|
||||
duration_ns: None,
|
||||
};
|
||||
// Clip 1 video: 0s then 10s — advances the frontier.
|
||||
sink.write(&vid(0, 0xAA)).unwrap();
|
||||
sink.write(&vid(10_000_000_000, 0xBB)).unwrap();
|
||||
// Clip 2 seam: video PTS jumps back to ~0 (> 3s back) → NEW epoch. The
|
||||
// video track (index 1) must drive this, bumping the offset.
|
||||
sink.write(&vid(0, 0xCC)).unwrap();
|
||||
|
||||
assert!(
|
||||
sink.timeline.offset_ns >= 10_000_000_000,
|
||||
"video (track 1) must drive the epoch: offset_ns should have advanced \
|
||||
past the previous high, got {}",
|
||||
sink.timeline.offset_ns
|
||||
);
|
||||
sink.finish().unwrap();
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
// ── End-to-end sink ──────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user