use crate::colour::ContainerColour;
use crate::error::Result;
use crate::mp4::colour::nclx_body;
use crate::mp4::mux::bytes::BoxBuf;
use crate::mp4::mux::bytes::invalid;
use crate::mp4::mux::spec::SampleEntrySpec;
use crate::track::{FourCc, PixelAspect};
pub(crate) const CHAPTER_TEXT_ENTRY: [u8; 59] = [
0x00, 0x00, 0x00, 0x3B, b't', b'e', b'x', b't', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0D, b'f', b't', b'a', b'b', 0x00, 0x01, 0x00, 0x01, 0x00,
];
pub(crate) const TEXT_GMHD: [u8; 76] = [
0x00, 0x00, 0x00, 0x4C, b'g', b'm', b'h', b'd', 0x00, 0x00, 0x00, 0x18, b'g', b'm', b'i', b'n',
0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x2C, b't', b'e', b'x', b't', 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
];
const DREF_AT: usize = 14;
pub(crate) fn build_entry(
spec: &SampleEntrySpec,
pixel_aspect: Option<PixelAspect>,
colour: Option<ContainerColour>,
) -> Result<(FourCc, Vec<u8>)> {
match spec {
SampleEntrySpec::Raw(raw) => raw_entry(raw),
SampleEntrySpec::Visual {
codec,
config,
width,
height,
} => {
let (kind, config_kind) = codec.boxes();
if config.is_empty() {
return Err(invalid(format!(
"the `{}` configuration of a `{}` entry is empty. Pass the codec \
configuration record",
FourCc(*config_kind),
FourCc(*kind)
)));
}
let path = FourCc(*kind).to_string();
let mut buf = BoxBuf::new();
let start = buf.open(kind);
buf.zeros(6);
buf.u16(1);
buf.zeros(16);
buf.u16(*width);
buf.u16(*height);
buf.u32(0x0048_0000);
buf.u32(0x0048_0000);
buf.u32(0);
buf.u16(1);
buf.zeros(32);
buf.u16(0x0018);
buf.u16(0xFFFF);
buf.boxed(
config_kind,
config,
&format!("{path}/{}", FourCc(*config_kind)),
)?;
if let Some(aspect) = pixel_aspect {
let pasp_start = buf.open(b"pasp");
buf.u32(aspect.h_spacing);
buf.u32(aspect.v_spacing);
buf.close(pasp_start, &format!("{path}/pasp"))?;
}
if let Some(body) = colour.as_ref().and_then(nclx_body) {
buf.boxed(b"colr", &body, &format!("{path}/colr"))?;
}
buf.close(start, &path)?;
Ok((FourCc(*kind), buf.into_bytes()))
}
SampleEntrySpec::Audio {
codec,
config,
channels,
sample_rate,
} => {
let (kind, config_kind) = codec.boxes();
let path = FourCc(*kind).to_string();
let mut buf = BoxBuf::new();
let start = buf.open(kind);
buf.zeros(6);
buf.u16(1);
buf.zeros(8);
buf.u16(*channels);
buf.u16(16);
buf.zeros(4);
buf.u32(if *sample_rate <= 0xFFFF {
sample_rate << 16
} else {
0
});
buf.boxed(
config_kind,
config,
&format!("{path}/{}", FourCc(*config_kind)),
)?;
buf.close(start, &path)?;
Ok((FourCc(*kind), buf.into_bytes()))
}
SampleEntrySpec::Tx3g => {
let mut buf = BoxBuf::new();
let start = buf.open(b"tx3g");
buf.zeros(6);
buf.u16(1);
buf.u32(0);
buf.bytes(&[0x01, 0xFF, 0x00, 0x00, 0x00, 0xFF]);
buf.zeros(8);
buf.bytes(&[0, 0, 0, 0, 0, 1, 0, 0x10, 0xFF, 0xFF, 0xFF, 0xFF]);
let ftab = buf.open(b"ftab");
buf.u16(1);
buf.u16(1);
buf.u8(5);
buf.bytes(b"Arial");
buf.close(ftab, "tx3g/ftab")?;
buf.close(start, "tx3g")?;
Ok((FourCc(*b"tx3g"), buf.into_bytes()))
}
}
}
fn raw_entry(raw: &[u8]) -> Result<(FourCc, Vec<u8>)> {
let (Some(size), Some(kind)) = (raw.get(..4), raw.get(4..8)) else {
return Err(invalid(format!(
"the raw sample entry holds {} bytes, too few for a box header",
raw.len()
)));
};
let mut kind_bytes = [0u8; 4];
kind_bytes.copy_from_slice(kind);
let kind = FourCc(kind_bytes);
let declared = u32::from_be_bytes([size[0], size[1], size[2], size[3]]);
if declared as usize != raw.len() || raw.len() < DREF_AT + 2 {
return Err(invalid(format!(
"the raw `{kind}` sample entry declares {declared} bytes and holds {}. \
Pass the whole box, as `SampleEntry::raw` holds it",
raw.len()
)));
}
let mut out = raw.to_vec();
if let Some(field) = out.get_mut(DREF_AT..DREF_AT + 2) {
field.copy_from_slice(&[0, 1]);
}
Ok((kind, out))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mp4::mux::spec::{AudioCodec, VisualCodec};
#[test]
fn visual_entry_holds_size_config_and_pasp() {
let spec = SampleEntrySpec::Visual {
codec: VisualCodec::Avc,
config: vec![1, 2, 3],
width: 96,
height: 64,
};
let aspect = PixelAspect {
h_spacing: 4,
v_spacing: 3,
};
let (kind, bytes) = build_entry(&spec, Some(aspect), None).expect("the entry builds");
assert_eq!(kind, FourCc(*b"avc1"));
assert_eq!(bytes.len(), 8 + 78 + 11 + 16);
assert_eq!(&bytes[32..36], &[0, 96, 0, 64]);
assert_eq!(
&bytes[86..97],
&[0, 0, 0, 11, b'a', b'v', b'c', b'C', 1, 2, 3]
);
assert_eq!(&bytes[101..105], b"pasp");
}
#[test]
fn visual_entry_writes_an_nclx_colr_box() {
let spec = SampleEntrySpec::Visual {
codec: VisualCodec::Hevc,
config: vec![1],
width: 96,
height: 64,
};
let colour = ContainerColour {
primaries: Some(9),
transfer: Some(16),
matrix: Some(9),
full_range: Some(true),
};
let (_, bytes) = build_entry(&spec, None, Some(colour)).expect("the entry builds");
let tail = &bytes[bytes.len() - 19..];
assert_eq!(&tail[..8], &[0, 0, 0, 19, b'c', b'o', b'l', b'r']);
assert_eq!(
&tail[8..],
&[b'n', b'c', b'l', b'x', 0, 9, 0, 16, 0, 9, 0x80]
);
let (_, plain) =
build_entry(&spec, None, Some(ContainerColour::default())).expect("the entry builds");
assert_eq!(plain.len() + 19, bytes.len());
}
#[test]
fn audio_entry_stores_the_rate_as_16_16() {
let spec = SampleEntrySpec::Audio {
codec: AudioCodec::Opus,
config: vec![0; 11],
channels: 2,
sample_rate: 48_000,
};
let (kind, bytes) = build_entry(&spec, None, None).expect("the entry builds");
assert_eq!(kind, FourCc(*b"Opus"));
assert_eq!(&bytes[24..26], &[0, 2]);
assert_eq!(&bytes[32..36], &(48_000u32 << 16).to_be_bytes());
assert_eq!(&bytes[40..44], b"dOps");
}
#[test]
fn raw_entry_resets_the_data_reference() {
let mut raw = vec![0, 0, 0, 16, b'm', b'p', b'4', b'a', 0, 0, 0, 0, 0, 0, 0, 7];
let (kind, out) = build_entry(&SampleEntrySpec::Raw(raw.clone()), None, None)
.expect("the entry is whole");
assert_eq!(kind, FourCc(*b"mp4a"));
assert_eq!(&out[14..16], &[0, 1]);
raw[3] = 40;
assert!(build_entry(&SampleEntrySpec::Raw(raw), None, None).is_err());
}
#[test]
fn text_constants_declare_their_length() {
assert_eq!(CHAPTER_TEXT_ENTRY[3] as usize, CHAPTER_TEXT_ENTRY.len());
assert_eq!(TEXT_GMHD[3] as usize, TEXT_GMHD.len());
}
}