Parse MKV lacing, route by real TrackNumber, honour NAL length size and edit lists
Four conformance defects in the read paths, two of them silent corruption. **Lacing was ignored entirely.** RFC 9559 §10.2 defines Xiph, EBML and fixed-size lacing, where one Block carries several frames; the reader took the Block payload verbatim, so a laced Block became a single "frame" consisting of a lacing header followed by concatenated frames — garbage to the codec parser, no error. Audio tracks from other muxers commonly use lacing, so an ordinary foreign MKV was silently mangled. All three modes are now parsed: Xiph 255-run sizes including the trailing-zero rule for exact multiples of 255, EBML unsigned first size plus SIGNED VINT deltas with the 2^((7*n)-1)-1 bias of §10.3.3, and fixed-size even division, with the last frame's size deduced from the remainder. Laced timestamps follow §10.3.5: the first frame takes the Block timestamp and the rest are spaced by the track's DefaultDuration, else BlockDuration/count, else shared with a warn. Parsing was chosen over refusing because refusal would leave freemkv unable to remux common foreign audio at all, and each mode is about fifteen lines. A malformed lacing header now raises a NEW code, E_MKV_LACING_INVALID = 9052, deliberately NOT MkvInvalid — because is_skippable_title_stub classifies MkvInvalid as a skippable nav stub, so reusing it would have recreated the exact conflation that is still open as a separate finding. A test asserts the new code is not skippable. **TrackNumber was assumed to be 1..N in TrackEntry order.** RFC 9559 §5.1.4.1.1 only requires it to be non-zero and unique, so sparse or unordered numbers are legal. Block routing and codec_private both computed track + 1. A real TrackNumber map is now built, recorded only for TrackEntries that yield a stream so dropped track types no longer shift the mapping. Verified red here independently, and the failure mode is worse than mis-routing: with track + 1 restored, a buttons track's payload was attributed to the AUDIO stream — wrong payload into the wrong codec parser. **The NAL length prefix was hardcoded to 4 bytes.** lengthSizeMinusOne lives in avcC byte 4 and hvcC byte 21 (ISO/IEC 14496-15 §5.3.3.1.2, §8.3.3.1.2) and was never read, so a source declaring 1- or 2-byte prefixes had its raw prefixed bytes emitted verbatim with no start codes. All four conversion sites now derive the width from the track's own configuration record. **Edit lists were ignored.** No edts/elst was parsed, so the presentation timeline an edit list defines (ISO/IEC 14496-12 §8.6.5/§8.6.6) was dropped — which is how encoder delay is normally expressed. Leading empty edits and the first media edit's media_time are now applied to both dts and pts, with the movie vs media timescale distinction respected. A list needing more than a constant shift applies the leading edit and warns rather than presenting the result as faithful. 17 tests. I reproduced the lacing mutant independently: returning the body whole kills five of them, including the exact-payload and malformed-header cases. Still open and deliberately untouched: the MkvInvalid / is_skippable_title_stub conflation across ~20 reader raise sites. It is a cross-cutting error.rs change and E_MKV_LACING_INVALID is the template for it.
This commit is contained in:
+51
-4
@@ -23,7 +23,9 @@
|
||||
//! The sink does NOT touch the MKV mux path; it is purely additive.
|
||||
|
||||
use crate::disc::{Chapter, Codec, DiscTitle, Stream as DiscStream};
|
||||
use crate::mux::hevc::{append_length_prefixed_as_annex_b, avcc_to_annex_b, hvcc_to_annex_b};
|
||||
use crate::mux::hevc::{
|
||||
append_length_prefixed_as_annex_b_sized, avcc_to_annex_b, hvcc_to_annex_b, nal_length_size,
|
||||
};
|
||||
use crate::mux::timeline::TimelineContinuity;
|
||||
use crate::pes::{PesFrame, Stream};
|
||||
use std::fs::File;
|
||||
@@ -207,12 +209,17 @@ impl EsWriter for PassthroughWriter {
|
||||
}
|
||||
}
|
||||
|
||||
/// HEVC/H.264 writer: reframes 4-byte-length-prefixed NALs (the hvcC/avcC form
|
||||
/// the parsers emit) into Annex-B, prepending the parameter sets once.
|
||||
/// HEVC/H.264 writer: reframes length-prefixed NALs (the hvcC/avcC form the
|
||||
/// parsers emit) into Annex-B, prepending the parameter sets once.
|
||||
struct AnnexBWriter {
|
||||
/// Annex-B-framed VPS/SPS/PPS (or SPS/PPS), parsed from the hvcC/avcC.
|
||||
params: Vec<u8>,
|
||||
wrote_params: bool,
|
||||
/// Octets per NAL length prefix, from the configuration record's
|
||||
/// `lengthSizeMinusOne` (ISO/IEC 14496-15). NOT assumed to be 4: a legal
|
||||
/// avcC/hvcC may declare 1 or 2, and reading those as u32-BE parses no NALs
|
||||
/// at all, so the raw prefixed bytes would be emitted as if already Annex B.
|
||||
length_size: usize,
|
||||
}
|
||||
|
||||
impl AnnexBWriter {
|
||||
@@ -223,6 +230,7 @@ impl AnnexBWriter {
|
||||
Self {
|
||||
params,
|
||||
wrote_params: false,
|
||||
length_size: nal_length_size(codec, codec_private),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,7 +252,7 @@ impl EsWriter for AnnexBWriter {
|
||||
// zero-length NALs and drops a truncated trailing NAL without panicking,
|
||||
// rather than `break`ing on the first zero-length NAL.
|
||||
let mut scratch = Vec::with_capacity(f.data.len() + (f.data.len() / 32) + 4);
|
||||
append_length_prefixed_as_annex_b(&mut scratch, &f.data);
|
||||
append_length_prefixed_as_annex_b_sized(&mut scratch, &f.data, self.length_size);
|
||||
w.write_all(&scratch)?;
|
||||
n += scratch.len();
|
||||
Ok(n)
|
||||
@@ -982,6 +990,45 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression (silent corruption): the sink reframed every frame as if the
|
||||
/// NAL length prefixes were 4 octets wide. ISO/IEC 14496-15 §5.3.3.1.2 lets
|
||||
/// an avcC declare `lengthSizeMinusOne = 1` (2-octet prefixes); reading those
|
||||
/// as one u32-BE gives an absurd length, so nothing parsed and the raw
|
||||
/// prefixed bytes were written to the `demux://` ES verbatim — no start
|
||||
/// codes, undecodable video, and no error anywhere.
|
||||
#[test]
|
||||
fn annexb_writer_honours_the_records_declared_nal_length_size() {
|
||||
// avcC with byte 4 = 0xFD → lengthSizeMinusOne 1 → 2-octet prefixes.
|
||||
// numSPS = 1 (0xE1), SPS len 2 = [0x67 0x42], numPPS = 1, PPS len 1.
|
||||
let rec = [
|
||||
1, 0x42, 0x00, 0x1F, 0xFD, 0xE1, 0, 2, 0x67, 0x42, 1, 0, 1, 0x68,
|
||||
];
|
||||
assert_eq!(nal_length_size(Codec::H264, Some(&rec)), 2);
|
||||
let mut w = AnnexBWriter::new(Codec::H264, Some(&rec));
|
||||
let mut out = Vec::new();
|
||||
let f = PesFrame {
|
||||
coding: None,
|
||||
source: None,
|
||||
track: 0,
|
||||
pts: 0,
|
||||
keyframe: true,
|
||||
// Two NALs with 2-octet length prefixes.
|
||||
data: vec![0, 2, 0xAA, 0xBB, 0, 3, 0x01, 0x02, 0x03],
|
||||
duration_ns: None,
|
||||
};
|
||||
w.write_frame(&mut out, &f, 0).unwrap();
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![
|
||||
0, 0, 0, 1, 0x67, 0x42, // SPS
|
||||
0, 0, 0, 1, 0x68, // PPS
|
||||
0, 0, 0, 1, 0xAA, 0xBB, // frame NAL #1
|
||||
0, 0, 0, 1, 0x01, 0x02, 0x03, // frame NAL #2
|
||||
],
|
||||
"2-octet-prefixed NALs must reach the ES as Annex B"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn annexb_writer_prepends_params_once() {
|
||||
let rec = [
|
||||
|
||||
Reference in New Issue
Block a user