use std::io::Cursor;
use std::sync::atomic::{AtomicU32, Ordering};
use oxideav_core::{
CodecId, CodecParameters, Packet, ReadSeek, SampleFormat, StreamInfo, TimeBase, WriteSeek,
};
use oxideav_mp4::muxer::open_with_options;
use oxideav_mp4::options::{BrandPreset, FragmentCadence, FragmentedOptions, Mp4MuxerOptions};
static NEXT_TMP: AtomicU32 = AtomicU32::new(0);
fn child_box(fourcc: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let size = 8 + payload.len();
let mut v = Vec::with_capacity(size);
v.extend_from_slice(&(size as u32).to_be_bytes());
v.extend_from_slice(fourcc);
v.extend_from_slice(payload);
v
}
fn build_trep_box(track_id: u32, children: &[Vec<u8>]) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&track_id.to_be_bytes());
for c in children {
body.extend_from_slice(c);
}
let total = 8 + body.len();
let mut out = Vec::with_capacity(total);
out.extend_from_slice(&(total as u32).to_be_bytes());
out.extend_from_slice(b"trep");
out.extend_from_slice(&body);
out
}
fn find_fourcc(bytes: &[u8], tag: &[u8; 4]) -> Option<usize> {
bytes.windows(4).position(|w| w == &tag[..])
}
fn splice_into_mvex(file: &[u8], box_bytes: &[u8]) -> Vec<u8> {
let moov_type_pos = find_fourcc(file, b"moov").expect("moov FourCC present");
let mvex_type_pos = find_fourcc(file, b"mvex").expect("mvex FourCC present");
assert!(
mvex_type_pos > moov_type_pos,
"mvex sits inside moov in our fragmented muxer output",
);
let moov_size_pos = moov_type_pos - 4;
let mvex_size_pos = mvex_type_pos - 4;
let moov_size = u32::from_be_bytes([
file[moov_size_pos],
file[moov_size_pos + 1],
file[moov_size_pos + 2],
file[moov_size_pos + 3],
]) as usize;
let mvex_size = u32::from_be_bytes([
file[mvex_size_pos],
file[mvex_size_pos + 1],
file[mvex_size_pos + 2],
file[mvex_size_pos + 3],
]) as usize;
let mvex_end = mvex_size_pos + mvex_size;
let moov_end = moov_size_pos + moov_size;
assert!(
mvex_end <= moov_end,
"mvex end {mvex_end} must lie within moov end {moov_end}"
);
let delta = box_bytes.len();
let new_mvex_size = (mvex_size + delta) as u32;
let new_moov_size = (moov_size + delta) as u32;
let mut out = Vec::with_capacity(file.len() + delta);
out.extend_from_slice(&file[..mvex_end]);
out.extend_from_slice(box_bytes);
out.extend_from_slice(&file[mvex_end..]);
out[mvex_size_pos..mvex_size_pos + 4].copy_from_slice(&new_mvex_size.to_be_bytes());
out[moov_size_pos..moov_size_pos + 4].copy_from_slice(&new_moov_size.to_be_bytes());
out
}
fn pcm_stream_info() -> StreamInfo {
let mut params = CodecParameters::audio(CodecId::new("pcm_s16le"));
params.channels = Some(2);
params.sample_rate = Some(48_000);
params.sample_format = Some(SampleFormat::S16);
StreamInfo {
index: 0,
time_base: TimeBase::new(1, 48_000),
duration: None,
start_time: Some(0),
params,
}
}
fn fragmented_options() -> Mp4MuxerOptions {
Mp4MuxerOptions {
brand: BrandPreset::Custom {
major: *b"iso6",
compatible: vec![*b"iso6", *b"mp41", *b"dash"],
},
faststart: false,
fragmented: Some(FragmentedOptions {
cadence: FragmentCadence::EveryNPackets(1),
styp: None,
emit_random_access_indexes: false,
}),
write_edit_list: false,
track_sample_groups: Vec::new(),
}
}
fn make_pcm_payload(samples: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(samples * 4);
for i in 0..samples {
let l = (i as i16).wrapping_mul(3);
let r = (i as i16).wrapping_mul(5);
out.extend_from_slice(&l.to_le_bytes());
out.extend_from_slice(&r.to_le_bytes());
}
out
}
fn mux_fragmented_pcm_to_bytes() -> Vec<u8> {
let stream = pcm_stream_info();
let frames_per_packet: i64 = 512;
let tmp = std::env::temp_dir().join(format!(
"oxideav-mp4-trep-r291-{}-{}.mp4",
std::process::id(),
NEXT_TMP.fetch_add(1, Ordering::Relaxed),
));
{
let f = std::fs::File::create(&tmp).unwrap();
let ws: Box<dyn WriteSeek> = Box::new(f);
let opts = fragmented_options();
let mut mux =
open_with_options(ws, std::slice::from_ref(&stream), opts).expect("open muxer");
mux.write_header().unwrap();
for i in 0..2 {
let mut pkt = Packet::new(
0,
stream.time_base,
make_pcm_payload(frames_per_packet as usize),
);
pkt.pts = Some(i * frames_per_packet);
pkt.duration = Some(frames_per_packet);
pkt.flags.keyframe = true;
mux.write_packet(&pkt).unwrap();
}
mux.write_trailer().unwrap();
}
let bytes = std::fs::read(&tmp).unwrap();
let _ = std::fs::remove_file(&tmp);
bytes
}
#[test]
fn trep_inside_mvex_surfaces_on_metadata() {
let assp = child_box(b"assp", &[0xaa; 8]);
let unkn = child_box(b"xtra", &[]);
let trep = build_trep_box(1, &[assp, unkn]);
let spliced = splice_into_mvex(&mux_fragmented_pcm_to_bytes(), &trep);
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(spliced));
let dmx = oxideav_mp4::demux::open(rs, &oxideav_core::NullCodecResolver).expect("demux opens");
let md = dmx.metadata();
let get = |k: &str| md.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone());
assert_eq!(
get("trep_0"),
Some("1 children=2 assp xtra".to_string()),
"trep_0 must carry track_id, child count, and child fourccs in order",
);
assert!(
get("trep_1").is_none(),
"only one trep box present — no trep_1 key expected",
);
}
#[test]
fn multiple_trep_boxes_each_surface_in_order() {
let trep_a = build_trep_box(1, &[child_box(b"assp", &[0; 4])]);
let trep_b = build_trep_box(2, &[]);
let mut both = trep_a.clone();
both.extend_from_slice(&trep_b);
let spliced = splice_into_mvex(&mux_fragmented_pcm_to_bytes(), &both);
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(spliced));
let dmx = oxideav_mp4::demux::open(rs, &oxideav_core::NullCodecResolver).expect("demux opens");
let md = dmx.metadata();
let get = |k: &str| md.iter().find(|(kk, _)| kk == k).map(|(_, v)| v.clone());
assert_eq!(get("trep_0"), Some("1 children=1 assp".to_string()));
assert_eq!(get("trep_1"), Some("2 children=0".to_string()));
}
#[test]
fn trep_absent_emits_no_trep_keys() {
let bytes = mux_fragmented_pcm_to_bytes();
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(bytes));
let dmx = oxideav_mp4::demux::open(rs, &oxideav_core::NullCodecResolver).expect("demux opens");
let md = dmx.metadata();
for (k, _) in md {
assert!(
!k.starts_with("trep"),
"unexpected trep metadata key {k} on a file with no trep box",
);
}
}
#[test]
fn trep_malformed_body_is_dropped_silently() {
let mut bad_body = Vec::new();
bad_body.extend_from_slice(&[0u8; 4]); bad_body.extend_from_slice(&[0u8; 2]); let total = 8 + bad_body.len();
let mut trep_box = Vec::with_capacity(total);
trep_box.extend_from_slice(&(total as u32).to_be_bytes());
trep_box.extend_from_slice(b"trep");
trep_box.extend_from_slice(&bad_body);
let spliced = splice_into_mvex(&mux_fragmented_pcm_to_bytes(), &trep_box);
let rs: Box<dyn ReadSeek> = Box::new(Cursor::new(spliced));
let dmx = oxideav_mp4::demux::open(rs, &oxideav_core::NullCodecResolver)
.expect("malformed trep must not abort open");
let md = dmx.metadata();
for (k, _) in md {
assert!(
!k.starts_with("trep"),
"malformed trep was silently dropped — no trep_* keys expected, saw {k}",
);
}
}
#[test]
fn trep_public_entry_point_parses_standalone_body() {
let trep_full = build_trep_box(77, &[child_box(b"assp", &[1, 2, 3, 4, 5, 6])]);
let body = &trep_full[8..];
let record = oxideav_mp4::demux::parse_trep_box(body).expect("standalone trep body parses");
assert_eq!(record.track_id, 77);
assert_eq!(record.children.len(), 1);
assert_eq!(&record.children[0].fourcc, b"assp");
assert_eq!(record.children[0].payload_len, 6);
}