Pin per-track handler routing, PID arithmetic and the shared sample budget
Nothing asserted that a hdlr other than vide/soun gets dropped rather than folded into the audio branch, that the per-track PID formulas (0x1011/0x1100 + track_idx) use the right operator and the right operand, that a sample-less track still advances track_idx for the next one, or that the cross-track sample_budget is actually decremented (as opposed to grown or divided) by each track's real count. All four were reachable with a single track_idx == 0, which made every existing fixture blind to +/-/* confusion on these sites - track_idx never moved past 0 in any of them. Also let audio_trak_missing omit stsz, needed to build a sample-less track for the track_idx test.
This commit is contained in:
+217
-1
@@ -2457,7 +2457,9 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let mut stbl = Vec::new();
|
let mut stbl = Vec::new();
|
||||||
stbl.extend_from_slice(&stsd);
|
stbl.extend_from_slice(&stsd);
|
||||||
stbl.extend_from_slice(&stsz);
|
if omit != b"stsz" {
|
||||||
|
stbl.extend_from_slice(&stsz);
|
||||||
|
}
|
||||||
if omit != b"stco" {
|
if omit != b"stco" {
|
||||||
stbl.extend_from_slice(&stco);
|
stbl.extend_from_slice(&stco);
|
||||||
}
|
}
|
||||||
@@ -2936,4 +2938,218 @@ mod tests {
|
|||||||
assert_eq!(entries[0].0, 0, "and the entries kept are the LEADING ones");
|
assert_eq!(entries[0].0, 0, "and the entries kept are the LEADING ones");
|
||||||
assert_eq!(entries[1].0, 1);
|
assert_eq!(entries[1].0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── from_reader: per-track handler routing, PID arithmetic, and the shared
|
||||||
|
// sample budget across tracks. ─────────────────────────────────────────
|
||||||
|
|
||||||
|
/// A minimal-but-complete video `trak` (one H.264 sample), mirroring
|
||||||
|
/// `audio_trak` but for the `vide` handler and an `avc1` sample entry.
|
||||||
|
fn video_trak(timescale: u32) -> Vec<u8> {
|
||||||
|
let mdhd = {
|
||||||
|
let mut p = vec![0u8; 24];
|
||||||
|
p[12..16].copy_from_slice(×cale.to_be_bytes());
|
||||||
|
mp4_box(b"mdhd", &p)
|
||||||
|
};
|
||||||
|
let hdlr = {
|
||||||
|
let mut p = vec![0u8; 12];
|
||||||
|
p[8..12].copy_from_slice(b"vide");
|
||||||
|
mp4_box(b"hdlr", &p)
|
||||||
|
};
|
||||||
|
let stsd = {
|
||||||
|
// VisualSampleEntry's fixed part is 78 bytes; dimensions don't
|
||||||
|
// matter for this test, so the body is all zero.
|
||||||
|
let entry_body = vec![0u8; 78];
|
||||||
|
let mut p = Vec::new();
|
||||||
|
p.extend_from_slice(&[0, 0, 0, 0]); // version+flags
|
||||||
|
p.extend_from_slice(&1u32.to_be_bytes()); // entry_count
|
||||||
|
p.extend_from_slice(&mp4_box(b"avc1", &entry_body));
|
||||||
|
mp4_box(b"stsd", &p)
|
||||||
|
};
|
||||||
|
let stsz = {
|
||||||
|
let mut p = Vec::new();
|
||||||
|
p.extend_from_slice(&[0, 0, 0, 0]);
|
||||||
|
p.extend_from_slice(&10u32.to_be_bytes()); // sample_size (fixed) = 10
|
||||||
|
p.extend_from_slice(&1u32.to_be_bytes()); // count = 1
|
||||||
|
mp4_box(b"stsz", &p)
|
||||||
|
};
|
||||||
|
let stco = {
|
||||||
|
let mut p = Vec::new();
|
||||||
|
p.extend_from_slice(&[0, 0, 0, 0]);
|
||||||
|
p.extend_from_slice(&1u32.to_be_bytes()); // count
|
||||||
|
p.extend_from_slice(&0u32.to_be_bytes()); // chunk offset 0
|
||||||
|
mp4_box(b"stco", &p)
|
||||||
|
};
|
||||||
|
let stsc = {
|
||||||
|
let mut p = Vec::new();
|
||||||
|
p.extend_from_slice(&[0, 0, 0, 0]);
|
||||||
|
p.extend_from_slice(&1u32.to_be_bytes()); // count
|
||||||
|
p.extend_from_slice(&1u32.to_be_bytes()); // first_chunk
|
||||||
|
p.extend_from_slice(&1u32.to_be_bytes()); // samples_per_chunk
|
||||||
|
p.extend_from_slice(&0u32.to_be_bytes()); // sample_desc_idx
|
||||||
|
mp4_box(b"stsc", &p)
|
||||||
|
};
|
||||||
|
let stts = {
|
||||||
|
let mut p = Vec::new();
|
||||||
|
p.extend_from_slice(&[0, 0, 0, 0]);
|
||||||
|
p.extend_from_slice(&1u32.to_be_bytes()); // entry_count
|
||||||
|
p.extend_from_slice(&1u32.to_be_bytes()); // sample_count
|
||||||
|
p.extend_from_slice(&1000u32.to_be_bytes()); // sample_delta
|
||||||
|
mp4_box(b"stts", &p)
|
||||||
|
};
|
||||||
|
let mut stbl = Vec::new();
|
||||||
|
stbl.extend_from_slice(&stsd);
|
||||||
|
stbl.extend_from_slice(&stsz);
|
||||||
|
stbl.extend_from_slice(&stco);
|
||||||
|
stbl.extend_from_slice(&stsc);
|
||||||
|
stbl.extend_from_slice(&stts);
|
||||||
|
let minf = mp4_box(b"minf", &mp4_box(b"stbl", &stbl));
|
||||||
|
let mut mdia = Vec::new();
|
||||||
|
mdia.extend_from_slice(&mdhd);
|
||||||
|
mdia.extend_from_slice(&hdlr);
|
||||||
|
mdia.extend_from_slice(&minf);
|
||||||
|
mp4_box(b"trak", &mp4_box(b"mdia", &mdia))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `hdlr` of anything other than `vide`/`soun` (e.g. a hint or subtitle
|
||||||
|
/// track) must be dropped, not folded into the audio branch. Before this
|
||||||
|
/// guard existed as a literal `&h == b"soun"` comparison, weakening it to
|
||||||
|
/// "anything that isn't vide" would silently turn a hint track's `stsd`
|
||||||
|
/// entry into a fabricated `AudioStream` — with whatever codec its sample
|
||||||
|
/// entry happened to name.
|
||||||
|
#[test]
|
||||||
|
fn non_av_handler_track_is_dropped_not_folded_into_audio() {
|
||||||
|
use std::io::Cursor;
|
||||||
|
let mut trak = audio_trak(48_000);
|
||||||
|
let pos = trak
|
||||||
|
.windows(4)
|
||||||
|
.position(|w| w == b"soun")
|
||||||
|
.expect("fixture has a soun handler");
|
||||||
|
trak[pos..pos + 4].copy_from_slice(b"hint");
|
||||||
|
let moov = mp4_box(b"moov", &trak);
|
||||||
|
let rd = Mp4Reader::from_reader(Cursor::new(moov), "hint".into());
|
||||||
|
assert!(
|
||||||
|
rd.is_err(),
|
||||||
|
"a hint (non-audio/video) handler must not become a stream; \
|
||||||
|
all-dropped → Mp4Invalid"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The per-track PID is `0x1011 + track_idx` for video and `0x1100 +
|
||||||
|
/// track_idx` for audio. At `track_idx == 0` a `+`↔`-`/`*` mutant of either
|
||||||
|
/// is invisible (`0x1011 + 0 == 0x1011 - 0 == 0x1011 * 0`… no — `* 0` is 0,
|
||||||
|
/// already different — but `+`↔`-` specifically needs `track_idx != 0` to
|
||||||
|
/// separate). Four tracks (two video, two audio) push `track_idx` to 1 and
|
||||||
|
/// 3 respectively, so every one of `+`→`*`, `+`→`-` is forced to disagree
|
||||||
|
/// with the correct PID on the second track of its kind.
|
||||||
|
#[test]
|
||||||
|
fn per_track_pid_arithmetic_is_exact_past_the_first_track() {
|
||||||
|
use crate::disc::Stream as DiscStreamE;
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
let mut traks = Vec::new();
|
||||||
|
traks.extend_from_slice(&video_trak(25)); // track_idx 0
|
||||||
|
traks.extend_from_slice(&video_trak(25)); // track_idx 1
|
||||||
|
traks.extend_from_slice(&audio_trak(48_000)); // track_idx 2
|
||||||
|
traks.extend_from_slice(&audio_trak(48_000)); // track_idx 3
|
||||||
|
let moov = mp4_box(b"moov", &traks);
|
||||||
|
let rd = Mp4Reader::from_reader(Cursor::new(moov), "pids".into()).unwrap();
|
||||||
|
assert_eq!(rd.info().streams.len(), 4);
|
||||||
|
|
||||||
|
let pid = |s: &crate::disc::Stream| -> u16 {
|
||||||
|
match s {
|
||||||
|
DiscStreamE::Video(v) => v.pid,
|
||||||
|
DiscStreamE::Audio(a) => a.pid,
|
||||||
|
DiscStreamE::Subtitle(_) => panic!("mp4 from_reader never emits subtitles"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert_eq!(pid(&rd.info().streams[0]), 0x1011, "video track_idx 0");
|
||||||
|
assert_eq!(
|
||||||
|
pid(&rd.info().streams[1]),
|
||||||
|
0x1011 + 1,
|
||||||
|
"video track_idx 1 — the boundary + vs - vs * separates on"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pid(&rd.info().streams[2]),
|
||||||
|
0x1100 + 2,
|
||||||
|
"audio track_idx 2 (track_idx is a single counter shared by \
|
||||||
|
every track, video or audio)"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pid(&rd.info().streams[3]),
|
||||||
|
0x1100 + 3,
|
||||||
|
"audio track_idx 3 — the + vs - boundary"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A track whose `stsz` is absent (`n == 0`) still occupies a `track_idx`
|
||||||
|
/// slot and must still advance the counter for the NEXT track — otherwise
|
||||||
|
/// two tracks collide on the same PID, or (worse, if the increment
|
||||||
|
/// direction is reversed) `track_idx` underflows a `usize` and panics on
|
||||||
|
/// the very first sample-less track.
|
||||||
|
#[test]
|
||||||
|
fn track_idx_advances_past_a_sample_less_track() {
|
||||||
|
use crate::disc::Stream as DiscStreamE;
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
let mut traks = audio_trak_missing(b"stsz"); // track_idx 0: no samples
|
||||||
|
traks.extend_from_slice(&audio_trak(48_000)); // track_idx 1: has samples
|
||||||
|
let moov = mp4_box(b"moov", &traks);
|
||||||
|
let rd = Mp4Reader::from_reader(Cursor::new(moov), "skip-idx".into()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
rd.info().streams.len(),
|
||||||
|
2,
|
||||||
|
"the sample-less track is still kept as a (silent) stream"
|
||||||
|
);
|
||||||
|
match &rd.info().streams[1] {
|
||||||
|
DiscStreamE::Audio(a) => assert_eq!(
|
||||||
|
a.pid,
|
||||||
|
0x1100 + 1,
|
||||||
|
"the second track's PID must reflect track_idx having \
|
||||||
|
advanced past the sample-less first track"
|
||||||
|
),
|
||||||
|
_ => panic!("expected an audio stream"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The global `sample_budget` is DECREMENTED by each track's real sample
|
||||||
|
/// count, and it is SHARED across tracks — a track late in the file gets
|
||||||
|
/// only what earlier tracks left behind. Build two tracks that both LIE
|
||||||
|
/// about their sample count (`stsz` fixed-size, count = `u32::MAX`, as in
|
||||||
|
/// `audio_trak_hostile_count`): the file is tiny, so the first track's
|
||||||
|
/// clamped count exactly exhausts the whole initial budget, and the
|
||||||
|
/// second track — clamped against what's left — must come back with
|
||||||
|
/// ZERO samples. An `+=`/`/=` mutant of the decrement either grows the
|
||||||
|
/// budget or leaves it nonzero, letting the second track claim samples it
|
||||||
|
/// must not have.
|
||||||
|
#[test]
|
||||||
|
fn sample_budget_is_shared_and_exhausted_across_tracks() {
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
let mut traks = audio_trak_hostile_count(); // track_idx 0
|
||||||
|
traks.extend_from_slice(&audio_trak_hostile_count()); // track_idx 1
|
||||||
|
let moov = mp4_box(b"moov", &traks);
|
||||||
|
let file_len = moov.len() as u64;
|
||||||
|
let expected_initial_budget =
|
||||||
|
MAX_SAMPLE_COUNT.min((file_len / MIN_FILE_BYTES_PER_SAMPLE) as usize);
|
||||||
|
// The fixture's own stsc/stts windows (0xFFFF) must not be the binding
|
||||||
|
// constraint, or the budget's effect is untestable.
|
||||||
|
assert!(
|
||||||
|
expected_initial_budget < 0xFFFF,
|
||||||
|
"fixture assumption: the shared byte budget must bind before the \
|
||||||
|
per-track stsc/stts window does"
|
||||||
|
);
|
||||||
|
|
||||||
|
let rd = Mp4Reader::from_reader(Cursor::new(moov), "budget".into()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
rd.samples.len(),
|
||||||
|
expected_initial_budget,
|
||||||
|
"the first track alone must exhaust the whole initial budget"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
rd.samples.iter().filter(|s| s.track == 1).count(),
|
||||||
|
0,
|
||||||
|
"the second track must be left with zero budget, not a grown or \
|
||||||
|
partially-decremented one"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user