use mp4box::registry::StructuredData;
use mp4box::{get_boxes, track_samples_from_reader};
use std::io::Cursor;
fn plain_box(typ: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let mut v = Vec::with_capacity(8 + payload.len());
v.extend_from_slice(&((8 + payload.len()) as u32).to_be_bytes());
v.extend_from_slice(typ);
v.extend_from_slice(payload);
v
}
fn full_box(typ: &[u8; 4], version: u8, flags: u32, payload: &[u8]) -> Vec<u8> {
let mut inner = Vec::with_capacity(4 + payload.len());
inner.push(version);
inner.extend_from_slice(&flags.to_be_bytes()[1..4]);
inner.extend_from_slice(payload);
plain_box(typ, &inner)
}
fn concat(parts: &[Vec<u8>]) -> Vec<u8> {
parts.iter().flatten().copied().collect()
}
fn fragmented_moov(default_duration: u32, default_flags: u32) -> Vec<u8> {
let mvhd = {
let mut p = Vec::new();
p.extend_from_slice(&[0u8; 8]); p.extend_from_slice(&1000u32.to_be_bytes()); p.extend_from_slice(&0u32.to_be_bytes()); p.extend_from_slice(&0x0001_0000u32.to_be_bytes()); p.extend_from_slice(&[0u8; 2 + 10 + 36 + 24]);
p.extend_from_slice(&2u32.to_be_bytes()); full_box(b"mvhd", 0, 0, &p)
};
let tkhd = {
let mut p = Vec::new();
p.extend_from_slice(&[0u8; 8]); p.extend_from_slice(&1u32.to_be_bytes()); p.extend_from_slice(&[0u8; 4]); p.extend_from_slice(&0u32.to_be_bytes()); p.extend_from_slice(&[0u8; 8 + 8 + 36 + 8]);
full_box(b"tkhd", 0, 3, &p)
};
let mdhd = {
let mut p = Vec::new();
p.extend_from_slice(&[0u8; 8]); p.extend_from_slice(&1000u32.to_be_bytes()); p.extend_from_slice(&0u32.to_be_bytes()); p.extend_from_slice(&0x55C4u16.to_be_bytes()); p.extend_from_slice(&[0u8; 2]);
full_box(b"mdhd", 0, 0, &p)
};
let hdlr = {
let mut p = Vec::new();
p.extend_from_slice(&[0u8; 4]);
p.extend_from_slice(b"vide");
p.extend_from_slice(&[0u8; 12]);
p.push(0); full_box(b"hdlr", 0, 0, &p)
};
let stbl = plain_box(
b"stbl",
&concat(&[
full_box(b"stsd", 0, 0, &0u32.to_be_bytes()),
full_box(b"stts", 0, 0, &0u32.to_be_bytes()),
full_box(b"stsc", 0, 0, &0u32.to_be_bytes()),
full_box(b"stsz", 0, 0, &[0u8; 8]),
full_box(b"stco", 0, 0, &0u32.to_be_bytes()),
]),
);
let minf = plain_box(b"minf", &stbl);
let mdia = plain_box(b"mdia", &concat(&[mdhd, hdlr, minf]));
let trak = plain_box(b"trak", &concat(&[tkhd, mdia]));
let trex = {
let mut p = Vec::new();
p.extend_from_slice(&1u32.to_be_bytes()); p.extend_from_slice(&1u32.to_be_bytes()); p.extend_from_slice(&default_duration.to_be_bytes());
p.extend_from_slice(&0u32.to_be_bytes()); p.extend_from_slice(&default_flags.to_be_bytes());
full_box(b"trex", 0, 0, &p)
};
let mvex = plain_box(b"mvex", &trex);
plain_box(b"moov", &concat(&[mvhd, trak, mvex]))
}
fn moof(
sequence: u32,
base_decode_time: u64,
sizes: &[u32],
first_sample_flags: u32,
data_offset_to_mdat: i32,
) -> Vec<u8> {
let mfhd = full_box(b"mfhd", 0, 0, &sequence.to_be_bytes());
let tfhd = full_box(b"tfhd", 0, 0x020000, &1u32.to_be_bytes());
let tfdt = full_box(b"tfdt", 1, 0, &base_decode_time.to_be_bytes());
let trun = {
let mut p = Vec::new();
p.extend_from_slice(&(sizes.len() as u32).to_be_bytes());
p.extend_from_slice(&data_offset_to_mdat.to_be_bytes());
p.extend_from_slice(&first_sample_flags.to_be_bytes());
for s in sizes {
p.extend_from_slice(&s.to_be_bytes());
}
full_box(b"trun", 0, 0x000205, &p)
};
let traf = plain_box(b"traf", &concat(&[tfhd, tfdt, trun]));
plain_box(b"moof", &concat(&[mfhd, traf]))
}
fn build_fragmented_file() -> Vec<u8> {
let moov = fragmented_moov(100, 0x0101_0000);
let sizes1: &[u32] = &[10, 20, 30];
let sizes2: &[u32] = &[40, 50];
let m1_len = moof(1, 0, sizes1, 0x0200_0000, 0).len() as i32;
let m2_len = moof(2, 300, sizes2, 0x0200_0000, 0).len() as i32;
let moof1 = moof(1, 0, sizes1, 0x0200_0000, m1_len + 8);
let mdat1 = plain_box(b"mdat", &[0xAA; 60]); let moof2 = moof(2, 300, sizes2, 0x0200_0000, m2_len + 8);
let mdat2 = plain_box(b"mdat", &[0xBB; 90]);
concat(&[
plain_box(b"ftyp", &concat(&[b"iso5".to_vec(), vec![0u8; 4]])),
moov,
moof1,
mdat1,
moof2,
mdat2,
])
}
#[test]
fn fragmented_file_samples_extracted() {
let data = build_fragmented_file();
let tracks = track_samples_from_reader(Cursor::new(&data)).unwrap();
assert_eq!(tracks.len(), 1);
let t = &tracks[0];
assert_eq!(t.track_id, 1);
assert_eq!(t.sample_count, 5);
assert_eq!(t.samples.len(), 5);
assert!(t.samples.iter().all(|s| s.duration == 100));
let dts: Vec<u64> = t.samples.iter().map(|s| s.dts).collect();
assert_eq!(dts, vec![0, 100, 200, 300, 400]);
assert_eq!(t.duration, 500);
let sizes: Vec<u32> = t.samples.iter().map(|s| s.size).collect();
assert_eq!(sizes, vec![10, 20, 30, 40, 50]);
let sync: Vec<bool> = t.samples.iter().map(|s| s.is_sync).collect();
assert_eq!(sync, vec![true, false, false, true, false]);
let ftyp_len = 16u64;
let moov_len = fragmented_moov(100, 0x0101_0000).len() as u64;
let m1_start = ftyp_len + moov_len;
let m1_len = moof(1, 0, &[10, 20, 30], 0x0200_0000, 0).len() as u64;
let mdat1_payload = m1_start + m1_len + 8;
let offsets: Vec<u64> = t.samples.iter().map(|s| s.file_offset).collect();
assert_eq!(offsets[0], mdat1_payload);
assert_eq!(offsets[1], mdat1_payload + 10);
assert_eq!(offsets[2], mdat1_payload + 30);
let m2_start = m1_start + m1_len + 8 + 60;
let m2_len = moof(2, 300, &[40, 50], 0x0200_0000, 0).len() as u64;
let mdat2_payload = m2_start + m2_len + 8;
assert_eq!(offsets[3], mdat2_payload);
assert_eq!(offsets[4], mdat2_payload + 40);
assert!(t.samples.iter().all(|s| s.pts == s.dts));
}
#[test]
fn structured_fragment_boxes_decode() {
let data = build_fragmented_file();
let len = data.len() as u64;
let mut cur = Cursor::new(&data);
let boxes = get_boxes(&mut cur, len, true).unwrap();
let moov = boxes.iter().find(|b| b.typ == "moov").unwrap();
let mvex = moov
.children
.as_ref()
.unwrap()
.iter()
.find(|b| b.typ == "mvex")
.unwrap();
let trex = &mvex.children.as_ref().unwrap()[0];
let Some(StructuredData::TrackExtends(d)) = &trex.structured_data else {
panic!("expected structured trex");
};
assert_eq!(d.track_id, 1);
assert_eq!(d.default_sample_duration, 100);
assert_eq!(d.default_sample_flags, 0x0101_0000);
let moof = boxes.iter().find(|b| b.typ == "moof").unwrap();
let traf = moof
.children
.as_ref()
.unwrap()
.iter()
.find(|b| b.typ == "traf")
.unwrap();
let tkids = traf.children.as_ref().unwrap();
let Some(StructuredData::TrackFragmentHeader(tfhd)) = &tkids
.iter()
.find(|b| b.typ == "tfhd")
.and_then(|b| b.structured_data.as_ref())
else {
panic!("expected structured tfhd");
};
assert_eq!(tfhd.track_id, 1);
assert!(tfhd.default_base_is_moof);
assert!(!tfhd.duration_is_empty);
assert_eq!(tfhd.base_data_offset, None);
let Some(StructuredData::TrackFragmentDecodeTime(tfdt)) = &tkids
.iter()
.find(|b| b.typ == "tfdt")
.and_then(|b| b.structured_data.as_ref())
else {
panic!("expected structured tfdt");
};
assert_eq!(tfdt.base_media_decode_time, 0);
}
#[test]
fn structured_mvhd_fields() {
let data = build_fragmented_file();
let len = data.len() as u64;
let mut cur = Cursor::new(&data);
let boxes = get_boxes(&mut cur, len, true).unwrap();
let moov = boxes.iter().find(|b| b.typ == "moov").unwrap();
let mvhd = moov
.children
.as_ref()
.unwrap()
.iter()
.find(|b| b.typ == "mvhd")
.unwrap();
let Some(StructuredData::MovieHeader(d)) = &mvhd.structured_data else {
panic!("expected structured mvhd");
};
assert_eq!(d.timescale, 1000);
assert_eq!(d.duration, 0);
assert_eq!(d.rate, 1.0);
assert_eq!(d.next_track_id, 2);
assert_eq!(mvhd.decoded.as_deref(), Some("timescale=1000 duration=0"));
}
#[test]
fn real_fragmented_file_ground_truth() {
let path = concat_home("Source/mp4-fixtures/output/fragmented_fmp4.mp4");
if !std::path::Path::new(&path).exists() {
eprintln!("skipping: {path} not present");
return;
}
let tracks = mp4box::track_samples_from_path(&path).unwrap();
assert_eq!(tracks.len(), 2);
let video = tracks.iter().find(|t| t.handler_type == "vide").unwrap();
assert_eq!(video.sample_count, 300);
assert_eq!(video.samples[0].file_offset, 6311);
assert_eq!(video.samples[0].size, 41665);
assert!(video.samples[0].is_sync);
assert!(!video.samples[1].is_sync);
let audio = tracks.iter().find(|t| t.handler_type == "soun").unwrap();
assert_eq!(audio.sample_count, 432);
assert_eq!(audio.samples[0].file_offset, 6667518);
}
fn concat_home(rel: &str) -> String {
let home = std::env::var("HOME").unwrap_or_default();
format!("{home}/{rel}")
}