use oxideav_core::{CodecParameters, Error, MediaType, Result};
pub(crate) struct SampleEntry {
pub fourcc: [u8; 4],
pub body: Vec<u8>,
}
pub(crate) fn sample_entry_for(params: &CodecParameters) -> Result<SampleEntry> {
match params.codec_id.as_str() {
"pcm_s16le" => pcm_sowt(params),
"flac" => flac_entry(params),
"aac" => aac_entry(params),
"h264" => h264_entry(params),
"mjpeg" => mjpeg_entry(params),
"mov_text" => subtitle_entry(params, *b"tx3g"),
"webvtt" => subtitle_entry(params, *b"wvtt"),
"ttml" => subtitle_entry(params, *b"stpp"),
"sbtt" => subtitle_entry(params, *b"sbtt"),
"stxt" => subtitle_entry(params, *b"stxt"),
other => Err(Error::unsupported(format!(
"mp4 muxer: no sample entry for codec {other}"
))),
}
}
pub(crate) fn subtitle_handler_for(codec_id: &str) -> [u8; 4] {
match codec_id {
"mov_text" => *b"text",
_ => *b"subt",
}
}
pub(crate) fn subtitle_uses_sthd(codec_id: &str) -> bool {
!matches!(codec_id, "mov_text")
}
fn subtitle_entry(params: &CodecParameters, fourcc: [u8; 4]) -> Result<SampleEntry> {
if params.media_type != MediaType::Subtitle {
return Err(Error::invalid(format!(
"mp4 muxer: subtitle codec {} must be Subtitle media",
params.codec_id.as_str()
)));
}
let mut body = Vec::with_capacity(8 + params.extradata.len());
body.extend_from_slice(&[0u8; 6]);
body.extend_from_slice(&1u16.to_be_bytes());
body.extend_from_slice(¶ms.extradata);
Ok(SampleEntry { fourcc, body })
}
fn mjpeg_entry(params: &CodecParameters) -> Result<SampleEntry> {
if params.media_type != MediaType::Video {
return Err(Error::invalid("mp4 muxer: mjpeg must be video"));
}
let width = params
.width
.ok_or_else(|| Error::invalid("mp4 muxer: mjpeg requires width"))?;
let height = params
.height
.ok_or_else(|| Error::invalid("mp4 muxer: mjpeg requires height"))?;
let body = visual_preamble(width, height).to_vec();
Ok(SampleEntry {
fourcc: *b"jpeg",
body,
})
}
fn audio_preamble(channels: u16, sample_size: u16, sample_rate: u32) -> [u8; 28] {
let mut out = [0u8; 28];
out[6] = 0;
out[7] = 1;
out[16..18].copy_from_slice(&channels.to_be_bytes());
out[18..20].copy_from_slice(&sample_size.to_be_bytes());
let sr_fixed = sample_rate << 16;
out[24..28].copy_from_slice(&sr_fixed.to_be_bytes());
out
}
fn visual_preamble(width: u32, height: u32) -> [u8; 78] {
let mut out = [0u8; 78];
out[6] = 0;
out[7] = 1;
let w = width as u16;
let h = height as u16;
out[24..26].copy_from_slice(&w.to_be_bytes());
out[26..28].copy_from_slice(&h.to_be_bytes());
let dpi = 72u32 << 16;
out[28..32].copy_from_slice(&dpi.to_be_bytes());
out[32..36].copy_from_slice(&dpi.to_be_bytes());
out[40..42].copy_from_slice(&1u16.to_be_bytes());
out[74..76].copy_from_slice(&0x0018u16.to_be_bytes());
out[76..78].copy_from_slice(&(-1i16).to_be_bytes());
out
}
fn pcm_sowt(params: &CodecParameters) -> Result<SampleEntry> {
let channels = params
.channels
.ok_or_else(|| Error::invalid("mp4 muxer: PCM requires channels"))?;
let sample_rate = params
.sample_rate
.ok_or_else(|| Error::invalid("mp4 muxer: PCM requires sample_rate"))?;
let body = audio_preamble(channels, 16, sample_rate).to_vec();
Ok(SampleEntry {
fourcc: *b"sowt",
body,
})
}
fn flac_entry(params: &CodecParameters) -> Result<SampleEntry> {
if params.media_type != MediaType::Audio {
return Err(Error::invalid("mp4 muxer: flac must be audio"));
}
let channels = params
.channels
.ok_or_else(|| Error::invalid("mp4 muxer: flac requires channels"))?;
let sample_rate = params
.sample_rate
.ok_or_else(|| Error::invalid("mp4 muxer: flac requires sample_rate"))?;
let bps = params
.sample_format
.map(|f| (f.bytes_per_sample() * 8) as u16)
.unwrap_or(16);
let mut body = audio_preamble(channels, bps, sample_rate).to_vec();
if params.extradata.is_empty() {
return Err(Error::invalid(
"mp4 muxer: flac stream missing extradata (STREAMINFO)",
));
}
let mut dfla_body = Vec::with_capacity(4 + params.extradata.len());
dfla_body.extend_from_slice(&[0, 0, 0, 0]); dfla_body.extend_from_slice(¶ms.extradata);
body.extend_from_slice(&write_simple_box(b"dfLa", &dfla_body));
Ok(SampleEntry {
fourcc: *b"fLaC",
body,
})
}
fn aac_entry(params: &CodecParameters) -> Result<SampleEntry> {
if params.media_type != MediaType::Audio {
return Err(Error::invalid("mp4 muxer: aac must be audio"));
}
let channels = params
.channels
.ok_or_else(|| Error::invalid("mp4 muxer: aac requires channels"))?;
let sample_rate = params
.sample_rate
.ok_or_else(|| Error::invalid("mp4 muxer: aac requires sample_rate"))?;
if params.extradata.is_empty() {
return Err(Error::invalid(
"mp4 muxer: aac stream missing extradata (AudioSpecificConfig)",
));
}
let mut body = audio_preamble(channels, 16, sample_rate).to_vec();
let asc = ¶ms.extradata;
let mut dsi = Vec::new();
dsi.push(0x05);
append_ber_length(&mut dsi, asc.len() as u32);
dsi.extend_from_slice(asc);
let mut dcd = Vec::new();
dcd.push(0x04);
let dcd_payload_len = 13 + dsi.len() as u32;
append_ber_length(&mut dcd, dcd_payload_len);
dcd.push(0x40); dcd.push((0x05 << 2) | 0x01); dcd.extend_from_slice(&[0, 0, 0]);
dcd.extend_from_slice(&[0, 0, 0, 0]);
dcd.extend_from_slice(&[0, 0, 0, 0]);
dcd.extend_from_slice(&dsi);
let mut slc = Vec::new();
slc.push(0x06);
append_ber_length(&mut slc, 1);
slc.push(0x02);
let mut esd = Vec::new();
esd.push(0x03);
let esd_payload_len = 3 + dcd.len() as u32 + slc.len() as u32;
append_ber_length(&mut esd, esd_payload_len);
esd.extend_from_slice(&[0, 0, 0]);
esd.extend_from_slice(&dcd);
esd.extend_from_slice(&slc);
let mut esds_body = Vec::with_capacity(4 + esd.len());
esds_body.extend_from_slice(&[0, 0, 0, 0]);
esds_body.extend_from_slice(&esd);
body.extend_from_slice(&write_simple_box(b"esds", &esds_body));
Ok(SampleEntry {
fourcc: *b"mp4a",
body,
})
}
fn h264_entry(params: &CodecParameters) -> Result<SampleEntry> {
if params.media_type != MediaType::Video {
return Err(Error::invalid("mp4 muxer: h264 must be video"));
}
let width = params
.width
.ok_or_else(|| Error::invalid("mp4 muxer: h264 requires width"))?;
let height = params
.height
.ok_or_else(|| Error::invalid("mp4 muxer: h264 requires height"))?;
if params.extradata.is_empty() {
return Err(Error::invalid(
"mp4 muxer: h264 stream missing extradata (AVCC configuration)",
));
}
let mut body = visual_preamble(width, height).to_vec();
body.extend_from_slice(&write_simple_box(b"avcC", ¶ms.extradata));
Ok(SampleEntry {
fourcc: *b"avc1",
body,
})
}
fn write_simple_box(kind: &[u8; 4], body: &[u8]) -> Vec<u8> {
let total = 8 + body.len() as u32;
let mut out = Vec::with_capacity(total as usize);
out.extend_from_slice(&total.to_be_bytes());
out.extend_from_slice(kind);
out.extend_from_slice(body);
out
}
fn append_ber_length(out: &mut Vec<u8>, mut value: u32) {
let mut bytes = [0u8; 4];
for i in (0..4).rev() {
bytes[i] = (value & 0x7F) as u8;
value >>= 7;
}
for b in &mut bytes[..3] {
*b |= 0x80;
}
out.extend_from_slice(&bytes);
}
#[cfg(test)]
mod tests {
use super::*;
use oxideav_core::{CodecId, CodecParameters, SampleFormat};
#[test]
fn pcm_sowt_shape() {
let mut p = CodecParameters::audio(CodecId::new("pcm_s16le"));
p.channels = Some(2);
p.sample_rate = Some(48_000);
p.sample_format = Some(SampleFormat::S16);
let e = sample_entry_for(&p).unwrap();
assert_eq!(&e.fourcc, b"sowt");
assert_eq!(e.body.len(), 28);
assert_eq!(u16::from_be_bytes([e.body[16], e.body[17]]), 2);
assert_eq!(u16::from_be_bytes([e.body[18], e.body[19]]), 16);
}
#[test]
fn flac_entry_has_dfla() {
let mut p = CodecParameters::audio(CodecId::new("flac"));
p.channels = Some(2);
p.sample_rate = Some(48_000);
p.sample_format = Some(SampleFormat::S16);
let mut extradata = Vec::new();
extradata.extend_from_slice(&[0x80, 0, 0, 34]); extradata.extend_from_slice(&[0u8; 34]);
p.extradata = extradata;
let e = sample_entry_for(&p).unwrap();
assert_eq!(&e.fourcc, b"fLaC");
assert_eq!(e.body.len(), 28 + 8 + 4 + 38);
assert_eq!(&e.body[32..36], b"dfLa");
}
#[test]
fn unsupported_codec_errors() {
let p = CodecParameters::audio(CodecId::new("vorbis"));
assert!(sample_entry_for(&p).is_err());
}
#[test]
fn mov_text_entry_shape() {
let mut p = CodecParameters::subtitle(CodecId::new("mov_text"));
let tx3g_header: [u8; 18] = [
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
p.extradata = tx3g_header.to_vec();
let e = sample_entry_for(&p).unwrap();
assert_eq!(&e.fourcc, b"tx3g");
assert_eq!(e.body.len(), 26);
assert_eq!(u16::from_be_bytes([e.body[6], e.body[7]]), 1);
assert_eq!(&e.body[8..], &tx3g_header);
}
#[test]
fn webvtt_entry_shape() {
let mut p = CodecParameters::subtitle(CodecId::new("webvtt"));
let mut vttc = Vec::new();
vttc.extend_from_slice(&14u32.to_be_bytes());
vttc.extend_from_slice(b"vttC");
vttc.extend_from_slice(b"WEBVTT");
p.extradata = vttc.clone();
let e = sample_entry_for(&p).unwrap();
assert_eq!(&e.fourcc, b"wvtt");
assert_eq!(e.body.len(), 8 + vttc.len());
assert_eq!(&e.body[12..16], b"vttC");
}
#[test]
fn ttml_entry_shape() {
let mut p = CodecParameters::subtitle(CodecId::new("ttml"));
let strings = b"http://www.w3.org/ns/ttml\0\0\0";
p.extradata = strings.to_vec();
let e = sample_entry_for(&p).unwrap();
assert_eq!(&e.fourcc, b"stpp");
assert_eq!(e.body.len(), 8 + strings.len());
assert!(e.body[8..].starts_with(b"http://www.w3.org/ns/ttml"));
}
#[test]
fn sbtt_entry_shape() {
let mut p = CodecParameters::subtitle(CodecId::new("sbtt"));
let strings = b"\0text/plain\0";
p.extradata = strings.to_vec();
let e = sample_entry_for(&p).unwrap();
assert_eq!(&e.fourcc, b"sbtt");
assert_eq!(e.body.len(), 8 + strings.len());
}
#[test]
fn stxt_entry_shape() {
let mut p = CodecParameters::subtitle(CodecId::new("stxt"));
let strings = b"\0text/html\0";
p.extradata = strings.to_vec();
let e = sample_entry_for(&p).unwrap();
assert_eq!(&e.fourcc, b"stxt");
assert_eq!(e.body.len(), 8 + strings.len());
}
#[test]
fn subtitle_handler_routing() {
assert_eq!(&subtitle_handler_for("mov_text"), b"text");
for c in ["webvtt", "ttml", "sbtt", "stxt"] {
assert_eq!(&subtitle_handler_for(c), b"subt");
}
}
#[test]
fn subtitle_header_routing() {
assert!(!subtitle_uses_sthd("mov_text"));
for c in ["webvtt", "ttml", "sbtt", "stxt"] {
assert!(subtitle_uses_sthd(c));
}
}
#[test]
fn subtitle_rejects_wrong_media_type() {
let p = CodecParameters::audio(CodecId::new("mov_text"));
assert!(sample_entry_for(&p).is_err());
}
}