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),
"h265" => video_config_entry(params, *b"hvc1", *b"hvcC", "HEVCDecoderConfigurationRecord"),
"av1" => video_config_entry(params, *b"av01", *b"av1C", "AV1CodecConfigurationRecord"),
"vp9" => video_config_entry(params, *b"vp09", *b"vpcC", "VPCodecConfigurationRecord"),
"vp8" => video_config_entry(params, *b"vp08", *b"vpcC", "VPCodecConfigurationRecord"),
"h263" => h263_entry(params),
"opus" => opus_entry(params),
"alac" => alac_entry(params),
"ac3" => dolby_entry(params, *b"ac-3", *b"dac3"),
"eac3" => dolby_entry(params, *b"ec-3", *b"dec3"),
"mp3" => mp3_entry(params),
"pcm_mulaw" => g711_entry(params, *b"ulaw"),
"pcm_alaw" => g711_entry(params, *b"alaw"),
"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();
body.extend_from_slice(&write_simple_box(
b"esds",
&build_esds_body(0x40, ¶ms.extradata),
));
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 video_config_entry(
params: &CodecParameters,
fourcc: [u8; 4],
config_fourcc: [u8; 4],
record_name: &str,
) -> Result<SampleEntry> {
if params.media_type != MediaType::Video {
return Err(Error::invalid(format!(
"mp4 muxer: {} must be video",
params.codec_id.as_str()
)));
}
let width = params.width.ok_or_else(|| {
Error::invalid(format!(
"mp4 muxer: {} requires width",
params.codec_id.as_str()
))
})?;
let height = params.height.ok_or_else(|| {
Error::invalid(format!(
"mp4 muxer: {} requires height",
params.codec_id.as_str()
))
})?;
if params.extradata.is_empty() {
return Err(Error::invalid(format!(
"mp4 muxer: {} stream missing extradata ({record_name})",
params.codec_id.as_str()
)));
}
let mut body = visual_preamble(width, height).to_vec();
body.extend_from_slice(&write_simple_box(&config_fourcc, ¶ms.extradata));
Ok(SampleEntry { fourcc, body })
}
fn h263_entry(params: &CodecParameters) -> Result<SampleEntry> {
if params.media_type != MediaType::Video {
return Err(Error::invalid("mp4 muxer: h263 must be video"));
}
let width = params
.width
.ok_or_else(|| Error::invalid("mp4 muxer: h263 requires width"))?;
let height = params
.height
.ok_or_else(|| Error::invalid("mp4 muxer: h263 requires height"))?;
let mut body = visual_preamble(width, height).to_vec();
if !params.extradata.is_empty() {
body.extend_from_slice(&write_simple_box(b"d263", ¶ms.extradata));
}
Ok(SampleEntry {
fourcc: *b"s263",
body,
})
}
fn opus_entry(params: &CodecParameters) -> Result<SampleEntry> {
if params.media_type != MediaType::Audio {
return Err(Error::invalid("mp4 muxer: opus must be audio"));
}
let channels = params
.channels
.ok_or_else(|| Error::invalid("mp4 muxer: opus requires channels"))?;
let sample_rate = params
.sample_rate
.ok_or_else(|| Error::invalid("mp4 muxer: opus requires sample_rate"))?;
if params.extradata.is_empty() {
return Err(Error::invalid(
"mp4 muxer: opus stream missing extradata (OpusHead / dOps config)",
));
}
let dops_body = match params.extradata.strip_prefix(b"OpusHead") {
Some(rest) => rest,
None => ¶ms.extradata[..],
};
if dops_body.len() < 11 {
return Err(Error::invalid(
"mp4 muxer: opus extradata too short for a dOps config",
));
}
let mut body = audio_preamble(channels, 16, sample_rate).to_vec();
body.extend_from_slice(&write_simple_box(b"dOps", dops_body));
Ok(SampleEntry {
fourcc: *b"Opus",
body,
})
}
fn alac_entry(params: &CodecParameters) -> Result<SampleEntry> {
if params.media_type != MediaType::Audio {
return Err(Error::invalid("mp4 muxer: alac must be audio"));
}
let channels = params
.channels
.ok_or_else(|| Error::invalid("mp4 muxer: alac requires channels"))?;
let sample_rate = params
.sample_rate
.ok_or_else(|| Error::invalid("mp4 muxer: alac requires sample_rate"))?;
if params.extradata.is_empty() {
return Err(Error::invalid(
"mp4 muxer: alac stream missing extradata (ALAC magic cookie)",
));
}
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();
let mut cfg = Vec::with_capacity(4 + params.extradata.len());
cfg.extend_from_slice(&[0, 0, 0, 0]); cfg.extend_from_slice(¶ms.extradata);
body.extend_from_slice(&write_simple_box(b"alac", &cfg));
Ok(SampleEntry {
fourcc: *b"alac",
body,
})
}
fn dolby_entry(
params: &CodecParameters,
fourcc: [u8; 4],
config_fourcc: [u8; 4],
) -> Result<SampleEntry> {
if params.media_type != MediaType::Audio {
return Err(Error::invalid(format!(
"mp4 muxer: {} must be audio",
params.codec_id.as_str()
)));
}
let channels = params.channels.ok_or_else(|| {
Error::invalid(format!(
"mp4 muxer: {} requires channels",
params.codec_id.as_str()
))
})?;
let sample_rate = params.sample_rate.ok_or_else(|| {
Error::invalid(format!(
"mp4 muxer: {} requires sample_rate",
params.codec_id.as_str()
))
})?;
if params.extradata.is_empty() {
return Err(Error::invalid(format!(
"mp4 muxer: {} stream missing extradata ({} config)",
params.codec_id.as_str(),
String::from_utf8_lossy(&config_fourcc)
)));
}
let mut body = audio_preamble(channels, 16, sample_rate).to_vec();
body.extend_from_slice(&write_simple_box(&config_fourcc, ¶ms.extradata));
Ok(SampleEntry { fourcc, body })
}
fn mp3_entry(params: &CodecParameters) -> Result<SampleEntry> {
if params.media_type != MediaType::Audio {
return Err(Error::invalid("mp4 muxer: mp3 must be audio"));
}
let channels = params
.channels
.ok_or_else(|| Error::invalid("mp4 muxer: mp3 requires channels"))?;
let sample_rate = params
.sample_rate
.ok_or_else(|| Error::invalid("mp4 muxer: mp3 requires sample_rate"))?;
let mut body = audio_preamble(channels, 16, sample_rate).to_vec();
body.extend_from_slice(&write_simple_box(b"esds", &build_esds_body(0x6B, &[])));
Ok(SampleEntry {
fourcc: *b"mp4a",
body,
})
}
fn g711_entry(params: &CodecParameters, fourcc: [u8; 4]) -> Result<SampleEntry> {
if params.media_type != MediaType::Audio {
return Err(Error::invalid("mp4 muxer: G.711 must be audio"));
}
let channels = params
.channels
.ok_or_else(|| Error::invalid("mp4 muxer: G.711 requires channels"))?;
let sample_rate = params
.sample_rate
.ok_or_else(|| Error::invalid("mp4 muxer: G.711 requires sample_rate"))?;
let body = audio_preamble(channels, 8, sample_rate).to_vec();
Ok(SampleEntry { fourcc, body })
}
fn build_esds_body(oti: u8, dsi_bytes: &[u8]) -> Vec<u8> {
let mut dsi = Vec::new();
if !dsi_bytes.is_empty() {
dsi.push(0x05);
append_ber_length(&mut dsi, dsi_bytes.len() as u32);
dsi.extend_from_slice(dsi_bytes);
}
let mut dcd = Vec::new();
dcd.push(0x04);
append_ber_length(&mut dcd, 13 + dsi.len() as u32);
dcd.push(oti);
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);
append_ber_length(&mut esd, 3 + dcd.len() as u32 + slc.len() as u32);
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);
esds_body
}
pub(crate) fn apply_protection(
entry: SampleEntry,
media_type: MediaType,
protection: &crate::options::TrackProtection,
) -> Result<SampleEntry> {
let enc_fourcc: [u8; 4] = match media_type {
MediaType::Video => *b"encv",
MediaType::Audio => *b"enca",
MediaType::Subtitle => *b"enct",
_ => *b"encs",
};
let sinf = crate::cenc::build_sinf_box(
entry.fourcc,
protection.scheme_type,
protection.scheme_version,
&protection.tenc,
)?;
let mut body = entry.body;
body.extend_from_slice(&sinf);
Ok(SampleEntry {
fourcc: enc_fourcc,
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());
}
fn video_params(codec: &str, extradata: &[u8]) -> CodecParameters {
let mut p = CodecParameters::video(CodecId::new(codec));
p.width = Some(640);
p.height = Some(360);
p.extradata = extradata.to_vec();
p
}
fn audio_params(codec: &str, extradata: &[u8]) -> CodecParameters {
let mut p = CodecParameters::audio(CodecId::new(codec));
p.channels = Some(2);
p.sample_rate = Some(48_000);
p.extradata = extradata.to_vec();
p
}
#[test]
fn video_config_entries_wrap_extradata() {
for (codec, fourcc, cfg) in [
("h265", b"hvc1", b"hvcC"),
("av1", b"av01", b"av1C"),
("vp9", b"vp09", b"vpcC"),
("vp8", b"vp08", b"vpcC"),
] {
let record = [0xAAu8, 0xBB, 0xCC, 0xDD, 0xEE];
let e = sample_entry_for(&video_params(codec, &record)).unwrap();
assert_eq!(&e.fourcc, fourcc, "{codec} entry fourcc");
assert_eq!(e.body.len(), 78 + 8 + record.len(), "{codec} body size");
assert_eq!(&e.body[82..86], cfg, "{codec} config fourcc");
assert_eq!(&e.body[86..], &record, "{codec} config body verbatim");
assert_eq!(u16::from_be_bytes([e.body[24], e.body[25]]), 640);
assert_eq!(u16::from_be_bytes([e.body[26], e.body[27]]), 360);
}
}
#[test]
fn video_config_entries_require_extradata() {
for codec in ["h265", "av1", "vp9", "vp8"] {
assert!(
sample_entry_for(&video_params(codec, &[])).is_err(),
"{codec} must require a config record"
);
}
}
#[test]
fn h263_entry_with_and_without_d263() {
let d263 = [b'O', b'X', b'A', b'V', 0, 10, 0];
let e = sample_entry_for(&video_params("h263", &d263)).unwrap();
assert_eq!(&e.fourcc, b"s263");
assert_eq!(e.body.len(), 78 + 8 + d263.len());
assert_eq!(&e.body[82..86], b"d263");
assert_eq!(&e.body[86..], &d263);
let e = sample_entry_for(&video_params("h263", &[])).unwrap();
assert_eq!(e.body.len(), 78);
}
#[test]
fn opus_entry_strips_opushead_magic() {
let dops = [1u8, 2, 0x01, 0x38, 0, 0, 0xBB, 0x80, 0, 0, 0];
let mut extradata = b"OpusHead".to_vec();
extradata.extend_from_slice(&dops);
let e = sample_entry_for(&audio_params("opus", &extradata)).unwrap();
assert_eq!(&e.fourcc, b"Opus");
assert_eq!(e.body.len(), 28 + 8 + dops.len());
assert_eq!(&e.body[32..36], b"dOps");
assert_eq!(&e.body[36..], &dops, "dOps body must lose the magic");
let e2 = sample_entry_for(&audio_params("opus", &dops)).unwrap();
assert_eq!(&e2.body[36..], &dops);
assert!(sample_entry_for(&audio_params("opus", b"OpusHead\x01")).is_err());
}
#[test]
fn alac_entry_wraps_cookie_in_fullbox() {
let cookie = [0u8, 0, 16, 0, 40, 10, 14, 2];
let e = sample_entry_for(&audio_params("alac", &cookie)).unwrap();
assert_eq!(&e.fourcc, b"alac");
assert_eq!(e.body.len(), 28 + 8 + 4 + cookie.len());
assert_eq!(&e.body[32..36], b"alac");
assert_eq!(&e.body[36..40], &[0, 0, 0, 0], "FullBox version/flags");
assert_eq!(&e.body[40..], &cookie);
assert!(sample_entry_for(&audio_params("alac", &[])).is_err());
}
#[test]
fn dolby_entries_carry_config_verbatim() {
let dac3 = [0x10u8, 0x4C, 0x40];
let e = sample_entry_for(&audio_params("ac3", &dac3)).unwrap();
assert_eq!(&e.fourcc, b"ac-3");
assert_eq!(&e.body[32..36], b"dac3");
assert_eq!(&e.body[36..], &dac3);
let dec3 = [0x07u8, 0xC0, 0x20, 0x00, 0x00];
let e = sample_entry_for(&audio_params("eac3", &dec3)).unwrap();
assert_eq!(&e.fourcc, b"ec-3");
assert_eq!(&e.body[32..36], b"dec3");
assert_eq!(&e.body[36..], &dec3);
assert!(sample_entry_for(&audio_params("ac3", &[])).is_err());
assert!(sample_entry_for(&audio_params("eac3", &[])).is_err());
}
#[test]
fn mp3_entry_emits_esds_with_mpeg1_oti() {
let e = sample_entry_for(&audio_params("mp3", &[])).unwrap();
assert_eq!(&e.fourcc, b"mp4a");
assert_eq!(&e.body[32..36], b"esds");
assert_eq!(e.body[28 + 8 + 4 + 13], 0x6B, "OTI must be MPEG-1 audio");
assert_eq!(e.body[28 + 8 + 4 + 12], 13, "DCD BER length");
}
#[test]
fn g711_entries_are_plain_8bit_audio() {
for (codec, fourcc) in [("pcm_mulaw", b"ulaw"), ("pcm_alaw", b"alaw")] {
let e = sample_entry_for(&audio_params(codec, &[])).unwrap();
assert_eq!(&e.fourcc, fourcc);
assert_eq!(e.body.len(), 28, "{codec} has no config child");
assert_eq!(
u16::from_be_bytes([e.body[18], e.body[19]]),
8,
"{codec} sample size"
);
}
}
#[test]
fn new_entries_reject_wrong_media_type() {
let p = CodecParameters::audio(CodecId::new("h265"));
assert!(sample_entry_for(&p).is_err());
let p = CodecParameters::video(CodecId::new("opus"));
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());
}
}