use mp4_track::{
ChunkOffsetKind, Codec, Edit, Error, FourCc, Mp4Reader, TrackId, TrackKind, Warning,
};
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::fs::File;
use std::path::{Path, PathBuf};
fn data() -> Value {
serde_json::from_slice(include_bytes!("data/conformance-vectors.json"))
.expect("conformance data must be valid JSON")
}
fn fixture_path(entry: &Value) -> PathBuf {
let file = entry["file"]
.as_str()
.expect("fixture file must be a string");
let name = Path::new(file)
.file_name()
.expect("fixture path must have a file name");
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
fn fourcc(value: &str) -> FourCc {
FourCc::try_from(value).expect("vector fourcc must contain four bytes")
}
fn codec_entry(codec: &Codec) -> FourCc {
match codec {
Codec::H264 { entry, .. } | Codec::H265 { entry, .. } | Codec::Other { entry, .. } => {
*entry
}
Codec::Aac { .. } => FourCc(*b"mp4a"),
Codec::Opus { .. } => FourCc(*b"Opus"),
}
}
fn codec_payload(codec: &Codec) -> &[u8] {
match codec {
Codec::H264 { config, .. } => &config.raw,
Codec::H265 { config, .. } => &config.raw,
Codec::Aac { config } => &config.raw_esds,
Codec::Opus { config } => &config.raw,
Codec::Other { raw, .. } => raw,
}
}
fn kind_name(kind: TrackKind) -> &'static str {
match kind {
TrackKind::Video => "vide",
TrackKind::Audio => "soun",
TrackKind::Subtitle => "subt",
TrackKind::Other(_) => "other",
}
}
#[test]
fn every_valid_vector_matches_parsed_facts() {
let vectors = data();
for entry in vectors["fixtures"]
.as_array()
.expect("fixtures must be an array")
.iter()
.filter(|entry| entry["kind"] == "valid")
{
let id = entry["id"].as_str().expect("fixture id must be a string");
let file = File::open(fixture_path(entry)).expect("fixture must open");
let reader = Mp4Reader::open(file).unwrap_or_else(|error| panic!("{id}: {error}"));
let expected = &entry["reference"];
assert_eq!(
reader.movie_timescale(),
expected["mvhd"]["timescale"].as_u64().expect("timescale") as u32,
"{id}"
);
assert_eq!(
reader.movie_duration(),
expected["mvhd"]["duration"].as_u64().expect("duration"),
"{id}"
);
assert_eq!(
reader.brands().major,
fourcc(
expected["ftyp"]["major_brand"]
.as_str()
.expect("major brand")
),
"{id}"
);
assert_eq!(
reader.brands().minor_version as u64,
expected["ftyp"]["minor_version"]
.as_u64()
.expect("minor version"),
"{id}"
);
let compatible: Vec<_> = expected["ftyp"]["compatible_brands"]
.as_array()
.expect("compatible brands")
.iter()
.map(|brand| fourcc(brand.as_str().expect("compatible brand")))
.collect();
assert_eq!(reader.brands().compatible, compatible, "{id}");
assert_eq!(
reader.is_fragmented(),
expected["mvex_present"].as_bool() == Some(true),
"{id}"
);
let tracks = expected["tracks"]
.as_array()
.expect("tracks must be an array");
assert_eq!(reader.tracks().len(), tracks.len(), "{id}");
for (actual, expected_track) in reader.tracks().iter().zip(tracks) {
let track_id = expected_track["track_id"].as_u64().expect("track id") as u32;
assert_eq!(actual.id, TrackId(track_id), "{id}");
assert_eq!(
actual.enabled,
expected_track["tkhd_flags"].as_u64().expect("track flags") & 1 != 0,
"{id} track {track_id}"
);
assert_eq!(
kind_name(actual.kind),
expected_track["handler_type"]
.as_str()
.expect("handler type"),
"{id} track {track_id}"
);
assert_eq!(
actual.handler_name,
expected_track["handler_name"]
.as_str()
.expect("handler name"),
"{id} track {track_id}"
);
assert_eq!(
actual.timescale,
expected_track["timescale"]
.as_u64()
.expect("track timescale") as u32,
"{id} track {track_id}"
);
if let Some(expected_duration) = expected_track["stts_total_duration"].as_u64() {
assert_eq!(actual.duration, expected_duration, "{id} track {track_id}");
}
assert_eq!(
actual.language,
expected_track["language"].as_str().expect("language"),
"{id} track {track_id}"
);
assert!(
(actual.width - expected_track["width"].as_f64().expect("width")).abs()
< f64::EPSILON,
"{id} track {track_id}"
);
assert!(
(actual.height - expected_track["height"].as_f64().expect("height")).abs()
< f64::EPSILON,
"{id} track {track_id}"
);
let expected_edits: Vec<_> = expected_track["elst"]["entries"]
.as_array()
.map(|entries| {
entries
.iter()
.map(|edit| {
let encoded_time =
edit["media_time"].as_i64().expect("edit media time");
Edit {
segment_duration: edit["segment_duration"]
.as_u64()
.expect("edit duration"),
media_time: u64::try_from(encoded_time).ok(),
media_rate: edit["media_rate"].as_f64().expect("edit rate") as f32,
}
})
.collect()
})
.unwrap_or_default();
assert_eq!(actual.edit_list, expected_edits, "{id} track {track_id}");
let non_empty: Vec<_> = expected_edits
.iter()
.filter_map(|edit| edit.media_time)
.collect();
assert_eq!(
actual.presentation_offset(),
if non_empty.len() == 1 {
non_empty.first().copied()
} else {
None
},
"{id} track {track_id}"
);
let expected_entry = &expected_track["stsd"]["entries"][0];
assert_eq!(
codec_entry(&actual.codec),
fourcc(expected_entry["fourcc"].as_str().expect("entry code")),
"{id} track {track_id}"
);
if let Some(configs) = expected_entry["config_boxes"].as_array() {
if let Some(config) = configs.iter().find(|item| {
matches!(
item["fourcc"].as_str(),
Some("avcC" | "hvcC" | "esds" | "dOps")
)
}) {
let digest = format!("{:x}", Sha256::digest(codec_payload(&actual.codec)));
assert_eq!(
digest,
config["payload_sha256"].as_str().expect("config digest"),
"{id} track {track_id}"
);
}
}
let expected_kind = if expected["mvex_present"].as_bool() == Some(true)
&& expected_track["samples"]["count"].as_u64() == Some(0)
{
"none"
} else {
expected_track["chunk_offset_box"]
.as_str()
.expect("chunk box")
};
let actual_kind = match actual.chunk_offsets {
ChunkOffsetKind::Stco => "stco",
ChunkOffsetKind::Co64 => "co64",
ChunkOffsetKind::None => "none",
};
assert_eq!(actual_kind, expected_kind, "{id} track {track_id}");
let expected_samples = &expected_track["samples"];
assert_eq!(
actual.sample_count as u64,
expected_samples["count"].as_u64().expect("sample count"),
"{id} track {track_id}"
);
assert_eq!(
actual.sync_sample_count.map(u64::from),
expected_track["stss_count"].as_u64(),
"{id} track {track_id}"
);
if let Some(crosscheck) = entry["crosscheck"]["per_track"]
.as_array()
.and_then(|tracks| tracks.iter().find(|track| track["track_id"] == track_id))
{
assert_eq!(
u64::try_from(reader.samples(actual.id).expect("samples").count())
.expect("sample count fits u64"),
crosscheck["packet_count_ffprobe"]
.as_u64()
.expect("packet count"),
"{id} track {track_id}"
);
let dts_shift = crosscheck["dts_shift_reference_minus_ffprobe"].as_i64();
assert!(
dts_shift.is_some(),
"{id} track {track_id}: dts shift must be a recorded constant, got {}",
crosscheck["dts_shift_reference_minus_ffprobe"]
);
assert!(
crosscheck["pts_shift_reference_minus_ffprobe"]
.as_i64()
.is_some(),
"{id} track {track_id}: pts shift must be a recorded constant, got {}",
crosscheck["pts_shift_reference_minus_ffprobe"]
);
assert_eq!(
crosscheck["offset_size_sequence_equal"].as_bool(),
Some(true),
"{id} track {track_id}"
);
assert_eq!(
crosscheck["sync_flags_equal"].as_bool(),
Some(true),
"{id} track {track_id}"
);
}
if actual.chunk_offsets == ChunkOffsetKind::None {
continue;
}
let samples: Vec<_> = reader
.samples(actual.id)
.expect("track samples must expand")
.collect();
if let Some(list) = expected_samples["list"].as_array() {
assert_eq!(samples.len(), list.len(), "{id} track {track_id}");
for (sample, expected_sample) in samples.iter().zip(list) {
assert_eq!(
sample.offset,
expected_sample["offset"].as_u64().expect("offset"),
"{id} track {track_id}"
);
assert_eq!(
sample.size as u64,
expected_sample["size"].as_u64().expect("size"),
"{id} track {track_id}"
);
assert_eq!(
sample.dts,
expected_sample["dts"].as_u64().expect("dts"),
"{id} track {track_id}"
);
assert_eq!(
sample.cts_offset,
expected_sample["cts_offset"].as_i64().expect("cts offset"),
"{id} track {track_id}"
);
assert_eq!(
sample.is_sync,
expected_sample["sync"].as_bool().expect("sync"),
"{id} track {track_id}"
);
assert_eq!(
u64::from(sample.chunk).saturating_add(1),
expected_sample["chunk"].as_u64().expect("chunk"),
"{id} track {track_id}"
);
assert_eq!(
u64::from(sample.description_index),
expected_sample["sdi"].as_u64().expect("description index"),
"{id} track {track_id}"
);
assert_eq!(
sample.pts,
i64::try_from(sample.dts)
.expect("test dts fits i64")
.saturating_add(sample.cts_offset),
"{id} track {track_id}"
);
assert_eq!(
reader
.sample_info(actual.id, sample.index)
.expect("indexed sample"),
*sample,
"{id} track {track_id}"
);
}
} else {
let csv = samples
.into_iter()
.map(|sample| {
format!(
"{},{},{},{},{}",
sample.offset,
sample.size,
sample.dts,
sample.cts_offset,
u8::from(sample.is_sync)
)
})
.collect::<Vec<_>>()
.join("\n");
let digest = format!("{:x}", Sha256::digest(csv.as_bytes()));
assert_eq!(
digest,
expected_samples["sha256_of_csv"]
.as_str()
.expect("sample digest"),
"{id} track {track_id}"
);
}
}
assert_eq!(
reader.fragments().len(),
expected["fragments"].as_array().expect("fragments").len(),
"{id}"
);
for (actual_fragment, expected_fragment) in reader
.fragments()
.iter()
.zip(expected["fragments"].as_array().expect("fragments"))
{
assert_eq!(
actual_fragment.sequence_number as u64,
expected_fragment["sequence_number"]
.as_u64()
.expect("fragment sequence"),
"{id}"
);
assert_eq!(
actual_fragment.moof_offset,
expected_fragment["moof_offset"]
.as_u64()
.expect("fragment offset"),
"{id}"
);
let expected_runs: Vec<_> = expected_fragment["trafs"]
.as_array()
.expect("fragment tracks")
.iter()
.flat_map(|traf| {
let track = traf["track_id"].as_u64().expect("fragment track") as u32;
traf["truns"]
.as_array()
.expect("fragment runs")
.iter()
.map(move |run| (track, &run["samples"]))
})
.collect();
assert_eq!(actual_fragment.runs.len(), expected_runs.len(), "{id}");
for (actual_run, (track, expected_samples)) in
actual_fragment.runs.iter().zip(expected_runs)
{
assert_eq!(actual_run.track, TrackId(track), "{id}");
let list = expected_samples["list"]
.as_array()
.expect("fragment samples");
assert_eq!(actual_run.samples.len(), list.len(), "{id}");
for (actual_sample, expected_sample) in actual_run.samples.iter().zip(list) {
assert_eq!(
(
actual_sample.offset,
u64::from(actual_sample.size),
actual_sample.dts,
actual_sample.cts_offset,
actual_sample.is_sync,
),
(
expected_sample["offset"].as_u64().expect("offset"),
expected_sample["size"].as_u64().expect("size"),
expected_sample["dts"].as_u64().expect("dts"),
expected_sample["cts_offset"].as_i64().expect("cts"),
expected_sample["sync"].as_bool().expect("sync"),
),
"{id}"
);
}
}
}
}
}
fn error_kind(error: &Error) -> &'static str {
match error {
Error::Truncated { .. } => "Truncated",
Error::BoxSizeTooSmall { .. } => "BoxSizeTooSmall",
Error::BoxExceedsFile { .. } => "BoxExceedsFile",
Error::ChildExceedsParent { .. } => "ChildExceedsParent",
Error::ZeroSizeNested { .. } => "ZeroSizeNested",
Error::NestingTooDeep { .. } => "NestingTooDeep",
Error::MissingBox { .. } => "MissingBox",
Error::Malformed { .. } => "Malformed",
Error::InvalidSampleTable { .. } => "InvalidSampleTable",
Error::InvalidTimescale { .. } => "InvalidTimescale",
Error::SampleOutOfFile { .. } => "SampleOutOfFile",
Error::LimitExceeded { .. } => "LimitExceeded",
other => panic!("unexpected error variant: {other}"),
}
}
#[test]
fn every_malformed_vector_returns_its_typed_error() {
let vectors = data();
for entry in vectors["fixtures"]
.as_array()
.expect("fixtures must be an array")
.iter()
.filter(|entry| entry["kind"] == "malformed")
{
let id = entry["id"].as_str().expect("fixture id must be a string");
let expected = entry["expect"]["error_kind"].as_str().expect("error kind");
let stage = entry["expect"]["stage"].as_str().expect("error stage");
let file = File::open(fixture_path(entry)).expect("fixture must open");
let result = std::panic::catch_unwind(|| Mp4Reader::open(file));
let open = result.unwrap_or_else(|_| panic!("{id}: parser panicked"));
if stage == "open" {
let error = open
.err()
.unwrap_or_else(|| panic!("{id}: expected open failure"));
assert_eq!(error_kind(&error), expected, "{id}: {error}");
} else {
let mut reader =
open.unwrap_or_else(|error| panic!("{id}: open failed early: {error}"));
let (track, index) = match id {
"m10_co64_offset_above_u32" => (TrackId(1), 0),
"m11_stsz_sum_overflows_u32" => (TrackId(1), 0),
"m16_truncated_mdat" => {
let track = reader.tracks().last().expect("truncated file has tracks");
(track.id, track.sample_count.saturating_sub(1))
}
_ => panic!("{id}: unknown sample-read case"),
};
let error = reader
.read_sample(track, index)
.expect_err("sample read must fail");
assert_eq!(error_kind(&error), expected, "{id}: {error}");
}
}
}
#[test]
fn d1_emsg_unterminated_string_is_typed_error() {
let file = File::open(
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/m09_emsg_unterminated_string.mp4"),
)
.expect("fixture");
assert!(
matches!(Mp4Reader::open(file), Err(Error::Malformed { kind, .. }) if kind == FourCc(*b"emsg"))
);
}
#[test]
fn d2_nested_zero_size_box_errors_within_bound() {
let file = File::open(
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/m04_nested_box_size_zero.mp4"),
)
.expect("fixture");
assert!(
matches!(Mp4Reader::open(file), Err(Error::ZeroSizeNested { kind, .. }) if kind == FourCc(*b"stss"))
);
}
#[test]
fn d3_stsz_sum_above_u32_is_exact_then_out_of_file() {
let path =
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/m11_stsz_sum_overflows_u32.mp4");
let mut bytes = std::fs::read(path).expect("fixture");
let stsc = bytes
.windows(4)
.position(|window| window == b"stsc")
.expect("stsc");
bytes[stsc + 16..stsc + 20].copy_from_slice(&25u32.to_be_bytes());
let stco = bytes
.windows(4)
.position(|window| window == b"stco")
.expect("stco");
bytes[stco + 8..stco + 12].copy_from_slice(&1u32.to_be_bytes());
let mut reader = Mp4Reader::open(std::io::Cursor::new(bytes)).expect("tables must parse");
let info = reader.sample_info(TrackId(1), 3).expect("fourth sample");
assert_eq!(info.offset, 202 + 3 * u64::from(u32::MAX));
assert!(matches!(
reader.read_sample(TrackId(1), 3),
Err(Error::SampleOutOfFile { .. })
));
}
#[test]
fn d3_co64_offset_above_u32_is_exact() {
let file = File::open(
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/m10_co64_offset_above_u32.mp4"),
)
.expect("fixture");
let mut reader = Mp4Reader::open(file).expect("tables must parse");
assert_eq!(
reader.sample_info(TrackId(1), 0).expect("sample").offset,
0x1_0000_0010
);
assert!(matches!(
reader.read_sample(TrackId(1), 0),
Err(Error::SampleOutOfFile { .. })
));
}
#[test]
fn e_esds_descriptor_walk_skips_bitrate_bytes() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v05_aac_only.mp4");
let bytes = std::fs::read(path).expect("fixture");
let expected_config = [0x11u8, 0x88, 0x56, 0xe5, 0x00];
let read_config = |bytes: Vec<u8>| {
let reader = Mp4Reader::open(std::io::Cursor::new(bytes)).expect("fixture must parse");
let track = reader.tracks().first().expect("audio track").clone();
match track.codec {
Codec::Aac { config } => (config.object_type, config.audio_specific_config),
other => panic!("expected AAC, found {other:?}"),
}
};
let (object_type, config) = read_config(bytes.clone());
assert_eq!(object_type, 0x40);
assert_eq!(config, expected_config);
let mut patched = bytes;
assert_eq!(&patched[6679..6683], &[0x00, 0x00, 0xbc, 0x1c]);
patched[6679..6683].copy_from_slice(&[0x00, 0x05, 0x02, 0x00]);
let (object_type, config) = read_config(patched);
assert_eq!(object_type, 0x40);
assert_eq!(config, expected_config);
}
#[test]
fn f_trailing_bytes_after_moov_are_a_warning() {
let path =
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v02_h264_aac_faststart.mp4");
let mut bytes = std::fs::read(path).expect("fixture");
let clean_len = bytes.len() as u64;
bytes.extend_from_slice(&[0xAB; 16]);
let reader =
Mp4Reader::open(std::io::Cursor::new(bytes)).expect("trailing junk must not fail open");
assert_eq!(reader.tracks().len(), 2);
assert!(
reader.warnings().contains(&Warning::TrailingBytes {
at: clean_len,
count: 16
}),
"expected a trailing-bytes warning, found {:?}",
reader.warnings()
);
}
#[test]
fn g_fragment_without_tfdt_continues_running_decode_time() {
let path =
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v07_fragmented_input.mp4");
let bytes = std::fs::read(path).expect("fixture");
let pristine = Mp4Reader::open(std::io::Cursor::new(bytes.clone())).expect("fixture");
let mut patched_bytes = bytes;
let mut renamed = 0usize;
for at in 0..patched_bytes.len().saturating_sub(4) {
if &patched_bytes[at..at + 4] == b"tfdt" {
patched_bytes[at..at + 4].copy_from_slice(b"free");
renamed += 1;
}
}
assert_eq!(renamed, 6, "v07 carries one tfdt per track fragment");
let patched = Mp4Reader::open(std::io::Cursor::new(patched_bytes)).expect("patched fixture");
assert_eq!(patched.fragments().len(), pristine.fragments().len());
assert!(pristine.fragments().len() > 1);
for (patched_fragment, pristine_fragment) in
patched.fragments().iter().zip(pristine.fragments())
{
let patched_dts: Vec<_> = patched_fragment
.runs
.iter()
.map(|run| {
(
run.track,
run.samples.iter().map(|s| s.dts).collect::<Vec<_>>(),
)
})
.collect();
let pristine_dts: Vec<_> = pristine_fragment
.runs
.iter()
.map(|run| {
(
run.track,
run.samples.iter().map(|s| s.dts).collect::<Vec<_>>(),
)
})
.collect();
assert_eq!(patched_dts, pristine_dts);
}
}
#[test]
fn unknown_box_nested_in_moov_warns() {
let path =
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v02_h264_aac_faststart.mp4");
let mut bytes = std::fs::read(path).expect("fixture");
let at = bytes
.windows(4)
.position(|window| window == b"udta")
.expect("v02 carries user data");
bytes[at..at + 4].copy_from_slice(b"zzzz");
let reader = Mp4Reader::open(std::io::Cursor::new(bytes)).expect("fixture must parse");
assert!(
reader.warnings().contains(&Warning::UnknownBox {
kind: FourCc(*b"zzzz"),
parent: Some(FourCc(*b"moov")),
}),
"expected an unknown-box warning, found {:?}",
reader.warnings()
);
}
#[test]
fn presentation_offset_ignores_leading_empty_edit() {
let base = {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/v05_aac_only.mp4");
let file = File::open(path).expect("fixture");
let reader = Mp4Reader::open(file).expect("fixture must parse");
reader.tracks().first().expect("audio track").clone()
};
let mut track = base;
track.edit_list = vec![
Edit {
segment_duration: 500,
media_time: None,
media_rate: 1.0,
},
Edit {
segment_duration: 1000,
media_time: Some(1024),
media_rate: 1.0,
},
];
assert_eq!(track.presentation_offset(), Some(1024));
track.edit_list.push(Edit {
segment_duration: 1000,
media_time: Some(2048),
media_rate: 1.0,
});
assert_eq!(track.presentation_offset(), None);
}