use std::io::{self, Write};
const FTYP: [u8; 4] = *b"ftyp";
const MOOV: [u8; 4] = *b"moov";
const MVHD: [u8; 4] = *b"mvhd";
const TRAK: [u8; 4] = *b"trak";
const TKHD: [u8; 4] = *b"tkhd";
const MDIA: [u8; 4] = *b"mdia";
const MDHD: [u8; 4] = *b"mdhd";
const HDLR: [u8; 4] = *b"hdlr";
const MINF: [u8; 4] = *b"minf";
const VMHD: [u8; 4] = *b"vmhd";
const DINF: [u8; 4] = *b"dinf";
const DREF: [u8; 4] = *b"dref";
const URL_: [u8; 4] = *b"url ";
const STBL: [u8; 4] = *b"stbl";
const STSD: [u8; 4] = *b"stsd";
const STTS: [u8; 4] = *b"stts";
const STSC: [u8; 4] = *b"stsc";
const STSZ: [u8; 4] = *b"stsz";
const STCO: [u8; 4] = *b"stco";
const MVEX: [u8; 4] = *b"mvex";
const TREX: [u8; 4] = *b"trex";
const MOVIE_TIMESCALE: u32 = 90_000;
const VIDEO_TRACK_ID: u32 = 1;
pub struct Fmp4Mux<W: Write> {
writer: W,
header_written: bool,
#[allow(dead_code)]
codec_private: Option<Vec<u8>>,
}
impl<W: Write> Fmp4Mux<W> {
pub fn new(writer: W) -> Self {
Self {
writer,
header_written: false,
codec_private: None,
}
}
pub fn set_video_codec_private(&mut self, hvcc: Vec<u8>) {
self.codec_private = Some(hvcc);
}
pub fn write_init_segment(&mut self) -> io::Result<()> {
if self.header_written {
return Ok(());
}
let ftyp = build_ftyp();
let moov = build_moov();
self.writer.write_all(&ftyp)?;
self.writer.write_all(&moov)?;
self.header_written = true;
Ok(())
}
pub fn write_video(&mut self, _pts_ns: i64, _keyframe: bool, _data: &[u8]) -> io::Result<()> {
self.write_init_segment()?;
Err(crate::error::Error::Fmp4Unimplemented.into())
}
pub fn finish(&mut self) -> io::Result<()> {
self.writer.flush()
}
}
fn build_ftyp() -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(b"iso6");
body.extend_from_slice(&1u32.to_be_bytes());
body.extend_from_slice(b"iso6");
body.extend_from_slice(b"dash");
body.extend_from_slice(b"msdh");
body.extend_from_slice(b"hvc1");
wrap_box(&FTYP, &body)
}
fn build_moov() -> Vec<u8> {
let mvhd = build_mvhd();
let trak = build_video_trak();
let mvex = build_mvex();
let mut body = Vec::new();
body.extend_from_slice(&mvhd);
body.extend_from_slice(&trak);
body.extend_from_slice(&mvex);
wrap_box(&MOOV, &body)
}
fn build_mvhd() -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&MOVIE_TIMESCALE.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&0x0001_0000u32.to_be_bytes()); body.extend_from_slice(&0x0100u16.to_be_bytes()); body.extend_from_slice(&[0u8; 2]); body.extend_from_slice(&[0u8; 8]); for v in [0x1_0000u32, 0, 0, 0, 0x1_0000, 0, 0, 0, 0x4000_0000] {
body.extend_from_slice(&v.to_be_bytes());
}
body.extend_from_slice(&[0u8; 24]); body.extend_from_slice(&2u32.to_be_bytes()); wrap_box(&MVHD, &body)
}
fn build_video_trak() -> Vec<u8> {
let tkhd = build_tkhd();
let mdia = build_mdia();
let mut body = Vec::new();
body.extend_from_slice(&tkhd);
body.extend_from_slice(&mdia);
wrap_box(&TRAK, &body)
}
fn build_tkhd() -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0, 7]);
body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&VIDEO_TRACK_ID.to_be_bytes());
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&[0u8; 8]); body.extend_from_slice(&0u16.to_be_bytes()); body.extend_from_slice(&0u16.to_be_bytes()); body.extend_from_slice(&0u16.to_be_bytes()); body.extend_from_slice(&[0u8; 2]); for v in [0x1_0000u32, 0, 0, 0, 0x1_0000, 0, 0, 0, 0x4000_0000] {
body.extend_from_slice(&v.to_be_bytes());
}
body.extend_from_slice(&(1920u32 << 16).to_be_bytes());
body.extend_from_slice(&(1080u32 << 16).to_be_bytes());
wrap_box(&TKHD, &body)
}
fn build_mdia() -> Vec<u8> {
let mdhd = build_mdhd();
let hdlr = build_hdlr_vide();
let minf = build_minf();
let mut body = Vec::new();
body.extend_from_slice(&mdhd);
body.extend_from_slice(&hdlr);
body.extend_from_slice(&minf);
wrap_box(&MDIA, &body)
}
fn build_mdhd() -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&MOVIE_TIMESCALE.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&[0x55, 0xC4]);
body.extend_from_slice(&0u16.to_be_bytes()); wrap_box(&MDHD, &body)
}
fn build_hdlr_vide() -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(b"vide");
body.extend_from_slice(&[0u8; 12]); body.extend_from_slice(b"VideoHandler\0");
wrap_box(&HDLR, &body)
}
fn build_minf() -> Vec<u8> {
let vmhd = build_vmhd();
let dinf = build_dinf();
let stbl = build_stbl();
let mut body = Vec::new();
body.extend_from_slice(&vmhd);
body.extend_from_slice(&dinf);
body.extend_from_slice(&stbl);
wrap_box(&MINF, &body)
}
fn build_vmhd() -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0, 1]); body.extend_from_slice(&0u16.to_be_bytes()); body.extend_from_slice(&[0u8; 6]); wrap_box(&VMHD, &body)
}
fn build_dinf() -> Vec<u8> {
let mut dref_body = Vec::new();
dref_body.extend_from_slice(&[0, 0, 0, 0]);
dref_body.extend_from_slice(&1u32.to_be_bytes()); let url_body = [0u8, 0, 0, 1];
dref_body.extend_from_slice(&wrap_box(&URL_, &url_body));
let dref = wrap_box(&DREF, &dref_body);
wrap_box(&DINF, &dref)
}
fn build_stbl() -> Vec<u8> {
let mut stsd_body = Vec::new();
stsd_body.extend_from_slice(&[0, 0, 0, 0]);
stsd_body.extend_from_slice(&0u32.to_be_bytes()); let stsd = wrap_box(&STSD, &stsd_body);
let stts = wrap_box(&STTS, &[0, 0, 0, 0, 0, 0, 0, 0]); let stsc = wrap_box(&STSC, &[0, 0, 0, 0, 0, 0, 0, 0]);
let stsz = wrap_box(
&STSZ,
&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], );
let stco = wrap_box(&STCO, &[0, 0, 0, 0, 0, 0, 0, 0]);
let mut body = Vec::new();
body.extend_from_slice(&stsd);
body.extend_from_slice(&stts);
body.extend_from_slice(&stsc);
body.extend_from_slice(&stsz);
body.extend_from_slice(&stco);
wrap_box(&STBL, &body)
}
fn build_mvex() -> Vec<u8> {
let mut trex_body = Vec::new();
trex_body.extend_from_slice(&[0, 0, 0, 0]); trex_body.extend_from_slice(&VIDEO_TRACK_ID.to_be_bytes());
trex_body.extend_from_slice(&1u32.to_be_bytes()); trex_body.extend_from_slice(&0u32.to_be_bytes()); trex_body.extend_from_slice(&0u32.to_be_bytes()); trex_body.extend_from_slice(&0u32.to_be_bytes()); let trex = wrap_box(&TREX, &trex_body);
wrap_box(&MVEX, &trex)
}
fn wrap_box(box_type: &[u8; 4], body: &[u8]) -> Vec<u8> {
let total = body.len() + 8;
debug_assert!(total <= u32::MAX as usize, "fMP4 box exceeds u32 size");
let size = u32::try_from(total).unwrap_or(u32::MAX);
let mut out = Vec::with_capacity(total);
out.extend_from_slice(&size.to_be_bytes());
out.extend_from_slice(box_type);
out.extend_from_slice(body);
out
}
#[cfg(test)]
mod tests {
use super::*;
fn read_box_header(buf: &[u8]) -> (u32, [u8; 4]) {
let size = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
let bt = [buf[4], buf[5], buf[6], buf[7]];
(size, bt)
}
#[test]
fn init_segment_starts_with_ftyp_then_moov() {
let mut sink: Vec<u8> = Vec::new();
let mut mux = Fmp4Mux::new(&mut sink);
mux.write_init_segment().unwrap();
mux.finish().unwrap();
drop(mux);
let (ftyp_size, ftyp_type) = read_box_header(&sink);
assert_eq!(&ftyp_type, b"ftyp");
assert!(ftyp_size >= 24, "ftyp too small: {ftyp_size}");
let (moov_size, moov_type) = read_box_header(&sink[ftyp_size as usize..]);
assert_eq!(&moov_type, b"moov");
assert!(moov_size > 100, "moov skeleton too small: {moov_size}");
let total = ftyp_size as usize + moov_size as usize;
assert_eq!(sink.len(), total, "stub leaked media bytes past moov");
}
#[test]
fn write_video_reports_unimplemented_and_buffers_nothing() {
let mut sink: Vec<u8> = Vec::new();
let mut mux = Fmp4Mux::new(&mut sink);
let err = mux.write_video(0, true, &[0xDE; 4096]).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::Unsupported);
mux.finish().unwrap();
drop(mux);
let (ftyp_size, _) = read_box_header(&sink);
let (moov_size, _) = read_box_header(&sink[ftyp_size as usize..]);
assert_eq!(sink.len(), ftyp_size as usize + moov_size as usize);
}
#[test]
fn moov_contains_trak_mvex() {
let mut buf: Vec<u8> = Vec::new();
let mut mux2 = Fmp4Mux::new(&mut buf);
mux2.write_init_segment().unwrap();
mux2.finish().unwrap();
drop(mux2);
let (ftyp_size, _) = read_box_header(&buf);
let moov_start = ftyp_size as usize;
let (moov_size, _) = read_box_header(&buf[moov_start..]);
let moov_payload = &buf[moov_start + 8..moov_start + moov_size as usize];
let has_trak = moov_payload.windows(4).any(|w| w == b"trak");
let has_mvex = moov_payload.windows(4).any(|w| w == b"mvex");
assert!(has_trak, "moov missing trak");
assert!(has_mvex, "moov missing mvex");
}
fn walk_boxes(buf: &[u8]) -> Vec<([u8; 4], usize, usize)> {
let mut out = Vec::new();
let mut pos = 0;
while pos + 8 <= buf.len() {
let (size, bt) = read_box_header(&buf[pos..]);
let size = size as usize;
assert!(size >= 8, "box {bt:?} size {size} < 8-byte header");
assert!(
pos + size <= buf.len(),
"box {bt:?} at {pos} size {size} overruns buffer {}",
buf.len()
);
out.push((bt, pos, size));
pos += size;
}
assert_eq!(pos, buf.len(), "boxes did not tile the buffer exactly");
out
}
fn child<'a>(container_payload: &'a [u8], want: &[u8; 4]) -> Option<&'a [u8]> {
let mut pos = 0;
while pos + 8 <= container_payload.len() {
let (size, bt) = read_box_header(&container_payload[pos..]);
let size = size as usize;
if size < 8 || pos + size > container_payload.len() {
return None;
}
if &bt == want {
return Some(&container_payload[pos..pos + size]);
}
pos += size;
}
None
}
fn init_segment() -> Vec<u8> {
let mut buf: Vec<u8> = Vec::new();
let mut mux = Fmp4Mux::new(&mut buf);
mux.write_init_segment().unwrap();
mux.finish().unwrap();
drop(mux);
buf
}
#[test]
fn init_segment_box_sizes_tile_exactly() {
let buf = init_segment();
let boxes = walk_boxes(&buf);
let types: Vec<[u8; 4]> = boxes.iter().map(|(t, _, _)| *t).collect();
assert_eq!(types, vec![*b"ftyp", *b"moov"]);
}
#[test]
fn ftyp_major_brand_and_compatible_brands() {
let buf = init_segment();
let (ftyp_size, _) = read_box_header(&buf);
let body = &buf[8..ftyp_size as usize];
assert_eq!(&body[0..4], b"iso6", "major_brand");
assert_eq!(
u32::from_be_bytes([body[4], body[5], body[6], body[7]]),
1,
"minor_version"
);
let brands = &body[8..];
assert_eq!(brands.len() % 4, 0, "compatible_brands must be 4-byte each");
let set: Vec<&[u8]> = brands.chunks(4).collect();
assert!(set.contains(&&b"iso6"[..]));
assert!(set.contains(&&b"dash"[..]));
assert!(set.contains(&&b"msdh"[..]));
assert!(set.contains(&&b"hvc1"[..]), "HEVC brand required for hvc1");
}
#[test]
fn moov_child_order_is_mvhd_trak_mvex() {
let buf = init_segment();
let boxes = walk_boxes(&buf);
let (_, moov_start, moov_size) = boxes.iter().find(|(t, _, _)| t == b"moov").unwrap();
let moov_payload = &buf[moov_start + 8..moov_start + moov_size];
let children = walk_boxes(moov_payload);
let types: Vec<[u8; 4]> = children.iter().map(|(t, _, _)| *t).collect();
assert_eq!(types, vec![*b"mvhd", *b"trak", *b"mvex"]);
}
#[test]
fn mvhd_timescale_and_next_track_id() {
let buf = init_segment();
let boxes = walk_boxes(&buf);
let (_, moov_start, moov_size) = boxes.iter().find(|(t, _, _)| t == b"moov").unwrap();
let moov_payload = &buf[moov_start + 8..moov_start + moov_size];
let mvhd = child(moov_payload, b"mvhd").expect("mvhd present");
let body = &mvhd[8..]; assert_eq!(&body[0..4], &[0, 0, 0, 0], "mvhd version 0, flags 0");
let timescale = u32::from_be_bytes([body[12], body[13], body[14], body[15]]);
assert_eq!(timescale, MOVIE_TIMESCALE);
assert_eq!(timescale, 90_000, "spec-fixed default timescale");
let n = body.len();
let next_id = u32::from_be_bytes([body[n - 4], body[n - 3], body[n - 2], body[n - 1]]);
assert_eq!(next_id, 2, "next_track_ID must exceed the sole track ID");
}
#[test]
fn trex_references_video_track_id() {
let buf = init_segment();
let boxes = walk_boxes(&buf);
let (_, moov_start, moov_size) = boxes.iter().find(|(t, _, _)| t == b"moov").unwrap();
let moov_payload = &buf[moov_start + 8..moov_start + moov_size];
let mvex = child(moov_payload, b"mvex").expect("mvex");
let trex = child(&mvex[8..], b"trex").expect("trex");
let body = &trex[8..];
let track_id = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
assert_eq!(track_id, VIDEO_TRACK_ID);
assert_eq!(track_id, 1);
let dsdi = u32::from_be_bytes([body[8], body[9], body[10], body[11]]);
assert_eq!(dsdi, 1);
}
#[test]
fn tkhd_track_id_matches_trex() {
let buf = init_segment();
let boxes = walk_boxes(&buf);
let (_, moov_start, moov_size) = boxes.iter().find(|(t, _, _)| t == b"moov").unwrap();
let moov_payload = &buf[moov_start + 8..moov_start + moov_size];
let trak = child(moov_payload, b"trak").expect("trak");
let tkhd = child(&trak[8..], b"tkhd").expect("tkhd");
let body = &tkhd[8..];
let track_id = u32::from_be_bytes([body[12], body[13], body[14], body[15]]);
assert_eq!(track_id, VIDEO_TRACK_ID, "tkhd track_ID must match trex");
assert_eq!(&body[0..4], &[0, 0, 0, 7]);
}
#[test]
fn stbl_present_with_empty_sample_tables() {
let buf = init_segment();
let boxes = walk_boxes(&buf);
let (_, moov_start, moov_size) = boxes.iter().find(|(t, _, _)| t == b"moov").unwrap();
let moov_payload = &buf[moov_start + 8..moov_start + moov_size];
let trak = child(moov_payload, b"trak").expect("trak");
let mdia = child(&trak[8..], b"mdia").expect("mdia");
let minf = child(&mdia[8..], b"minf").expect("minf");
let stbl = child(&minf[8..], b"stbl").expect("stbl");
let stsd = child(&stbl[8..], b"stsd").expect("stsd");
let body = &stsd[8..];
let entry_count = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
assert_eq!(entry_count, 0, "stub stsd has no sample entries yet");
for fourcc in [b"stts", b"stsc", b"stsz", b"stco"] {
assert!(
child(&stbl[8..], fourcc).is_some(),
"stbl missing {:?}",
std::str::from_utf8(fourcc).unwrap()
);
}
}
#[test]
fn hdlr_declares_video_handler() {
let buf = init_segment();
let boxes = walk_boxes(&buf);
let (_, moov_start, moov_size) = boxes.iter().find(|(t, _, _)| t == b"moov").unwrap();
let moov_payload = &buf[moov_start + 8..moov_start + moov_size];
let trak = child(moov_payload, b"trak").expect("trak");
let mdia = child(&trak[8..], b"mdia").expect("mdia");
let hdlr = child(&mdia[8..], b"hdlr").expect("hdlr");
let body = &hdlr[8..];
assert_eq!(&body[8..12], b"vide", "handler_type must be 'vide'");
}
#[test]
fn wrap_box_size_includes_header() {
let body = [0xAAu8; 13];
let boxed = wrap_box(b"test", &body);
assert_eq!(boxed.len(), 13 + 8);
let (size, bt) = read_box_header(&boxed);
assert_eq!(size as usize, 13 + 8, "size must include the 8-byte header");
assert_eq!(&bt, b"test");
assert_eq!(&boxed[8..], &body);
let empty = wrap_box(b"free", &[]);
assert_eq!(empty.len(), 8);
assert_eq!(
u32::from_be_bytes([empty[0], empty[1], empty[2], empty[3]]),
8
);
}
#[test]
fn write_init_segment_is_idempotent() {
let mut buf: Vec<u8> = Vec::new();
let mut mux = Fmp4Mux::new(&mut buf);
mux.write_init_segment().unwrap();
mux.write_init_segment().unwrap(); mux.finish().unwrap();
drop(mux);
let boxes = walk_boxes(&buf);
let ftyp_count = boxes.iter().filter(|(t, _, _)| t == b"ftyp").count();
let moov_count = boxes.iter().filter(|(t, _, _)| t == b"moov").count();
assert_eq!(ftyp_count, 1, "second write_init_segment must be a no-op");
assert_eq!(moov_count, 1);
}
#[test]
fn write_video_after_init_still_unimplemented_and_no_media() {
let mut buf: Vec<u8> = Vec::new();
let mut mux = Fmp4Mux::new(&mut buf);
mux.write_init_segment().unwrap();
let err = mux.write_video(0, true, &[0u8; 8]).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::Unsupported);
mux.finish().unwrap();
drop(mux);
let boxes = walk_boxes(&buf);
let types: Vec<[u8; 4]> = boxes.iter().map(|(t, _, _)| *t).collect();
assert_eq!(types, vec![*b"ftyp", *b"moov"]);
}
}