#![forbid(unsafe_code)]
use std::io::{self, Write};
use crate::track_info::{TrackInfo, TrackType};
#[derive(Debug, thiserror::Error)]
pub enum BasicMp4Error {
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("invalid brand '{0}': must be exactly 4 ASCII bytes")]
InvalidBrand(String),
}
pub struct BasicMp4Muxer {
output: Box<dyn Write>,
}
impl BasicMp4Muxer {
pub fn new(output: Box<dyn Write>) -> Self {
Self { output }
}
pub fn write_ftyp(&mut self, brand: &str) -> Result<(), BasicMp4Error> {
let brand_bytes = validate_brand(brand)?;
let payload_size: u32 = 4 + 4 + 4; let box_size: u32 = 8 + payload_size;
write_box_header(&mut self.output, box_size, b"ftyp")?;
self.output.write_all(&brand_bytes)?; self.output.write_all(&0u32.to_be_bytes())?; self.output.write_all(&brand_bytes)?; Ok(())
}
pub fn write_mdat(&mut self, data: &[u8]) -> Result<(), BasicMp4Error> {
let box_size = 8u32.saturating_add(u32::try_from(data.len()).unwrap_or(u32::MAX));
write_box_header(&mut self.output, box_size, b"mdat")?;
self.output.write_all(data)?;
Ok(())
}
pub fn write_moov(&mut self, tracks: &[TrackInfo]) -> Result<(), BasicMp4Error> {
let moov_payload = build_moov_payload(tracks);
write_box_header(&mut self.output, 8 + moov_payload.len() as u32, b"moov")?;
self.output.write_all(&moov_payload)?;
Ok(())
}
}
fn write_box_header(w: &mut dyn Write, size: u32, box_type: &[u8; 4]) -> io::Result<()> {
w.write_all(&size.to_be_bytes())?;
w.write_all(box_type)?;
Ok(())
}
fn encode_box(box_type: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let total = 8u32.saturating_add(payload.len() as u32);
let mut out = Vec::with_capacity(total as usize);
out.extend_from_slice(&total.to_be_bytes());
out.extend_from_slice(box_type);
out.extend_from_slice(payload);
out
}
fn encode_full_box(box_type: &[u8; 4], version: u8, flags: u32, payload: &[u8]) -> Vec<u8> {
let mut full_payload = Vec::with_capacity(4 + payload.len());
full_payload.push(version);
full_payload.push(((flags >> 16) & 0xFF) as u8);
full_payload.push(((flags >> 8) & 0xFF) as u8);
full_payload.push((flags & 0xFF) as u8);
full_payload.extend_from_slice(payload);
encode_box(box_type, &full_payload)
}
fn validate_brand(brand: &str) -> Result<[u8; 4], BasicMp4Error> {
if brand.len() != 4 || !brand.is_ascii() {
return Err(BasicMp4Error::InvalidBrand(brand.to_owned()));
}
let b = brand.as_bytes();
Ok([b[0], b[1], b[2], b[3]])
}
fn build_moov_payload(tracks: &[TrackInfo]) -> Vec<u8> {
let mut payload = Vec::new();
payload.extend(build_mvhd());
for (i, track) in tracks.iter().enumerate() {
payload.extend(build_trak(track, (i + 1) as u32));
}
payload
}
fn build_mvhd() -> Vec<u8> {
let timescale: u32 = 1000;
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(×cale.to_be_bytes()); payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(&0x00010000u32.to_be_bytes()); payload.extend_from_slice(&0x0100u16.to_be_bytes()); payload.extend_from_slice(&[0u8; 10]); let matrix: [u32; 9] = [0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000];
for v in &matrix {
payload.extend_from_slice(&v.to_be_bytes());
}
payload.extend_from_slice(&[0u8; 24]); payload.extend_from_slice(&0xFFFF_FFFFu32.to_be_bytes()); encode_full_box(b"mvhd", 0, 0, &payload)
}
fn build_trak(track: &TrackInfo, track_id: u32) -> Vec<u8> {
let mut payload = Vec::new();
payload.extend(build_tkhd(track_id, &track.track_type));
payload.extend(build_mdia(track));
encode_box(b"trak", &payload)
}
fn build_tkhd(track_id: u32, _track_type: &TrackType) -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(&track_id.to_be_bytes()); payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(&[0u8; 8]); payload.extend_from_slice(&0i16.to_be_bytes()); payload.extend_from_slice(&0i16.to_be_bytes()); payload.extend_from_slice(&0x0100u16.to_be_bytes()); payload.extend_from_slice(&0u16.to_be_bytes()); let matrix: [u32; 9] = [0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000];
for v in &matrix {
payload.extend_from_slice(&v.to_be_bytes());
}
payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(&0u32.to_be_bytes()); encode_full_box(b"tkhd", 0, 3, &payload)
}
fn build_mdia(track: &TrackInfo) -> Vec<u8> {
let mut payload = Vec::new();
payload.extend(build_mdhd());
payload.extend(build_hdlr(&track.track_type));
payload.extend(build_minf(&track.track_type));
encode_box(b"mdia", &payload)
}
fn build_mdhd() -> Vec<u8> {
let timescale: u32 = 1000;
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(×cale.to_be_bytes()); payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(&0x55C4u16.to_be_bytes());
payload.extend_from_slice(&0u16.to_be_bytes()); encode_full_box(b"mdhd", 0, 0, &payload)
}
fn build_hdlr(track_type: &TrackType) -> Vec<u8> {
let (handler_type, name): (&[u8; 4], &str) = match track_type {
TrackType::Video => (b"vide", "OxiMedia Video Handler"),
TrackType::Audio => (b"soun", "OxiMedia Audio Handler"),
TrackType::Subtitle => (b"text", "OxiMedia Text Handler"),
TrackType::Data => (b"data", "OxiMedia Data Handler"),
};
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes()); payload.extend_from_slice(handler_type); payload.extend_from_slice(&[0u8; 12]); payload.extend_from_slice(name.as_bytes()); payload.push(0u8); encode_full_box(b"hdlr", 0, 0, &payload)
}
fn build_minf(track_type: &TrackType) -> Vec<u8> {
let mut payload = Vec::new();
match track_type {
TrackType::Video => {
let vmhd_payload = [0u8; 8]; payload.extend(encode_full_box(b"vmhd", 0, 1, &vmhd_payload));
}
TrackType::Audio => {
let smhd_payload = [0u8; 4]; payload.extend(encode_full_box(b"smhd", 0, 0, &smhd_payload));
}
_ => {
payload.extend(encode_full_box(b"nmhd", 0, 0, &[]));
}
}
payload.extend(build_dinf());
payload.extend(build_stbl());
encode_box(b"minf", &payload)
}
fn build_dinf() -> Vec<u8> {
let mut dref_payload = Vec::new();
dref_payload.extend_from_slice(&1u32.to_be_bytes()); let url_entry = encode_full_box(b"url ", 0, 1, &[]);
dref_payload.extend(url_entry);
let dref = encode_full_box(b"dref", 0, 0, &dref_payload);
encode_box(b"dinf", &dref)
}
fn build_stbl() -> Vec<u8> {
let mut payload = Vec::new();
let mut stsd_p = Vec::new();
stsd_p.extend_from_slice(&0u32.to_be_bytes()); payload.extend(encode_full_box(b"stsd", 0, 0, &stsd_p));
let mut stts_p = Vec::new();
stts_p.extend_from_slice(&0u32.to_be_bytes()); payload.extend(encode_full_box(b"stts", 0, 0, &stts_p));
let mut stsc_p = Vec::new();
stsc_p.extend_from_slice(&0u32.to_be_bytes()); payload.extend(encode_full_box(b"stsc", 0, 0, &stsc_p));
let mut stsz_p = Vec::new();
stsz_p.extend_from_slice(&0u32.to_be_bytes()); stsz_p.extend_from_slice(&0u32.to_be_bytes()); payload.extend(encode_full_box(b"stsz", 0, 0, &stsz_p));
let mut stco_p = Vec::new();
stco_p.extend_from_slice(&0u32.to_be_bytes()); payload.extend(encode_full_box(b"stco", 0, 0, &stco_p));
encode_box(b"stbl", &payload)
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use super::*;
#[derive(Clone)]
struct SharedVec(Arc<Mutex<Vec<u8>>>);
impl SharedVec {
fn new() -> Self {
Self(Arc::new(Mutex::new(Vec::new())))
}
fn take(&self) -> Vec<u8> {
self.0.lock().expect("lock ok").clone()
}
}
impl Write for SharedVec {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0
.lock()
.map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "lock poisoned"))?
.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn capture(f: impl FnOnce(&mut BasicMp4Muxer)) -> Vec<u8> {
let sv = SharedVec::new();
let mut muxer = BasicMp4Muxer::new(Box::new(sv.clone()));
f(&mut muxer);
sv.take()
}
fn read_u32_be(buf: &[u8], offset: usize) -> u32 {
u32::from_be_bytes([
buf[offset],
buf[offset + 1],
buf[offset + 2],
buf[offset + 3],
])
}
fn find_box(buf: &[u8], box_type: &[u8; 4]) -> bool {
buf.windows(4).any(|w| w == box_type)
}
#[test]
fn test_write_ftyp_valid() {
let out = capture(|m| {
m.write_ftyp("isom").expect("should succeed");
});
assert_eq!(out.len(), 20);
assert_eq!(read_u32_be(&out, 0), 20);
assert_eq!(&out[4..8], b"ftyp");
assert_eq!(&out[8..12], b"isom"); }
#[test]
fn test_write_ftyp_invalid_brand_short() {
let sv = SharedVec::new();
let mut muxer = BasicMp4Muxer::new(Box::new(sv));
assert!(muxer.write_ftyp("iso").is_err());
}
#[test]
fn test_write_ftyp_invalid_brand_long() {
let sv = SharedVec::new();
let mut muxer = BasicMp4Muxer::new(Box::new(sv));
assert!(muxer.write_ftyp("isom2").is_err());
}
#[test]
fn test_write_mdat_basic() {
let payload: &[u8] = b"hello world";
let out = capture(|m| {
m.write_mdat(payload).expect("should succeed");
});
let expected_size = 8 + payload.len() as u32;
assert_eq!(read_u32_be(&out, 0), expected_size);
assert_eq!(&out[4..8], b"mdat");
assert_eq!(&out[8..], payload);
}
#[test]
fn test_write_mdat_empty() {
let out = capture(|m| {
m.write_mdat(&[]).expect("should succeed");
});
assert_eq!(read_u32_be(&out, 0), 8);
assert_eq!(&out[4..8], b"mdat");
}
#[test]
fn test_write_moov_contains_mvhd() {
let tracks = vec![TrackInfo::new(0, TrackType::Video, "av01")];
let out = capture(|m| {
m.write_moov(&tracks).expect("should succeed");
});
assert_eq!(&out[4..8], b"moov");
assert!(find_box(&out, b"mvhd"));
}
#[test]
fn test_write_moov_video_track() {
let tracks = vec![TrackInfo::new(0, TrackType::Video, "av01")];
let out = capture(|m| {
m.write_moov(&tracks).expect("should succeed");
});
assert!(find_box(&out, b"trak"));
assert!(find_box(&out, b"tkhd"));
assert!(find_box(&out, b"mdia"));
assert!(find_box(&out, b"hdlr"));
assert!(find_box(&out, b"minf"));
assert!(find_box(&out, b"vmhd"));
assert!(find_box(&out, b"stbl"));
}
#[test]
fn test_write_moov_audio_track() {
let tracks = vec![TrackInfo::new(0, TrackType::Audio, "Opus")];
let out = capture(|m| {
m.write_moov(&tracks).expect("should succeed");
});
assert!(find_box(&out, b"smhd"));
}
#[test]
fn test_write_moov_empty_tracks() {
let out = capture(|m| {
m.write_moov(&[]).expect("should succeed");
});
assert_eq!(&out[4..8], b"moov");
assert!(find_box(&out, b"mvhd"));
}
#[test]
fn test_write_moov_size_consistent() {
let tracks = vec![
TrackInfo::new(0, TrackType::Video, "av01"),
TrackInfo::new(1, TrackType::Audio, "Opus"),
];
let out = capture(|m| {
m.write_moov(&tracks).expect("should succeed");
});
let box_size = read_u32_be(&out, 0) as usize;
assert_eq!(box_size, out.len());
}
#[test]
fn test_full_sequence() {
let tracks = vec![TrackInfo::new(0, TrackType::Video, "av01")];
let media_data: &[u8] = b"fake encoded video";
let out = capture(|m| {
m.write_ftyp("isom").expect("ftyp ok");
m.write_mdat(media_data).expect("mdat ok");
m.write_moov(&tracks).expect("moov ok");
});
assert!(find_box(&out, b"ftyp"));
assert!(find_box(&out, b"mdat"));
assert!(find_box(&out, b"moov"));
}
#[test]
fn test_write_ftyp_av01_brand() {
let out = capture(|m| {
m.write_ftyp("av01").expect("should succeed");
});
assert_eq!(&out[8..12], b"av01");
}
#[test]
fn test_write_moov_contains_dinf() {
let tracks = vec![TrackInfo::new(0, TrackType::Video, "av01")];
let out = capture(|m| {
m.write_moov(&tracks).expect("should succeed");
});
assert!(find_box(&out, b"dinf"));
assert!(find_box(&out, b"dref"));
}
#[test]
fn test_write_moov_subtitle_track() {
let tracks = vec![TrackInfo::new(0, TrackType::Subtitle, "wvtt")];
let out = capture(|m| {
m.write_moov(&tracks).expect("should succeed");
});
assert!(find_box(&out, b"nmhd"));
}
#[test]
fn test_write_moov_multiple_tracks_count() {
let tracks = vec![
TrackInfo::new(0, TrackType::Video, "av01"),
TrackInfo::new(1, TrackType::Audio, "Opus"),
];
let out = capture(|m| {
m.write_moov(&tracks).expect("should succeed");
});
let trak_count = out.windows(4).filter(|w| *w == b"trak").count();
assert_eq!(trak_count, 2);
}
}