use anyhow::{Context, Result};
use bytes::Bytes;
use codec::encode::EncodedPacket;
use codec::frame::ColorMetadata;
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::Path;
use tempfile::NamedTempFile;
use crate::AudioInfo;
pub struct Av1Mp4Muxer {
width: u32,
height: u32,
frame_rate: f64,
mdat_tmp: NamedTempFile,
mdat_writer: BufWriter<File>,
sample_sizes: Vec<u32>,
keyframe_indices: Vec<u32>,
first_packet_header: Option<Vec<u8>>,
packet_count: u32,
mdat_payload_bytes: u64,
audio: Option<AudioTrackState>,
color_metadata: ColorMetadata,
#[doc(hidden)]
force_largesize_mdat: bool,
}
struct AudioTrackState {
info: AudioInfo,
audio_tmp: NamedTempFile,
audio_writer: BufWriter<File>,
sample_sizes: Vec<u32>,
durations: Vec<u32>,
total_duration_ticks: u64,
mdat_payload_bytes: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AudioCodecKind {
Aac,
Opus,
Ac3,
Eac3,
}
impl AudioCodecKind {
fn from_codec_tag(codec: &str) -> Option<Self> {
if codec.eq_ignore_ascii_case("aac") {
Some(Self::Aac)
} else if codec.eq_ignore_ascii_case("opus") {
Some(Self::Opus)
} else if codec.eq_ignore_ascii_case("ac3") || codec.eq_ignore_ascii_case("ac-3") {
Some(Self::Ac3)
} else if codec.eq_ignore_ascii_case("eac3") || codec.eq_ignore_ascii_case("e-ac-3") {
Some(Self::Eac3)
} else {
None
}
}
}
impl Av1Mp4Muxer {
pub fn new(width: u32, height: u32, frame_rate: f64) -> Result<Self> {
let mdat_tmp = NamedTempFile::new().context("creating mdat tempfile")?;
let handle = mdat_tmp
.reopen()
.context("reopening mdat tempfile for write")?;
let mdat_writer = BufWriter::new(handle);
Ok(Self {
width,
height,
frame_rate,
mdat_tmp,
mdat_writer,
sample_sizes: Vec::new(),
keyframe_indices: Vec::new(),
first_packet_header: None,
packet_count: 0,
mdat_payload_bytes: 0,
audio: None,
color_metadata: ColorMetadata::default(),
force_largesize_mdat: false,
})
}
#[doc(hidden)]
pub fn force_largesize_mdat_for_test(&mut self) -> &mut Self {
self.force_largesize_mdat = true;
self
}
pub fn set_color_metadata(&mut self, color_metadata: ColorMetadata) -> &mut Self {
self.color_metadata = color_metadata;
self
}
pub fn add_packet(&mut self, packet: EncodedPacket) -> Result<()> {
if self.first_packet_header.is_none() {
self.first_packet_header = Some(packet.data.to_vec());
}
let size = packet.data.len() as u32;
self.mdat_writer
.write_all(&packet.data)
.context("writing packet to mdat tempfile")?;
self.sample_sizes.push(size);
self.packet_count = self
.packet_count
.checked_add(1)
.context("packet count overflow")?;
if packet.is_keyframe {
self.keyframe_indices.push(self.packet_count);
}
self.mdat_payload_bytes = self
.mdat_payload_bytes
.checked_add(size as u64)
.context("mdat payload overflow")?;
Ok(())
}
pub fn with_audio(&mut self, info: AudioInfo) -> Result<&mut Self> {
let codec_kind = AudioCodecKind::from_codec_tag(&info.codec).ok_or_else(|| {
anyhow::anyhow!(
"audio mux: only AAC-LC, Opus, AC-3, E-AC-3 are supported; got codec '{}'",
info.codec
)
})?;
match codec_kind {
AudioCodecKind::Aac => {
if !matches!(info.channels, 1 | 2 | 6 | 7) {
anyhow::bail!(
"audio mux: AAC supports mono/stereo/5.1(channels=6)/7.1(channels=7) layouts; \
got {} channels — extended Atmos / object layouts are not supported",
info.channels
);
}
}
AudioCodecKind::Opus => {
if info.channels < 1 || info.channels > 8 {
anyhow::bail!(
"audio mux: Opus supports 1..=8 channels; got {}",
info.channels
);
}
}
AudioCodecKind::Ac3 | AudioCodecKind::Eac3 => {
if !(1..=6).contains(&info.channels) {
anyhow::bail!(
"audio mux: AC-3 / E-AC-3 channel count must be 1..=6 (mono..5.1); got {}",
info.channels
);
}
}
}
if info.sample_rate == 0 {
anyhow::bail!("audio mux: sample_rate must be > 0");
}
if info.timescale == 0 {
anyhow::bail!("audio mux: timescale must be > 0");
}
match codec_kind {
AudioCodecKind::Aac => {
if info.asc_bytes.is_empty() {
anyhow::bail!("audio mux: AudioSpecificConfig bytes missing");
}
let parsed = crate::aac_asc::parse_aac_asc(&info.asc_bytes)
.with_context(|| "audio mux: failed to parse AudioSpecificConfig")?;
use crate::aac_asc::AscSignaling;
match parsed.signaling {
AscSignaling::ImplicitMaybe => {
anyhow::bail!(
"audio mux: ASC uses implicit HE-AAC signaling (AOT=2 core at \
{} Hz with no SBR/PS layer in the ASC). Apple players silently \
downgrade to mono 22.05 kHz core. Caller must upgrade with \
aac_asc::upgrade_to_explicit_signaling before muxing.",
parsed.sample_rate
);
}
AscSignaling::NoExtension
| AscSignaling::ExplicitSbr
| AscSignaling::ExplicitPs => {
let core_aot = parsed.aot;
if !matches!(core_aot, 2 | 42) {
anyhow::bail!(
"audio mux: only AAC-LC (AOT=2) and xHE-AAC USAC (AOT=42) \
cores are supported; ASC core AOT={}",
core_aot
);
}
}
}
}
AudioCodecKind::Opus => {
if info.codec_private.len() < 11 {
anyhow::bail!(
"audio mux: Opus codec_private must be ≥11 bytes (RFC 7845 §5.1 \
minimum body for ChannelMappingFamily=0); got {} bytes",
info.codec_private.len()
);
}
if info.timescale != 48_000 {
anyhow::bail!(
"audio mux: Opus mdhd timescale must be 48000 (RFC 7845 §3); \
got timescale={}",
info.timescale
);
}
let cmf = info.codec_private[10];
match cmf {
0 => {
if info.channels > 2 {
anyhow::bail!(
"audio mux: Opus ChannelMappingFamily=0 only supports 1..=2 channels; got {}",
info.channels
);
}
}
1 => {
let n = info.channels as usize;
let needed = 11 + 2 + n;
if info.codec_private.len() < needed {
anyhow::bail!(
"audio mux: Opus family=1 codec_private must be ≥{needed} bytes \
(11 preamble + 2 stream/coupled + {n} mapping); got {}",
info.codec_private.len()
);
}
let stream_count = info.codec_private[11];
let coupled_count = info.codec_private[12];
if stream_count < 1 {
anyhow::bail!(
"audio mux: Opus family=1 StreamCount must be >= 1; got {stream_count}"
);
}
if coupled_count > stream_count {
anyhow::bail!(
"audio mux: Opus family=1 CoupledCount ({coupled_count}) > StreamCount ({stream_count})"
);
}
if (stream_count as u16) + (coupled_count as u16) > info.channels {
anyhow::bail!(
"audio mux: Opus family=1 StreamCount ({stream_count}) + CoupledCount ({coupled_count}) > channels ({})",
info.channels
);
}
let mapping_max = stream_count + coupled_count;
for i in 0..n {
let m = info.codec_private[13 + i];
if m >= mapping_max {
anyhow::bail!(
"audio mux: Opus family=1 ChannelMapping[{i}]={m} \
exceeds streams+coupled ({mapping_max})"
);
}
}
}
other => {
anyhow::bail!(
"audio mux: only Opus ChannelMappingFamily 0 (mono/stereo) and 1 (surround 1..=8) supported; \
got family={other}"
);
}
}
}
AudioCodecKind::Ac3 => {
if info.codec_private.len() != 3 {
anyhow::bail!(
"audio mux: AC-3 codec_private (dac3 body) must be exactly 3 bytes \
per ETSI TS 102 366 §F.4; got {} bytes",
info.codec_private.len()
);
}
match info.sample_rate {
32_000 | 44_100 | 48_000 => {}
other => anyhow::bail!(
"audio mux: AC-3 sample_rate must be 32000 / 44100 / 48000; got {}",
other
),
}
}
AudioCodecKind::Eac3 => {
if info.codec_private.len() < 5 {
anyhow::bail!(
"audio mux: E-AC-3 codec_private (dec3 body) must be ≥5 bytes \
per ETSI TS 102 366 §F.6; got {} bytes",
info.codec_private.len()
);
}
match info.sample_rate {
16_000 | 22_050 | 24_000 | 32_000 | 44_100 | 48_000 => {}
other => anyhow::bail!(
"audio mux: E-AC-3 sample_rate must be 16000 / 22050 / 24000 / 32000 / \
44100 / 48000; got {}",
other
),
}
}
}
if self.audio.is_some() {
anyhow::bail!("audio mux: with_audio called twice");
}
let audio_tmp = NamedTempFile::new().context("creating audio mdat tempfile")?;
let handle = audio_tmp
.reopen()
.context("reopening audio tempfile for write")?;
let audio_writer = BufWriter::new(handle);
self.audio = Some(AudioTrackState {
info,
audio_tmp,
audio_writer,
sample_sizes: Vec::new(),
durations: Vec::new(),
total_duration_ticks: 0,
mdat_payload_bytes: 0,
});
Ok(self)
}
pub fn add_audio_sample(
&mut self,
sample: &[u8],
_pts_ticks: u64,
duration_ticks: u32,
) -> Result<()> {
let audio = self
.audio
.as_mut()
.context("audio mux: add_audio_sample called before with_audio")?;
if sample.is_empty() {
anyhow::bail!("audio mux: refusing to add empty audio access unit");
}
audio
.audio_writer
.write_all(sample)
.context("writing audio sample to tempfile")?;
audio.sample_sizes.push(sample.len() as u32);
let dur = if duration_ticks == 0 {
match AudioCodecKind::from_codec_tag(&audio.info.codec) {
Some(AudioCodecKind::Aac) => 1024,
Some(AudioCodecKind::Opus) => 960,
Some(AudioCodecKind::Ac3) | Some(AudioCodecKind::Eac3) => 1536,
None => 1024, }
} else {
duration_ticks
};
audio.durations.push(dur);
audio.total_duration_ticks = audio
.total_duration_ticks
.checked_add(dur as u64)
.context("audio total duration overflow")?;
audio.mdat_payload_bytes = audio
.mdat_payload_bytes
.checked_add(sample.len() as u64)
.context("audio mdat payload overflow")?;
Ok(())
}
pub fn finalize_to_file(mut self, output_path: &Path) -> Result<()> {
if self.packet_count == 0 {
anyhow::bail!("cannot finalize MP4 with zero packets");
}
self.mdat_writer.flush().context("flushing mdat tempfile")?;
if let Some(ref mut audio) = self.audio {
audio
.audio_writer
.flush()
.context("flushing audio mdat tempfile")?;
if audio.sample_sizes.is_empty() {
tracing::warn!(
"audio mux: with_audio called but no samples pushed; dropping audio"
);
self.audio = None;
}
}
let video_timescale: u32 = 90_000;
let frame_duration: u32 = ((video_timescale as f64) / self.frame_rate)
.round()
.max(1.0) as u32;
let total_video_duration: u64 = frame_duration as u64 * self.packet_count as u64;
let first_packet = self
.first_packet_header
.as_ref()
.context("first packet header missing; add_packet never called?")?;
let config_obus = extract_sequence_header(first_packet)
.context("extracting AV1 sequence header OBU from first packet")?;
let ftyp = build_ftyp();
let video_spc: u32 = (self.frame_rate.round() as u32).max(1).min(120);
let movie_timescale: u32 = video_timescale;
let audio_plan: Option<AudioBuildPlan> = self.audio.as_ref().map(|a| {
let frames_per_sec = match AudioCodecKind::from_codec_tag(&a.info.codec) {
Some(AudioCodecKind::Opus) => (a.info.timescale as f64) / 960.0,
Some(AudioCodecKind::Ac3) | Some(AudioCodecKind::Eac3) => {
(a.info.timescale as f64) / 1536.0
}
Some(AudioCodecKind::Aac) | None => (a.info.timescale as f64) / 1024.0,
};
let audio_spc = (frames_per_sec.round() as u32).max(1).min(200);
let audio_duration_movie: u64 =
((a.total_duration_ticks as u128) * movie_timescale as u128
/ a.info.timescale.max(1) as u128) as u64;
AudioBuildPlan {
info: a.info.clone(),
sample_sizes: a.sample_sizes.clone(),
durations: a.durations.clone(),
total_duration_in_own_ts: a.total_duration_ticks,
total_duration_in_movie_ts: audio_duration_movie,
samples_per_chunk: audio_spc,
}
});
let video_duration_movie: u64 = total_video_duration; let movie_duration: u64 = match audio_plan.as_ref() {
Some(p) => video_duration_movie.max(p.total_duration_in_movie_ts),
None => video_duration_movie,
};
let video_payload_bytes = self.mdat_payload_bytes;
let audio_payload_bytes = audio_plan
.as_ref()
.map(|p| p.sample_sizes.iter().map(|&s| s as u64).sum::<u64>())
.unwrap_or(0);
let mdat_payload_total = video_payload_bytes
.checked_add(audio_payload_bytes)
.context("combined mdat payload overflow")?;
let mdat_payload_plus_short_header = 8u64
.checked_add(mdat_payload_total)
.context("mdat short-header size overflow")?;
let use_largesize_mdat =
mdat_payload_plus_short_header > u32::MAX as u64 || self.force_largesize_mdat;
let mdat_header_len: u64 = if use_largesize_mdat { 16 } else { 8 };
let mdat_box_size: u64 = mdat_header_len
.checked_add(mdat_payload_total)
.context("mdat box size overflow")?;
let video_chunk_count = chunk_count_of(self.sample_sizes.len(), video_spc);
let audio_chunk_count = audio_plan
.as_ref()
.map(|p| chunk_count_of(p.sample_sizes.len(), p.samples_per_chunk))
.unwrap_or(0);
let video_zero_offsets: Vec<u64> = vec![0; video_chunk_count];
let audio_zero_offsets: Vec<u64> = vec![0; audio_chunk_count];
let moov_co64_size = build_moov_any(
self.width,
self.height,
video_timescale,
movie_timescale,
movie_duration,
total_video_duration,
frame_duration,
&self.sample_sizes,
&self.keyframe_indices,
&config_obus,
&video_zero_offsets,
video_spc,
audio_plan.as_ref(),
&audio_zero_offsets,
true,
&self.color_metadata,
)
.len() as u64;
let upper_bound: u64 = (ftyp.len() as u64)
.checked_add(moov_co64_size)
.context("moov size overflow")?
.checked_add(mdat_header_len)
.context("mdat header overflow")?
.checked_add(mdat_payload_total)
.context("mdat payload overflow")?;
let use_co64 = upper_bound > u32::MAX as u64;
let moov_without_offsets = build_moov_any(
self.width,
self.height,
video_timescale,
movie_timescale,
movie_duration,
total_video_duration,
frame_duration,
&self.sample_sizes,
&self.keyframe_indices,
&config_obus,
&video_zero_offsets,
video_spc,
audio_plan.as_ref(),
&audio_zero_offsets,
use_co64,
&self.color_metadata,
);
let mdat_offset_in_file = (ftyp.len() + moov_without_offsets.len()) as u64;
let first_sample_file_offset = mdat_offset_in_file + mdat_header_len;
if !use_co64 && first_sample_file_offset > u32::MAX as u64 {
anyhow::bail!(
"internal: chose stco but first_sample_file_offset {} exceeds u32",
first_sample_file_offset
);
}
let (video_chunk_offsets, audio_chunk_offsets, interleave_plan) = plan_interleaved_layout(
first_sample_file_offset,
&self.sample_sizes,
video_spc,
audio_plan.as_ref(),
);
debug_assert_eq!(video_chunk_offsets.len(), video_chunk_count);
debug_assert_eq!(audio_chunk_offsets.len(), audio_chunk_count);
let moov = build_moov_any(
self.width,
self.height,
video_timescale,
movie_timescale,
movie_duration,
total_video_duration,
frame_duration,
&self.sample_sizes,
&self.keyframe_indices,
&config_obus,
&video_chunk_offsets,
video_spc,
audio_plan.as_ref(),
&audio_chunk_offsets,
use_co64,
&self.color_metadata,
);
assert_eq!(
moov.len(),
moov_without_offsets.len(),
"moov size must be stable across rebuild"
);
let out_file = File::create(output_path)
.with_context(|| format!("creating output file {}", output_path.display()))?;
let mut out = BufWriter::new(out_file);
out.write_all(&ftyp).context("writing ftyp")?;
out.write_all(&moov).context("writing moov")?;
if use_largesize_mdat {
out.write_all(&1u32.to_be_bytes())
.context("writing mdat largesize sentinel")?;
out.write_all(b"mdat").context("writing mdat type")?;
out.write_all(&mdat_box_size.to_be_bytes())
.context("writing mdat largesize")?;
} else {
let mdat_size_u32 = mdat_box_size as u32;
out.write_all(&mdat_size_u32.to_be_bytes())
.context("writing mdat size")?;
out.write_all(b"mdat").context("writing mdat type")?;
}
let video_payload_handle = self
.mdat_tmp
.reopen()
.context("reopening mdat tempfile for read")?;
let mut video_payload = BufReader::new(video_payload_handle);
video_payload
.seek(SeekFrom::Start(0))
.context("rewinding mdat tempfile")?;
let mut audio_payload: Option<BufReader<File>> = match self.audio.as_ref() {
Some(a) => {
let h = a
.audio_tmp
.reopen()
.context("reopening audio mdat tempfile for read")?;
let mut r = BufReader::new(h);
r.seek(SeekFrom::Start(0))
.context("rewinding audio mdat tempfile")?;
Some(r)
}
None => None,
};
let mut video_copied: u64 = 0;
let mut audio_copied: u64 = 0;
for step in &interleave_plan {
match step.track {
InterleaveTrack::Video => {
let copied =
std::io::copy(&mut (&mut video_payload).take(step.bytes), &mut out)
.context("copying video chunk into mdat")?;
if copied != step.bytes {
anyhow::bail!(
"video chunk short read: wanted {}, got {}",
step.bytes,
copied
);
}
video_copied += copied;
}
InterleaveTrack::Audio => {
let audio_r = audio_payload.as_mut().context(
"internal: interleave plan has audio step but no audio tempfile",
)?;
let copied = std::io::copy(&mut audio_r.take(step.bytes), &mut out)
.context("copying audio chunk into mdat")?;
if copied != step.bytes {
anyhow::bail!(
"audio chunk short read: wanted {}, got {}",
step.bytes,
copied
);
}
audio_copied += copied;
}
}
}
if video_copied != video_payload_bytes {
anyhow::bail!(
"video mdat payload length mismatch: expected {}, copied {}",
video_payload_bytes,
video_copied
);
}
if audio_copied != audio_payload_bytes {
anyhow::bail!(
"audio mdat payload length mismatch: expected {}, copied {}",
audio_payload_bytes,
audio_copied
);
}
out.flush().context("flushing output")?;
Ok(())
}
pub fn finalize(self) -> Result<Bytes> {
let tmp = NamedTempFile::new().context("creating finalize buffer tempfile")?;
let path = tmp.path().to_path_buf();
self.finalize_to_file(&path)?;
let mut f = File::open(&path).context("reopening finalize buffer tempfile")?;
let mut buf = Vec::new();
f.read_to_end(&mut buf).context("reading finalize buffer")?;
Ok(Bytes::from(buf))
}
}
struct AudioBuildPlan {
info: AudioInfo,
sample_sizes: Vec<u32>,
durations: Vec<u32>,
total_duration_in_own_ts: u64,
total_duration_in_movie_ts: u64,
samples_per_chunk: u32,
}
#[derive(Debug, Clone, Copy)]
struct InterleaveStep {
track: InterleaveTrack,
bytes: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InterleaveTrack {
Video,
Audio,
}
fn chunk_count_of(sample_count: usize, spc: u32) -> usize {
if sample_count == 0 {
return 0;
}
let spc = spc.max(1) as usize;
sample_count.div_ceil(spc)
}
fn chunk_byte_sizes(sample_sizes: &[u32], spc: u32) -> Vec<u64> {
let spc = spc.max(1) as usize;
let mut out = Vec::new();
let mut i = 0usize;
while i < sample_sizes.len() {
let end = (i + spc).min(sample_sizes.len());
let mut total: u64 = 0;
for &s in &sample_sizes[i..end] {
total += s as u64;
}
out.push(total);
i = end;
}
out
}
fn plan_interleaved_layout(
first_sample_file_offset: u64,
video_sample_sizes: &[u32],
video_spc: u32,
audio_plan: Option<&AudioBuildPlan>,
) -> (Vec<u64>, Vec<u64>, Vec<InterleaveStep>) {
let video_chunks = chunk_byte_sizes(video_sample_sizes, video_spc);
let audio_chunks = match audio_plan {
Some(p) => chunk_byte_sizes(&p.sample_sizes, p.samples_per_chunk),
None => Vec::new(),
};
let mut video_offsets: Vec<u64> = Vec::with_capacity(video_chunks.len());
let mut audio_offsets: Vec<u64> = Vec::with_capacity(audio_chunks.len());
let mut plan: Vec<InterleaveStep> = Vec::with_capacity(video_chunks.len() + audio_chunks.len());
let mut cursor = first_sample_file_offset;
let mut vi = 0usize;
let mut ai = 0usize;
loop {
if vi < video_chunks.len() {
video_offsets.push(cursor);
let size = video_chunks[vi];
plan.push(InterleaveStep {
track: InterleaveTrack::Video,
bytes: size,
});
cursor = cursor.saturating_add(size);
vi += 1;
}
if ai < audio_chunks.len() {
audio_offsets.push(cursor);
let size = audio_chunks[ai];
plan.push(InterleaveStep {
track: InterleaveTrack::Audio,
bytes: size,
});
cursor = cursor.saturating_add(size);
ai += 1;
}
if vi >= video_chunks.len() && ai >= audio_chunks.len() {
break;
}
}
(video_offsets, audio_offsets, plan)
}
fn build_ftyp() -> Vec<u8> {
let mut b = BoxBuilder::new(b"ftyp");
b.extend(b"iso6"); b.u32(512); b.extend(b"iso6"); b.extend(b"iso2"); b.extend(b"av01"); b.extend(b"mp41"); b.extend(b"mp42"); b.finish()
}
#[cfg(test)]
fn build_moov(
width: u32,
height: u32,
timescale: u32,
duration: u64,
frame_duration: u32,
sample_sizes: &[u32],
keyframe_indices: &[u32],
config_obus: &[u8],
chunk_offsets: &[u64],
samples_per_chunk: u32,
use_co64: bool,
) -> Vec<u8> {
build_moov_any(
width,
height,
timescale,
timescale,
duration,
duration,
frame_duration,
sample_sizes,
keyframe_indices,
config_obus,
chunk_offsets,
samples_per_chunk,
None,
&[],
use_co64,
&ColorMetadata::default(),
)
}
fn build_moov_any(
width: u32,
height: u32,
video_timescale: u32,
movie_timescale: u32,
movie_duration: u64,
video_duration_in_video_ts: u64,
frame_duration: u32,
sample_sizes: &[u32],
keyframe_indices: &[u32],
config_obus: &[u8],
video_chunk_offsets: &[u64],
video_spc: u32,
audio_plan: Option<&AudioBuildPlan>,
audio_chunk_offsets: &[u64],
use_co64: bool,
color_metadata: &ColorMetadata,
) -> Vec<u8> {
let next_track_id: u32 = if audio_plan.is_some() { 3 } else { 2 };
let mvhd = build_mvhd_v2(movie_timescale, movie_duration, next_track_id);
let video_duration_movie: u64 = if video_timescale == movie_timescale {
video_duration_in_video_ts
} else {
((video_duration_in_video_ts as u128) * movie_timescale as u128
/ video_timescale.max(1) as u128) as u64
};
let video_trak = build_video_trak(
width,
height,
video_timescale,
video_duration_movie,
video_duration_in_video_ts,
frame_duration,
sample_sizes,
keyframe_indices,
config_obus,
video_chunk_offsets,
video_spc,
use_co64,
color_metadata,
);
let mut b = BoxBuilder::new(b"moov");
b.extend(&mvhd);
b.extend(&video_trak);
if let Some(plan) = audio_plan {
let audio_trak = build_audio_trak(
plan,
plan.total_duration_in_movie_ts,
audio_chunk_offsets,
use_co64,
);
b.extend(&audio_trak);
}
b.finish()
}
fn build_mvhd_v2(timescale: u32, duration: u64, next_track_id: u32) -> Vec<u8> {
let mut b = BoxBuilder::new(b"mvhd");
b.u8(0); b.extend(&[0, 0, 0]); b.u32(0); b.u32(0); b.u32(timescale);
b.u32(duration as u32);
b.u32(0x00010000); b.u16(0x0100); b.u16(0); b.u32(0); b.u32(0);
write_unity_matrix(&mut b);
for _ in 0..6 {
b.u32(0);
} b.u32(next_track_id);
b.finish()
}
fn build_video_trak(
width: u32,
height: u32,
mdhd_timescale: u32,
duration_in_movie_ts: u64,
duration_in_mdhd_ts: u64,
frame_duration: u32,
sample_sizes: &[u32],
keyframe_indices: &[u32],
config_obus: &[u8],
chunk_offsets: &[u64],
samples_per_chunk: u32,
use_co64: bool,
color_metadata: &ColorMetadata,
) -> Vec<u8> {
let tkhd = build_video_tkhd(width, height, duration_in_movie_ts);
let mdia = build_video_mdia(
width,
height,
mdhd_timescale,
duration_in_mdhd_ts,
frame_duration,
sample_sizes,
keyframe_indices,
config_obus,
chunk_offsets,
samples_per_chunk,
use_co64,
color_metadata,
);
let mut b = BoxBuilder::new(b"trak");
b.extend(&tkhd);
b.extend(&mdia);
b.finish()
}
fn build_video_tkhd(width: u32, height: u32, duration: u64) -> Vec<u8> {
let mut b = BoxBuilder::new(b"tkhd");
b.u8(0); b.extend(&[0, 0, 0x03]); b.u32(0); b.u32(0); b.u32(1); b.u32(0); b.u32(duration as u32);
b.u32(0); b.u32(0);
b.u16(0); b.u16(0); b.u16(0); b.u16(0); write_unity_matrix(&mut b);
b.u32(width << 16); b.u32(height << 16);
b.finish()
}
fn build_video_mdia(
width: u32,
height: u32,
timescale: u32,
duration: u64,
frame_duration: u32,
sample_sizes: &[u32],
keyframe_indices: &[u32],
config_obus: &[u8],
chunk_offsets: &[u64],
samples_per_chunk: u32,
use_co64: bool,
color_metadata: &ColorMetadata,
) -> Vec<u8> {
let mdhd = build_mdhd(timescale, duration);
let hdlr = build_video_hdlr();
let minf = build_minf(
width,
height,
frame_duration,
sample_sizes,
keyframe_indices,
config_obus,
chunk_offsets,
samples_per_chunk,
use_co64,
color_metadata,
);
let mut b = BoxBuilder::new(b"mdia");
b.extend(&mdhd);
b.extend(&hdlr);
b.extend(&minf);
b.finish()
}
fn build_mdhd(timescale: u32, duration: u64) -> Vec<u8> {
let mut b = BoxBuilder::new(b"mdhd");
b.u8(0); b.extend(&[0, 0, 0]); b.u32(0); b.u32(0); b.u32(timescale);
b.u32(duration as u32);
b.u16(0x55c4); b.u16(0); b.finish()
}
fn build_video_hdlr() -> Vec<u8> {
let mut b = BoxBuilder::new(b"hdlr");
b.u8(0); b.extend(&[0, 0, 0]); b.u32(0); b.extend(b"vide"); b.u32(0); b.u32(0); b.u32(0); b.extend(b"VideoHandler\0");
b.finish()
}
fn build_audio_trak(
plan: &AudioBuildPlan,
duration_in_movie_ts: u64,
chunk_offsets: &[u64],
use_co64: bool,
) -> Vec<u8> {
let tkhd = build_audio_tkhd(duration_in_movie_ts);
let mdia = build_audio_mdia(plan, chunk_offsets, use_co64);
let mut b = BoxBuilder::new(b"trak");
b.extend(&tkhd);
b.extend(&mdia);
b.finish()
}
fn build_audio_tkhd(duration_in_movie_ts: u64) -> Vec<u8> {
let mut b = BoxBuilder::new(b"tkhd");
b.u8(0); b.extend(&[0, 0, 0x03]); b.u32(0); b.u32(0); b.u32(2); b.u32(0); b.u32(duration_in_movie_ts as u32);
b.u32(0); b.u32(0);
b.u16(0); b.u16(0x0001); b.u16(0x0100); b.u16(0); write_unity_matrix(&mut b);
b.u32(0); b.u32(0); b.finish()
}
fn build_audio_mdia(plan: &AudioBuildPlan, chunk_offsets: &[u64], use_co64: bool) -> Vec<u8> {
let mdhd = build_mdhd(plan.info.timescale, plan.total_duration_in_own_ts);
let hdlr = build_audio_hdlr();
let minf = build_audio_minf(plan, chunk_offsets, use_co64);
let mut b = BoxBuilder::new(b"mdia");
b.extend(&mdhd);
b.extend(&hdlr);
b.extend(&minf);
b.finish()
}
fn build_audio_hdlr() -> Vec<u8> {
let mut b = BoxBuilder::new(b"hdlr");
b.u8(0);
b.extend(&[0, 0, 0]);
b.u32(0); b.extend(b"soun"); b.u32(0);
b.u32(0);
b.u32(0);
b.extend(b"SoundHandler\0");
b.finish()
}
fn build_audio_minf(plan: &AudioBuildPlan, chunk_offsets: &[u64], use_co64: bool) -> Vec<u8> {
let smhd = build_smhd();
let dinf = build_dinf();
let stbl = build_audio_stbl(plan, chunk_offsets, use_co64);
let mut b = BoxBuilder::new(b"minf");
b.extend(&smhd);
b.extend(&dinf);
b.extend(&stbl);
b.finish()
}
fn build_smhd() -> Vec<u8> {
let mut b = BoxBuilder::new(b"smhd");
b.u8(0);
b.extend(&[0, 0, 0]); b.u16(0); b.u16(0); b.finish()
}
fn build_audio_stbl(plan: &AudioBuildPlan, chunk_offsets: &[u64], use_co64: bool) -> Vec<u8> {
let stsd = build_audio_stsd(&plan.info);
let stts = build_audio_stts(&plan.durations);
let stsc = build_stsc(plan.sample_sizes.len() as u32, plan.samples_per_chunk);
let stsz = build_stsz(&plan.sample_sizes);
let chunk_offset_box = if use_co64 {
build_co64(chunk_offsets)
} else {
build_stco(chunk_offsets)
};
let mut b = BoxBuilder::new(b"stbl");
b.extend(&stsd);
b.extend(&stts);
b.extend(&stsc);
b.extend(&stsz);
b.extend(&chunk_offset_box);
b.finish()
}
pub(crate) fn build_audio_stsd(info: &AudioInfo) -> Vec<u8> {
let kind = AudioCodecKind::from_codec_tag(&info.codec)
.expect("with_audio gate already validated codec tag");
let entry = match kind {
AudioCodecKind::Aac => build_mp4a(info),
AudioCodecKind::Opus => build_opus_sample_entry(info),
AudioCodecKind::Ac3 => build_ac3_sample_entry(info),
AudioCodecKind::Eac3 => build_ec3_sample_entry(info),
};
let mut b = BoxBuilder::new(b"stsd");
b.u8(0);
b.extend(&[0, 0, 0]);
b.u32(1); b.extend(&entry);
b.finish()
}
fn build_mp4a(info: &AudioInfo) -> Vec<u8> {
let mut b = BoxBuilder::new(b"mp4a");
for _ in 0..6 {
b.u8(0);
} b.u16(1); b.u32(0); b.u32(0); b.u16(info.channels); b.u16(16); b.u16(0); b.u16(0); b.u32(info.sample_rate << 16); b.extend(&build_esds(info));
if let Some(chan) = build_chan_box(info.channels) {
b.extend(&chan);
}
b.finish()
}
pub(crate) fn build_chan_box(channels: u16) -> Option<Vec<u8>> {
let tag: u32 = match channels {
1 | 2 => return None, 6 => (114u32 << 16) | 6, 7 => (127u32 << 16) | 8, _ => return None, };
let mut b = BoxBuilder::new(b"chan");
b.u32(tag); b.u32(0); b.u32(0); Some(b.finish())
}
fn build_opus_sample_entry(info: &AudioInfo) -> Vec<u8> {
let mut b = BoxBuilder::new(b"Opus");
for _ in 0..6 {
b.u8(0);
} b.u16(1); b.u32(0); b.u32(0); b.u16(info.channels); b.u16(16); b.u16(0); b.u16(0); b.u32(48_000u32 << 16); b.extend(&build_dops(info));
b.finish()
}
fn build_dops(info: &AudioInfo) -> Vec<u8> {
let p = &info.codec_private;
debug_assert!(
p.len() >= 11,
"with_audio gate must enforce dOps minimum size"
);
let output_channels = p[1];
let pre_skip = u16::from_le_bytes([p[2], p[3]]);
let input_sample_rate = u32::from_le_bytes([p[4], p[5], p[6], p[7]]);
let output_gain = i16::from_le_bytes([p[8], p[9]]);
let channel_mapping_family = p[10];
let mut b = BoxBuilder::new(b"dOps");
b.u8(0); b.u8(output_channels); b.u16(pre_skip); b.u32(input_sample_rate); b.u16(output_gain as u16); b.u8(channel_mapping_family);
if channel_mapping_family != 0 {
let trailer_len = 2 + output_channels as usize;
debug_assert!(
p.len() >= 11 + trailer_len,
"family={channel_mapping_family} requires {trailer_len} more bytes after the 11-byte preamble; codec_private has {}",
p.len()
);
b.u8(p[11]); b.u8(p[12]); for i in 0..output_channels as usize {
b.u8(p[13 + i]); }
}
b.finish()
}
fn build_ac3_sample_entry(info: &AudioInfo) -> Vec<u8> {
let mut b = BoxBuilder::new(b"ac-3");
for _ in 0..6 {
b.u8(0);
} b.u16(1); b.u32(0); b.u32(0); b.u16(info.channels); b.u16(16); b.u16(0); b.u16(0); b.u32(info.sample_rate << 16); b.extend(&build_dac3(info)); b.finish()
}
fn build_ec3_sample_entry(info: &AudioInfo) -> Vec<u8> {
let mut b = BoxBuilder::new(b"ec-3");
for _ in 0..6 {
b.u8(0);
}
b.u16(1);
b.u32(0);
b.u32(0);
b.u16(info.channels);
b.u16(16);
b.u16(0);
b.u16(0);
b.u32(info.sample_rate << 16);
b.extend(&build_dec3(info));
b.finish()
}
fn build_dac3(info: &AudioInfo) -> Vec<u8> {
debug_assert_eq!(
info.codec_private.len(),
3,
"with_audio gate must enforce dac3 body == 3 bytes"
);
let mut b = BoxBuilder::new(b"dac3");
b.extend(&info.codec_private);
b.finish()
}
fn build_dec3(info: &AudioInfo) -> Vec<u8> {
debug_assert!(
info.codec_private.len() >= 5,
"with_audio gate must enforce dec3 body >= 5 bytes"
);
let mut b = BoxBuilder::new(b"dec3");
b.extend(&info.codec_private);
b.finish()
}
pub fn dac3_body_from_sync(s: &crate::ac3_sync::Ac3SyncInfo) -> [u8; 3] {
let mut bw = MsbBitWriter::new();
bw.put(2, s.fscod as u32);
bw.put(5, s.bsid as u32);
bw.put(3, s.bsmod as u32);
bw.put(3, s.acmod as u32);
bw.put(1, if s.lfeon { 1 } else { 0 });
bw.put(5, s.bit_rate_code as u32);
bw.put(5, 0); let bytes = bw.finish();
[bytes[0], bytes[1], bytes[2]]
}
pub fn dec3_body_from_sync(s: &crate::ac3_sync::Eac3SyncInfo, data_rate_div2_kbps: u16) -> [u8; 5] {
let mut bw = MsbBitWriter::new();
bw.put(13, (data_rate_div2_kbps & 0x1FFF) as u32);
bw.put(3, 0); bw.put(2, s.fscod as u32);
bw.put(5, 16); bw.put(1, 0); bw.put(1, 0); bw.put(3, s.bsmod as u32);
bw.put(3, s.acmod as u32);
bw.put(1, if s.lfeon { 1 } else { 0 });
bw.put(3, 0); bw.put(4, 0); let bytes = bw.finish();
debug_assert_eq!(bytes.len(), 5, "dec3 single-substream body must be 5 bytes");
[bytes[0], bytes[1], bytes[2], bytes[3], bytes[4]]
}
struct MsbBitWriter {
bytes: Vec<u8>,
bit_pos: usize,
}
impl MsbBitWriter {
fn new() -> Self {
Self {
bytes: Vec::new(),
bit_pos: 0,
}
}
fn put(&mut self, n: usize, v: u32) {
debug_assert!(n <= 24);
for i in (0..n).rev() {
let bit = ((v >> i) & 0x01) as u8;
if self.bit_pos.is_multiple_of(8) {
self.bytes.push(0);
}
let byte_idx = self.bit_pos / 8;
let bit_idx = 7 - (self.bit_pos % 8);
self.bytes[byte_idx] |= bit << bit_idx;
self.bit_pos += 1;
}
}
fn finish(self) -> Vec<u8> {
self.bytes
}
}
fn build_esds(info: &AudioInfo) -> Vec<u8> {
let asc_len = info.asc_bytes.len() as u32;
let mut dsi = Vec::new();
dsi.push(0x05u8);
write_descriptor_length(&mut dsi, asc_len);
dsi.extend_from_slice(&info.asc_bytes);
let mut dcd_payload = Vec::new();
dcd_payload.push(0x40); dcd_payload.push((0x05 << 2) | 0x01); dcd_payload.extend_from_slice(&[0, 0, 0]); dcd_payload.extend_from_slice(&0u32.to_be_bytes()); dcd_payload.extend_from_slice(&0u32.to_be_bytes()); dcd_payload.extend_from_slice(&dsi);
let mut dcd = Vec::new();
dcd.push(0x04);
write_descriptor_length(&mut dcd, dcd_payload.len() as u32);
dcd.extend_from_slice(&dcd_payload);
let mut slc = Vec::new();
slc.push(0x06);
write_descriptor_length(&mut slc, 1);
slc.push(0x02);
let mut es_payload = Vec::new();
es_payload.extend_from_slice(&0u16.to_be_bytes()); es_payload.push(0); es_payload.extend_from_slice(&dcd);
es_payload.extend_from_slice(&slc);
let mut es = Vec::new();
es.push(0x03);
write_descriptor_length(&mut es, es_payload.len() as u32);
es.extend_from_slice(&es_payload);
let mut b = BoxBuilder::new(b"esds");
b.u8(0);
b.extend(&[0, 0, 0]);
b.extend(&es);
b.finish()
}
fn write_descriptor_length(buf: &mut Vec<u8>, len: u32) {
if len < 128 {
buf.push(len as u8);
return;
}
buf.push(((len >> 21) & 0x7F) as u8 | 0x80);
buf.push(((len >> 14) & 0x7F) as u8 | 0x80);
buf.push(((len >> 7) & 0x7F) as u8 | 0x80);
buf.push((len & 0x7F) as u8);
}
fn build_audio_stts(durations: &[u32]) -> Vec<u8> {
let mut b = BoxBuilder::new(b"stts");
b.u8(0);
b.extend(&[0, 0, 0]);
let mut runs: Vec<(u32, u32)> = Vec::new();
for &d in durations {
if let Some(last) = runs.last_mut()
&& last.1 == d
{
last.0 += 1;
continue;
}
runs.push((1, d));
}
b.u32(runs.len() as u32);
for (count, delta) in runs {
b.u32(count);
b.u32(delta);
}
b.finish()
}
fn build_minf(
width: u32,
height: u32,
frame_duration: u32,
sample_sizes: &[u32],
keyframe_indices: &[u32],
config_obus: &[u8],
chunk_offsets: &[u64],
samples_per_chunk: u32,
use_co64: bool,
color_metadata: &ColorMetadata,
) -> Vec<u8> {
let vmhd = build_vmhd();
let dinf = build_dinf();
let stbl = build_stbl(
width,
height,
frame_duration,
sample_sizes,
keyframe_indices,
config_obus,
chunk_offsets,
samples_per_chunk,
use_co64,
color_metadata,
);
let mut b = BoxBuilder::new(b"minf");
b.extend(&vmhd);
b.extend(&dinf);
b.extend(&stbl);
b.finish()
}
fn build_vmhd() -> Vec<u8> {
let mut b = BoxBuilder::new(b"vmhd");
b.u8(0);
b.extend(&[0, 0, 0x01]); b.u16(0); b.u16(0);
b.u16(0);
b.u16(0); b.finish()
}
fn build_dinf() -> Vec<u8> {
let mut dref = BoxBuilder::new(b"dref");
dref.u8(0);
dref.extend(&[0, 0, 0]);
dref.u32(1); let mut url = BoxBuilder::new(b"url ");
url.u8(0);
url.extend(&[0, 0, 0x01]); dref.extend(&url.finish());
let mut b = BoxBuilder::new(b"dinf");
b.extend(&dref.finish());
b.finish()
}
fn build_stbl(
width: u32,
height: u32,
frame_duration: u32,
sample_sizes: &[u32],
keyframe_indices: &[u32],
config_obus: &[u8],
chunk_offsets: &[u64],
samples_per_chunk: u32,
use_co64: bool,
color_metadata: &ColorMetadata,
) -> Vec<u8> {
let stsd = build_stsd(width, height, config_obus, color_metadata);
let stts = build_stts(sample_sizes.len() as u32, frame_duration);
let stsc = build_stsc(sample_sizes.len() as u32, samples_per_chunk);
let stsz = build_stsz(sample_sizes);
let chunk_offset_box = if use_co64 {
build_co64(chunk_offsets)
} else {
build_stco(chunk_offsets)
};
let stss_box = if !keyframe_indices.is_empty() && keyframe_indices.len() < sample_sizes.len() {
Some(build_stss(keyframe_indices))
} else {
None
};
let mut b = BoxBuilder::new(b"stbl");
b.extend(&stsd);
b.extend(&stts);
if let Some(ss) = &stss_box {
b.extend(ss);
}
b.extend(&stsc);
b.extend(&stsz);
b.extend(&chunk_offset_box);
b.finish()
}
fn build_stsd(
width: u32,
height: u32,
config_obus: &[u8],
color_metadata: &ColorMetadata,
) -> Vec<u8> {
let av01 = build_av01(width, height, config_obus, color_metadata);
let mut b = BoxBuilder::new(b"stsd");
b.u8(0);
b.extend(&[0, 0, 0]); b.u32(1); b.extend(&av01);
b.finish()
}
pub(crate) fn build_av01(
width: u32,
height: u32,
config_obus: &[u8],
color_metadata: &ColorMetadata,
) -> Vec<u8> {
let av1c = build_av1c(config_obus);
let colr = build_colr_nclx(color_metadata);
let mdcv = color_metadata.mastering_display.as_ref().map(build_mdcv);
let clli = color_metadata.content_light_level.as_ref().map(build_clli);
let mut b = BoxBuilder::new(b"av01");
for _ in 0..6 {
b.u8(0);
} b.u16(1); b.u16(0); b.u16(0); for _ in 0..3 {
b.u32(0);
} b.u16(width as u16);
b.u16(height as u16);
b.u32(0x00480000); b.u32(0x00480000); b.u32(0); b.u16(1); b.u8(0);
for _ in 0..31 {
b.u8(0);
}
b.u16(0x0018); b.u16(0xFFFF); b.extend(&av1c);
b.extend(&colr);
if let Some(mdcv) = &mdcv {
b.extend(mdcv);
}
if let Some(clli) = &clli {
b.extend(clli);
}
b.finish()
}
fn transfer_to_h273(transfer: codec::frame::TransferFn) -> u8 {
use codec::frame::TransferFn;
match transfer {
TransferFn::Bt709 => 1,
TransferFn::Bt470Bg => 4,
TransferFn::Linear => 8,
TransferFn::St2084 => 16,
TransferFn::AribStdB67 => 18,
TransferFn::Unspecified => 2,
}
}
fn build_colr_nclx(color_metadata: &ColorMetadata) -> Vec<u8> {
let mut b = BoxBuilder::new(b"colr");
b.extend(b"nclx");
b.u16(color_metadata.colour_primaries as u16);
b.u16(transfer_to_h273(color_metadata.transfer) as u16);
b.u16(color_metadata.matrix_coefficients as u16);
let full_range_byte: u8 = if color_metadata.full_range {
0x80
} else {
0x00
};
b.u8(full_range_byte);
b.finish()
}
fn build_mdcv(md: &codec::frame::MasteringDisplay) -> Vec<u8> {
let mut b = BoxBuilder::new(b"mdcv");
b.u16(md.primaries_r_x);
b.u16(md.primaries_r_y);
b.u16(md.primaries_g_x);
b.u16(md.primaries_g_y);
b.u16(md.primaries_b_x);
b.u16(md.primaries_b_y);
b.u16(md.white_point_x);
b.u16(md.white_point_y);
b.u32(md.max_luminance);
b.u32(md.min_luminance);
b.finish()
}
fn build_clli(cll: &codec::frame::ContentLightLevel) -> Vec<u8> {
let mut b = BoxBuilder::new(b"clli");
b.u16(cll.max_cll);
b.u16(cll.max_fall);
b.finish()
}
fn build_av1c(config_obus: &[u8]) -> Vec<u8> {
let mut b = BoxBuilder::new(b"av1C");
b.u8(0x81);
let (
seq_profile,
seq_level_idx_0,
seq_tier_0,
high_bitdepth,
twelve_bit,
monochrome,
chroma_sub_x,
chroma_sub_y,
chroma_sample_position,
) = parse_seq_header_params(config_obus);
b.u8(((seq_profile & 0x7) << 5) | (seq_level_idx_0 & 0x1F));
let byte3 = ((seq_tier_0 & 0x1) << 7)
| ((high_bitdepth as u8 & 0x1) << 6)
| ((twelve_bit as u8 & 0x1) << 5)
| ((monochrome as u8 & 0x1) << 4)
| ((chroma_sub_x & 0x1) << 3)
| ((chroma_sub_y & 0x1) << 2)
| (chroma_sample_position & 0x3);
b.u8(byte3);
b.u8(0);
b.extend(config_obus);
b.finish()
}
fn build_stts(sample_count: u32, frame_duration: u32) -> Vec<u8> {
let mut b = BoxBuilder::new(b"stts");
b.u8(0);
b.extend(&[0, 0, 0]);
b.u32(1); b.u32(sample_count);
b.u32(frame_duration);
b.finish()
}
fn build_stss(keyframes: &[u32]) -> Vec<u8> {
let mut b = BoxBuilder::new(b"stss");
b.u8(0);
b.extend(&[0, 0, 0]);
b.u32(keyframes.len() as u32);
for &k in keyframes {
b.u32(k);
}
b.finish()
}
fn build_stsc(sample_count: u32, samples_per_chunk: u32) -> Vec<u8> {
let mut b = BoxBuilder::new(b"stsc");
b.u8(0);
b.extend(&[0, 0, 0]);
let spc = samples_per_chunk.max(1);
if sample_count == 0 {
b.u32(0);
return b.finish();
}
let full_chunks = sample_count / spc;
let remainder = sample_count % spc;
if remainder == 0 {
b.u32(1);
b.u32(1); b.u32(spc); b.u32(1); } else if full_chunks == 0 {
b.u32(1);
b.u32(1);
b.u32(remainder);
b.u32(1);
} else {
b.u32(2);
b.u32(1);
b.u32(spc);
b.u32(1);
b.u32(full_chunks + 1); b.u32(remainder);
b.u32(1);
}
b.finish()
}
fn build_stsz(sample_sizes: &[u32]) -> Vec<u8> {
let mut b = BoxBuilder::new(b"stsz");
b.u8(0);
b.extend(&[0, 0, 0]);
b.u32(0); b.u32(sample_sizes.len() as u32); for &s in sample_sizes {
b.u32(s);
}
b.finish()
}
fn build_stco(chunk_offsets: &[u64]) -> Vec<u8> {
let mut b = BoxBuilder::new(b"stco");
b.u8(0);
b.extend(&[0, 0, 0]);
b.u32(chunk_offsets.len() as u32);
for &off in chunk_offsets {
debug_assert!(
off <= u32::MAX as u64,
"stco offset exceeds u32; should be co64"
);
b.u32(off as u32);
}
b.finish()
}
fn build_co64(chunk_offsets: &[u64]) -> Vec<u8> {
let mut b = BoxBuilder::new(b"co64");
b.u8(0);
b.extend(&[0, 0, 0]);
b.u32(chunk_offsets.len() as u32);
for &off in chunk_offsets {
b.u64(off);
}
b.finish()
}
#[cfg(test)]
fn compute_chunk_offsets(
first_sample_file_offset: u64,
sample_sizes: &[u32],
samples_per_chunk: u32,
) -> Vec<u64> {
let spc = samples_per_chunk.max(1) as usize;
let total = sample_sizes.len();
if total == 0 {
return Vec::new();
}
let chunk_count = (total + spc - 1) / spc;
let mut offsets = Vec::with_capacity(chunk_count);
let mut cursor = first_sample_file_offset;
let mut sample_idx = 0usize;
for _ in 0..chunk_count {
offsets.push(cursor);
let end = (sample_idx + spc).min(total);
for &size in &sample_sizes[sample_idx..end] {
cursor = cursor.saturating_add(size as u64);
}
sample_idx = end;
}
offsets
}
pub(crate) fn write_unity_matrix(b: &mut BoxBuilder) {
b.u32(0x00010000);
b.u32(0);
b.u32(0);
b.u32(0);
b.u32(0x00010000);
b.u32(0);
b.u32(0);
b.u32(0);
b.u32(0x40000000);
}
pub(crate) struct BoxBuilder {
buf: Vec<u8>,
}
impl BoxBuilder {
pub(crate) fn new(box_type: &[u8; 4]) -> Self {
let mut buf = Vec::with_capacity(64);
buf.extend_from_slice(&[0, 0, 0, 0]); buf.extend_from_slice(box_type);
Self { buf }
}
pub(crate) fn u8(&mut self, v: u8) {
self.buf.push(v);
}
pub(crate) fn u16(&mut self, v: u16) {
self.buf.extend_from_slice(&v.to_be_bytes());
}
pub(crate) fn u32(&mut self, v: u32) {
self.buf.extend_from_slice(&v.to_be_bytes());
}
pub(crate) fn u64(&mut self, v: u64) {
self.buf.extend_from_slice(&v.to_be_bytes());
}
pub(crate) fn extend(&mut self, v: &[u8]) {
self.buf.extend_from_slice(v);
}
pub(crate) fn current_len(&self) -> usize {
self.buf.len()
}
pub(crate) fn finish(mut self) -> Vec<u8> {
let size = self.buf.len() as u32;
self.buf[0..4].copy_from_slice(&size.to_be_bytes());
self.buf
}
}
pub(crate) fn extract_sequence_header(data: &[u8]) -> Result<Vec<u8>> {
let mut pos = 0;
while pos < data.len() {
let header_byte = data[pos];
pos += 1;
let obu_type = (header_byte >> 3) & 0x0F;
let extension_flag = (header_byte >> 2) & 0x1;
let has_size = (header_byte >> 1) & 0x1;
if has_size == 0 {
anyhow::bail!(
"AV1 packet uses Annex-B style OBUs (obu_has_size_field=0); \
expected LOB format from the encoder"
);
}
if extension_flag != 0 {
if pos >= data.len() {
anyhow::bail!("truncated OBU extension header");
}
pos += 1;
}
let (size64, size_len) = read_leb128(&data[pos..])?;
let size = size64 as usize;
pos += size_len;
if pos + size > data.len() {
anyhow::bail!("OBU payload extends past packet");
}
if obu_type == 1 {
let header: u8 = (1 << 3) | (1 << 1);
let mut out = Vec::with_capacity(1 + 8 + size);
out.push(header);
write_leb128(&mut out, size as u64);
out.extend_from_slice(&data[pos..pos + size]);
return Ok(out);
}
pos += size;
}
anyhow::bail!("no OBU_SEQUENCE_HEADER found in first packet")
}
fn read_leb128(data: &[u8]) -> Result<(u64, usize)> {
let mut value: u64 = 0;
let mut len = 0usize;
for i in 0..8 {
if i >= data.len() {
anyhow::bail!("truncated leb128");
}
let byte = data[i];
value |= ((byte & 0x7F) as u64) << (i * 7);
len += 1;
if (byte & 0x80) == 0 {
return Ok((value, len));
}
}
anyhow::bail!("leb128 too long")
}
fn write_leb128(out: &mut Vec<u8>, mut value: u64) {
loop {
let mut byte = (value & 0x7F) as u8;
value >>= 7;
if value != 0 {
byte |= 0x80;
out.push(byte);
} else {
out.push(byte);
return;
}
}
}
fn parse_seq_header_params(obu: &[u8]) -> (u8, u8, u8, bool, bool, bool, u8, u8, u8) {
if obu.len() < 2 {
return (0, 0, 0, false, false, false, 1, 1, 0);
}
let mut pos = 1;
if obu[0] & 0x02 != 0 {
match read_leb128(&obu[pos..]) {
Ok((_, len)) => pos += len,
Err(_) => return (0, 0, 0, false, false, false, 1, 1, 0),
}
}
if pos >= obu.len() {
return (0, 0, 0, false, false, false, 1, 1, 0);
}
let mut br = BitReader::new(&obu[pos..]);
let seq_profile = br.bits(3).unwrap_or(0) as u8;
let _still_picture = br.bits(1).unwrap_or(0);
let reduced_still_picture_header = br.bits(1).unwrap_or(0);
let (seq_level_idx_0, seq_tier_0) = if reduced_still_picture_header != 0 {
(br.bits(5).unwrap_or(0) as u8, 0)
} else {
let timing_info_present = br.bits(1).unwrap_or(0);
if timing_info_present != 0 {
let _num_units = br.bits(32);
let _time_scale = br.bits(32);
let equal_pts = br.bits(1).unwrap_or(0);
if equal_pts != 0 {
let _nticks = read_uvlc(&mut br);
}
let decoder_model_info_present = br.bits(1).unwrap_or(0);
if decoder_model_info_present != 0 {
let _bdlm1 = br.bits(5);
let _nts = br.bits(32);
let _brslm1 = br.bits(5);
let _frpdlm1 = br.bits(5);
}
}
let initial_display_delay_present = br.bits(1).unwrap_or(0);
let operating_points_cnt_minus_1 = br.bits(5).unwrap_or(0);
let mut level0 = 0u8;
let mut tier0 = 0u8;
for i in 0..=operating_points_cnt_minus_1 {
let _operating_point_idc = br.bits(12).unwrap_or(0);
let seq_level_idx_i = br.bits(5).unwrap_or(0) as u8;
let seq_tier_i = if seq_level_idx_i > 7 {
br.bits(1).unwrap_or(0) as u8
} else {
0
};
if i == 0 {
level0 = seq_level_idx_i;
tier0 = seq_tier_i;
}
if initial_display_delay_present != 0 {
let present = br.bits(1).unwrap_or(0);
if present != 0 {
let _iddm1 = br.bits(4);
}
}
}
(level0, tier0)
};
let frame_width_bits_minus_1 = br.bits(4).unwrap_or(0);
let frame_height_bits_minus_1 = br.bits(4).unwrap_or(0);
let _max_frame_width_minus_1 = br.bits(frame_width_bits_minus_1 + 1);
let _max_frame_height_minus_1 = br.bits(frame_height_bits_minus_1 + 1);
if reduced_still_picture_header == 0 {
let frame_id_numbers_present = br.bits(1).unwrap_or(0);
if frame_id_numbers_present != 0 {
let _delta_fid_len = br.bits(4);
let _add_fid_len = br.bits(3);
}
}
let _use_128x128 = br.bits(1);
let _enable_filter_intra = br.bits(1);
let _enable_intra_edge_filter = br.bits(1);
if reduced_still_picture_header == 0 {
let _enable_interintra = br.bits(1);
let _enable_masked = br.bits(1);
let _enable_warped = br.bits(1);
let _enable_dual_filter = br.bits(1);
let _enable_order_hint = br.bits(1);
let enable_order_hint = _enable_order_hint.unwrap_or(0);
if enable_order_hint != 0 {
let _enable_jnt_comp = br.bits(1);
let _enable_ref_frame_mvs = br.bits(1);
}
let seq_choose_screen_detection_tools = br.bits(1).unwrap_or(0);
let seq_force_screen_content_tools = if seq_choose_screen_detection_tools != 0 {
2
} else {
br.bits(1).unwrap_or(0)
};
if seq_force_screen_content_tools > 0 {
let seq_choose_integer_mv = br.bits(1).unwrap_or(0);
if seq_choose_integer_mv == 0 {
let _seq_force_integer_mv = br.bits(1);
}
}
if enable_order_hint != 0 {
let _order_hint_bits_minus_1 = br.bits(3);
}
}
let _enable_superres = br.bits(1);
let _enable_cdef = br.bits(1);
let _enable_restoration = br.bits(1);
let high_bitdepth = br.bits(1).unwrap_or(0) != 0;
let twelve_bit = if seq_profile == 2 && high_bitdepth {
br.bits(1).unwrap_or(0) != 0
} else {
false
};
let monochrome = if seq_profile == 1 {
false
} else {
br.bits(1).unwrap_or(0) != 0
};
let color_description_present = br.bits(1).unwrap_or(0) != 0;
let (color_primaries, transfer_characteristics, matrix_coefficients) =
if color_description_present {
let cp = br.bits(8).unwrap_or(2) as u8;
let tc = br.bits(8).unwrap_or(2) as u8;
let mc = br.bits(8).unwrap_or(2) as u8;
(cp, tc, mc)
} else {
(2u8, 2u8, 2u8) };
let (subsampling_x, subsampling_y, chroma_sample_position) = if monochrome {
let _color_range = br.bits(1);
(1u8, 1u8, 0u8)
} else if color_primaries == 1
&& transfer_characteristics == 13
&& matrix_coefficients == 0
{
(0u8, 0u8, 0u8)
} else {
let _color_range = br.bits(1);
let (sx, sy) = if seq_profile == 0 {
(1u8, 1u8)
} else if seq_profile == 1 {
(0u8, 0u8)
} else {
let bit_depth = if high_bitdepth {
if twelve_bit { 12 } else { 10 }
} else {
8
};
if bit_depth == 12 {
let sxb = br.bits(1).unwrap_or(1) as u8;
let syb = if sxb != 0 {
br.bits(1).unwrap_or(1) as u8
} else {
0
};
(sxb, syb)
} else {
(1u8, 0u8)
}
};
let csp = if sx != 0 && sy != 0 {
br.bits(2).unwrap_or(0) as u8
} else {
0u8
};
(sx, sy, csp)
};
(
seq_profile,
seq_level_idx_0,
seq_tier_0,
high_bitdepth,
twelve_bit,
monochrome,
subsampling_x,
subsampling_y,
chroma_sample_position,
)
}
struct BitReader<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> BitReader<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 }
}
fn bits(&mut self, n: u32) -> Option<u32> {
let mut v: u32 = 0;
for _ in 0..n {
if self.pos / 8 >= self.data.len() {
return None;
}
let byte = self.data[self.pos / 8];
let bit = (byte >> (7 - (self.pos % 8))) & 1;
v = (v << 1) | bit as u32;
self.pos += 1;
}
Some(v)
}
}
fn read_uvlc(br: &mut BitReader) -> u32 {
let mut leading_zeros = 0u32;
while leading_zeros < 32 {
match br.bits(1) {
Some(0) => leading_zeros += 1,
Some(_) => break,
None => return 0,
}
}
if leading_zeros >= 32 {
return u32::MAX;
}
let value = br.bits(leading_zeros).unwrap_or(0);
value + ((1u32 << leading_zeros) - 1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ftyp_starts_with_size_and_type() {
let ftyp = build_ftyp();
let size = u32::from_be_bytes([ftyp[0], ftyp[1], ftyp[2], ftyp[3]]);
assert_eq!(size as usize, ftyp.len());
assert_eq!(&ftyp[4..8], b"ftyp");
}
#[test]
fn leb128_roundtrip() {
let mut buf = Vec::new();
write_leb128(&mut buf, 300);
let (v, n) = read_leb128(&buf).unwrap();
assert_eq!(v, 300);
assert_eq!(n, buf.len());
}
#[test]
fn box_builder_sizes_correctly() {
let mut b = BoxBuilder::new(b"test");
b.u32(0xDEADBEEF);
let out = b.finish();
assert_eq!(out.len(), 12);
assert_eq!(&out[4..8], b"test");
assert_eq!(u32::from_be_bytes([out[0], out[1], out[2], out[3]]), 12);
}
fn parse_stsc_entries(stsc: &[u8]) -> Vec<(u32, u32, u32)> {
assert_eq!(&stsc[4..8], b"stsc");
let count = u32::from_be_bytes([stsc[12], stsc[13], stsc[14], stsc[15]]) as usize;
let mut out = Vec::with_capacity(count);
let mut p = 16usize;
for _ in 0..count {
let fc = u32::from_be_bytes([stsc[p], stsc[p + 1], stsc[p + 2], stsc[p + 3]]);
let spc = u32::from_be_bytes([stsc[p + 4], stsc[p + 5], stsc[p + 6], stsc[p + 7]]);
let sdi = u32::from_be_bytes([stsc[p + 8], stsc[p + 9], stsc[p + 10], stsc[p + 11]]);
out.push((fc, spc, sdi));
p += 12;
}
out
}
#[test]
fn mux_stsc_emits_multiple_chunk_runs() {
let stsc = build_stsc(120, 24);
let entries = parse_stsc_entries(&stsc);
assert_eq!(entries, vec![(1, 24, 1)]);
}
#[test]
fn mux_stsc_last_chunk_under_spc_emits_tail_entry() {
let stsc = build_stsc(121, 24);
let entries = parse_stsc_entries(&stsc);
assert_eq!(entries, vec![(1, 24, 1), (6, 1, 1)]);
}
#[test]
fn mux_stsc_all_under_spc_single_entry() {
let stsc = build_stsc(10, 24);
let entries = parse_stsc_entries(&stsc);
assert_eq!(entries, vec![(1, 10, 1)]);
}
#[test]
fn compute_chunk_offsets_walks_sample_sizes() {
let sizes = vec![100u32, 200, 300, 400, 500, 600, 700];
let offs = compute_chunk_offsets(1000, &sizes, 3);
assert_eq!(offs, vec![1000, 1600, 3100]);
}
#[test]
fn compute_chunk_offsets_single_chunk() {
let sizes = vec![10u32; 5];
let offs = compute_chunk_offsets(42, &sizes, 120);
assert_eq!(offs, vec![42]);
}
#[test]
fn build_stco_emits_32bit_offsets() {
let offs = vec![8u64, 1_000_000, u32::MAX as u64];
let box_bytes = build_stco(&offs);
assert_eq!(&box_bytes[4..8], b"stco");
let count =
u32::from_be_bytes([box_bytes[12], box_bytes[13], box_bytes[14], box_bytes[15]]);
assert_eq!(count, 3);
assert_eq!(box_bytes.len(), 16 + 12);
let last = u32::from_be_bytes([box_bytes[24], box_bytes[25], box_bytes[26], box_bytes[27]]);
assert_eq!(last, u32::MAX);
}
#[test]
fn build_co64_emits_64bit_offsets() {
let big = (u32::MAX as u64) + 100;
let offs = vec![8u64, big, big + 1_000_000];
let box_bytes = build_co64(&offs);
assert_eq!(&box_bytes[4..8], b"co64");
let count =
u32::from_be_bytes([box_bytes[12], box_bytes[13], box_bytes[14], box_bytes[15]]);
assert_eq!(count, 3);
assert_eq!(box_bytes.len(), 16 + 24);
let got = u64::from_be_bytes([
box_bytes[24],
box_bytes[25],
box_bytes[26],
box_bytes[27],
box_bytes[28],
box_bytes[29],
box_bytes[30],
box_bytes[31],
]);
assert_eq!(got, big);
}
#[test]
fn build_co64_offsets_are_monotonic_and_be() {
let offs: Vec<u64> = (0..5)
.map(|i| 10_000_000_000u64 + i as u64 * 4096)
.collect();
let box_bytes = build_co64(&offs);
let mut prev = 0u64;
for i in 0..5 {
let p = 16 + i * 8;
let v = u64::from_be_bytes([
box_bytes[p],
box_bytes[p + 1],
box_bytes[p + 2],
box_bytes[p + 3],
box_bytes[p + 4],
box_bytes[p + 5],
box_bytes[p + 6],
box_bytes[p + 7],
]);
assert!(v > prev, "offsets not monotonic: {v} after {prev}");
prev = v;
}
}
fn find_fourcc(data: &[u8], tag: &[u8; 4]) -> Option<usize> {
data.windows(4).position(|w| w == tag)
}
#[test]
fn moov_with_use_co64_true_emits_co64_not_stco() {
let sample_sizes = vec![1000u32; 120];
let chunk_offsets: Vec<u64> = (0..5)
.map(|i| (u32::MAX as u64) + i * 1_000_000_000)
.collect();
let config_obus = vec![0x0Au8, 0x03, 0x00, 0x00, 0x00];
let moov = build_moov(
1920,
1080,
90_000,
120 * 3750,
3750,
&sample_sizes,
&[],
&config_obus,
&chunk_offsets,
24,
true,
);
assert!(find_fourcc(&moov, b"co64").is_some(), "co64 box missing");
assert!(
find_fourcc(&moov, b"stco").is_none(),
"stco present when co64 chosen"
);
}
#[test]
fn moov_with_use_co64_false_emits_stco_not_co64() {
let sample_sizes = vec![1000u32; 120];
let chunk_offsets: Vec<u64> = (0..5).map(|i| 1000 + i * 24_000).collect();
let config_obus = vec![0x0Au8, 0x03, 0x00, 0x00, 0x00];
let moov = build_moov(
1920,
1080,
90_000,
120 * 3750,
3750,
&sample_sizes,
&[],
&config_obus,
&chunk_offsets,
24,
false,
);
assert!(find_fourcc(&moov, b"stco").is_some(), "stco box missing");
assert!(
find_fourcc(&moov, b"co64").is_none(),
"co64 present when stco chosen"
);
}
#[test]
fn ftyp_lists_av01_and_iso6_and_mp42_brands() {
let ftyp = build_ftyp();
assert_eq!(&ftyp[8..12], b"iso6", "major_brand should be iso6");
let compat = &ftyp[16..];
let brands: Vec<&[u8]> = compat.chunks_exact(4).collect();
assert!(
brands.contains(&b"av01".as_ref()),
"compatible_brands must list av01 per AV1-ISOBMFF §2.1; got {:?}",
brands
);
assert!(
brands.contains(&b"iso6".as_ref()),
"compatible_brands must list iso6 (14496-12 v6 — covers co64/largesize)"
);
assert!(
brands.contains(&b"mp42".as_ref()),
"compatible_brands should list mp42 for AAC parsing rules"
);
}
fn count_fourcc_occurrences(data: &[u8], tag: &[u8; 4]) -> usize {
data.windows(4).filter(|w| *w == tag).count()
}
#[test]
fn av01_sample_entry_includes_colr_nclx_box() {
let cm = ColorMetadata::default();
let sample_sizes = vec![100u32; 30];
let chunk_offsets: Vec<u64> = vec![1000];
let config_obus = vec![0x0Au8, 0x03, 0x00, 0x00, 0x00];
let moov = build_moov_any(
1920,
1080,
90_000,
90_000,
30 * 3000,
30 * 3000,
3000,
&sample_sizes,
&[],
&config_obus,
&chunk_offsets,
30,
None,
&[],
false,
&cm,
);
let colr_pos = find_fourcc(&moov, b"colr").expect("colr atom missing");
assert_eq!(
&moov[colr_pos + 4..colr_pos + 8],
b"nclx",
"colour_type must be 'nclx' per ISO/IEC 23001-8"
);
let cp = u16::from_be_bytes([moov[colr_pos + 8], moov[colr_pos + 9]]);
assert_eq!(cp, 1, "default BT.709 colour_primaries=1");
let tc = u16::from_be_bytes([moov[colr_pos + 10], moov[colr_pos + 11]]);
assert_eq!(tc, 1, "default BT.709 transfer_characteristics=1");
let mc = u16::from_be_bytes([moov[colr_pos + 12], moov[colr_pos + 13]]);
assert_eq!(mc, 1, "default BT.709 matrix_coefficients=1");
let fr = moov[colr_pos + 14];
assert_eq!(fr & 0x80, 0x00, "default limited-range full_range_flag=0");
}
#[test]
fn colr_nclx_carries_hdr10_metadata() {
let cm = ColorMetadata {
transfer: codec::frame::TransferFn::St2084,
matrix_coefficients: 9,
colour_primaries: 9,
full_range: false,
..ColorMetadata::default()
};
let colr = build_colr_nclx(&cm);
assert_eq!(&colr[4..8], b"colr");
assert_eq!(&colr[8..12], b"nclx");
let cp = u16::from_be_bytes([colr[12], colr[13]]);
let tc = u16::from_be_bytes([colr[14], colr[15]]);
let mc = u16::from_be_bytes([colr[16], colr[17]]);
let fr = colr[18];
assert_eq!(cp, 9, "BT.2020 NCL primaries");
assert_eq!(tc, 16, "ST 2084 PQ transfer");
assert_eq!(mc, 9, "BT.2020 NCL matrix");
assert_eq!(fr & 0x80, 0x00, "HDR10 typically signals limited range");
}
#[test]
fn colr_nclx_full_range_sets_high_bit() {
let cm = ColorMetadata {
transfer: codec::frame::TransferFn::Bt709,
matrix_coefficients: 1,
colour_primaries: 1,
full_range: true,
..ColorMetadata::default()
};
let colr = build_colr_nclx(&cm);
assert_eq!(colr[18] & 0x80, 0x80, "full_range high bit must be set");
assert_eq!(colr[18] & 0x7F, 0x00, "reserved bits must be zero");
}
#[test]
fn colr_nclx_box_size_matches_layout() {
let colr = build_colr_nclx(&ColorMetadata::default());
let size = u32::from_be_bytes([colr[0], colr[1], colr[2], colr[3]]) as usize;
assert_eq!(
size,
colr.len(),
"colr box size field must equal box length"
);
assert_eq!(size, 19, "colr nclx must be exactly 19 bytes");
}
#[test]
fn colr_lives_inside_av01_sample_entry() {
let cm = ColorMetadata::default();
let sample_sizes = vec![100u32; 30];
let chunk_offsets: Vec<u64> = vec![1000];
let config_obus = vec![0x0Au8, 0x03, 0x00, 0x00, 0x00];
let moov = build_moov_any(
1920,
1080,
90_000,
90_000,
30 * 3000,
30 * 3000,
3000,
&sample_sizes,
&[],
&config_obus,
&chunk_offsets,
30,
None,
&[],
false,
&cm,
);
let av01_pos = find_fourcc(&moov, b"av01").expect("av01 sample entry missing");
let av01_size = u32::from_be_bytes([
moov[av01_pos - 4],
moov[av01_pos - 3],
moov[av01_pos - 2],
moov[av01_pos - 1],
]) as usize;
let av01_end = av01_pos - 4 + av01_size;
let colr_pos = find_fourcc(&moov, b"colr").expect("colr missing");
assert!(
colr_pos > av01_pos && colr_pos < av01_end,
"colr must be nested inside av01 sample entry: av01@{}..{} colr@{}",
av01_pos,
av01_end,
colr_pos
);
assert_eq!(
count_fourcc_occurrences(&moov, b"colr"),
1,
"exactly one colr atom expected"
);
}
#[test]
fn transfer_to_h273_emits_canonical_codes() {
use codec::frame::TransferFn;
assert_eq!(transfer_to_h273(TransferFn::Bt709), 1);
assert_eq!(transfer_to_h273(TransferFn::Bt470Bg), 4);
assert_eq!(transfer_to_h273(TransferFn::Linear), 8);
assert_eq!(transfer_to_h273(TransferFn::St2084), 16);
assert_eq!(transfer_to_h273(TransferFn::AribStdB67), 18);
assert_eq!(transfer_to_h273(TransferFn::Unspecified), 2);
}
fn hdr10_mastering_display() -> codec::frame::MasteringDisplay {
codec::frame::MasteringDisplay {
primaries_r_x: 35400,
primaries_r_y: 14600,
primaries_g_x: 8500,
primaries_g_y: 39850,
primaries_b_x: 6550,
primaries_b_y: 2300,
white_point_x: 15635,
white_point_y: 16450,
max_luminance: 10_000_000,
min_luminance: 1,
}
}
#[test]
fn mdcv_box_24_byte_payload_layout() {
let md = hdr10_mastering_display();
let mdcv = build_mdcv(&md);
assert_eq!(
mdcv.len(),
32,
"mdcv box must be exactly 32 bytes (8 header + 24 payload)"
);
let size = u32::from_be_bytes([mdcv[0], mdcv[1], mdcv[2], mdcv[3]]) as usize;
assert_eq!(size, mdcv.len(), "size field must equal box length");
assert_eq!(&mdcv[4..8], b"mdcv", "box type must be 'mdcv' (not 'SmDm')");
let u16_at = |off: usize| u16::from_be_bytes([mdcv[off], mdcv[off + 1]]);
let u32_at = |off: usize| {
u32::from_be_bytes([mdcv[off], mdcv[off + 1], mdcv[off + 2], mdcv[off + 3]])
};
assert_eq!(u16_at(8), 35400, "primaries_r_x");
assert_eq!(u16_at(10), 14600, "primaries_r_y");
assert_eq!(u16_at(12), 8500, "primaries_g_x");
assert_eq!(u16_at(14), 39850, "primaries_g_y");
assert_eq!(u16_at(16), 6550, "primaries_b_x");
assert_eq!(u16_at(18), 2300, "primaries_b_y");
assert_eq!(u16_at(20), 15635, "white_point_x");
assert_eq!(u16_at(22), 16450, "white_point_y");
assert_eq!(u32_at(24), 10_000_000, "max_luminance (0.0001 cd/m² steps)");
assert_eq!(u32_at(28), 1, "min_luminance");
}
#[test]
fn clli_box_4_byte_payload_layout() {
let cll = codec::frame::ContentLightLevel {
max_cll: 1000,
max_fall: 400,
};
let clli = build_clli(&cll);
assert_eq!(
clli.len(),
12,
"clli box must be exactly 12 bytes (8 header + 4 payload)"
);
let size = u32::from_be_bytes([clli[0], clli[1], clli[2], clli[3]]) as usize;
assert_eq!(size, clli.len(), "size field must equal box length");
assert_eq!(&clli[4..8], b"clli", "box type must be 'clli' (not 'CoLL')");
let max_cll = u16::from_be_bytes([clli[8], clli[9]]);
let max_fall = u16::from_be_bytes([clli[10], clli[11]]);
assert_eq!(max_cll, 1000, "max_cll");
assert_eq!(max_fall, 400, "max_fall");
}
#[test]
fn mdcv_omitted_when_none() {
let cm = ColorMetadata::default(); let sample_sizes = vec![100u32; 30];
let chunk_offsets: Vec<u64> = vec![1000];
let config_obus = vec![0x0Au8, 0x03, 0x00, 0x00, 0x00];
let moov = build_moov_any(
1920,
1080,
90_000,
90_000,
30 * 3000,
30 * 3000,
3000,
&sample_sizes,
&[],
&config_obus,
&chunk_offsets,
30,
None,
&[],
false,
&cm,
);
assert!(
find_fourcc(&moov, b"mdcv").is_none(),
"SDR (mastering_display=None) moov must NOT contain mdcv box"
);
}
#[test]
fn clli_omitted_when_none() {
let cm = ColorMetadata::default();
let sample_sizes = vec![100u32; 30];
let chunk_offsets: Vec<u64> = vec![1000];
let config_obus = vec![0x0Au8, 0x03, 0x00, 0x00, 0x00];
let moov = build_moov_any(
1920,
1080,
90_000,
90_000,
30 * 3000,
30 * 3000,
3000,
&sample_sizes,
&[],
&config_obus,
&chunk_offsets,
30,
None,
&[],
false,
&cm,
);
assert!(
find_fourcc(&moov, b"clli").is_none(),
"SDR (content_light_level=None) moov must NOT contain clli box"
);
}
#[test]
fn av01_sample_entry_emits_mdcv_and_clli_in_order() {
let cm = ColorMetadata {
transfer: codec::frame::TransferFn::St2084,
matrix_coefficients: 9,
colour_primaries: 9,
full_range: false,
mastering_display: Some(hdr10_mastering_display()),
content_light_level: Some(codec::frame::ContentLightLevel {
max_cll: 1000,
max_fall: 400,
}),
};
let sample_sizes = vec![100u32; 30];
let chunk_offsets: Vec<u64> = vec![1000];
let config_obus = vec![0x0Au8, 0x03, 0x00, 0x00, 0x00];
let moov = build_moov_any(
1920,
1080,
90_000,
90_000,
30 * 3000,
30 * 3000,
3000,
&sample_sizes,
&[],
&config_obus,
&chunk_offsets,
30,
None,
&[],
false,
&cm,
);
let av01_pos = find_fourcc(&moov, b"av01").expect("av01 sample entry missing");
let av01_size = u32::from_be_bytes([
moov[av01_pos - 4],
moov[av01_pos - 3],
moov[av01_pos - 2],
moov[av01_pos - 1],
]) as usize;
let av01_end = av01_pos - 4 + av01_size;
let av01_body = &moov[av01_pos..av01_end];
let colr_rel = av01_body
.windows(4)
.position(|w| w == b"colr")
.expect("colr nested in av01");
let mdcv_rel = av01_body
.windows(4)
.position(|w| w == b"mdcv")
.expect("mdcv nested in av01");
let clli_rel = av01_body
.windows(4)
.position(|w| w == b"clli")
.expect("clli nested in av01");
assert!(
colr_rel < mdcv_rel,
"colr ({}) must precede mdcv ({})",
colr_rel,
mdcv_rel
);
assert!(
mdcv_rel < clli_rel,
"mdcv ({}) must precede clli ({})",
mdcv_rel,
clli_rel
);
assert_eq!(
count_fourcc_occurrences(&moov, b"mdcv"),
1,
"exactly one mdcv expected"
);
assert_eq!(
count_fourcc_occurrences(&moov, b"clli"),
1,
"exactly one clli expected"
);
}
#[test]
fn colr_handles_pq_transfer_code_16() {
let cm = ColorMetadata {
transfer: codec::frame::TransferFn::St2084,
matrix_coefficients: 9,
colour_primaries: 9,
full_range: false,
..ColorMetadata::default()
};
let colr = build_colr_nclx(&cm);
let tc = u16::from_be_bytes([colr[14], colr[15]]);
assert_eq!(tc, 16, "PQ transfer must encode as H.273 code 16");
}
#[test]
fn colr_handles_hlg_transfer_code_18() {
let cm = ColorMetadata {
transfer: codec::frame::TransferFn::AribStdB67,
matrix_coefficients: 9,
colour_primaries: 9,
full_range: false,
..ColorMetadata::default()
};
let colr = build_colr_nclx(&cm);
let tc = u16::from_be_bytes([colr[14], colr[15]]);
assert_eq!(tc, 18, "HLG transfer must encode as H.273 code 18");
}
#[test]
fn colr_bt2020_primaries_matrix() {
let cm_ncl = ColorMetadata {
transfer: codec::frame::TransferFn::St2084,
matrix_coefficients: 9,
colour_primaries: 9,
full_range: false,
..ColorMetadata::default()
};
let colr_ncl = build_colr_nclx(&cm_ncl);
let cp_ncl = u16::from_be_bytes([colr_ncl[12], colr_ncl[13]]);
let mc_ncl = u16::from_be_bytes([colr_ncl[16], colr_ncl[17]]);
assert_eq!(cp_ncl, 9, "BT.2020 colour_primaries must be 9");
assert_eq!(mc_ncl, 9, "BT.2020 NCL matrix must be 9");
let cm_cl = ColorMetadata {
matrix_coefficients: 10,
..cm_ncl
};
let colr_cl = build_colr_nclx(&cm_cl);
let mc_cl = u16::from_be_bytes([colr_cl[16], colr_cl[17]]);
assert_eq!(
mc_cl, 10,
"BT.2020 CL matrix must be 10 (preserved verbatim)"
);
}
fn opus_head_stereo_48k_preskip_312() -> Vec<u8> {
let mut head = Vec::with_capacity(11);
head.push(1u8); head.push(2u8); head.extend_from_slice(&312u16.to_le_bytes()); head.extend_from_slice(&48_000u32.to_le_bytes()); head.extend_from_slice(&0i16.to_le_bytes()); head.push(0u8); head
}
fn opus_info_stereo_48k() -> AudioInfo {
AudioInfo {
codec: "opus".into(),
sample_rate: 48_000,
channels: 2,
timescale: 48_000,
asc_bytes: Vec::new(),
codec_private: opus_head_stereo_48k_preskip_312(),
}
}
#[test]
fn dops_box_11_byte_payload_layout() {
let info = opus_info_stereo_48k();
let dops = build_dops(&info);
assert_eq!(
dops.len(),
19,
"dOps must be exactly 19 bytes (8 header + 11 payload)"
);
let size = u32::from_be_bytes([dops[0], dops[1], dops[2], dops[3]]) as usize;
assert_eq!(size, dops.len(), "size field must equal box length");
assert_eq!(
&dops[4..8],
b"dOps",
"box type must be 'dOps' (capital O lowercase ps)"
);
assert_eq!(dops[8], 0, "Version (RFC 7845 §4.5: MUST be 0)");
assert_eq!(dops[9], 2, "OutputChannelCount = stereo");
let pre_skip = u16::from_be_bytes([dops[10], dops[11]]);
assert_eq!(pre_skip, 312, "PreSkip = 312 (BE)");
let input_sample_rate = u32::from_be_bytes([dops[12], dops[13], dops[14], dops[15]]);
assert_eq!(input_sample_rate, 48_000, "InputSampleRate = 48000 (BE)");
let output_gain = i16::from_be_bytes([dops[16], dops[17]]);
assert_eq!(output_gain, 0, "OutputGain = 0 (Q8 dB, BE)");
assert_eq!(dops[18], 0, "ChannelMappingFamily = 0 (mono/stereo)");
}
#[test]
fn dops_byte_order_flipped_from_opushead() {
let info = opus_info_stereo_48k();
assert_eq!(
info.codec_private[2..4],
[0x38, 0x01],
"OpusHead PreSkip must be LE"
);
let dops = build_dops(&info);
assert_eq!(
dops[10..12],
[0x01, 0x38],
"dOps PreSkip must be BE — got {:02X?}",
&dops[10..12]
);
}
#[test]
fn opus_sample_entry_size_and_fourcc() {
let info = opus_info_stereo_48k();
let entry = build_opus_sample_entry(&info);
let size = u32::from_be_bytes([entry[0], entry[1], entry[2], entry[3]]) as usize;
assert_eq!(size, entry.len(), "size field must equal box length");
assert_eq!(&entry[4..8], b"Opus", "4-cc MUST be 'Opus' (capital O)");
assert_ne!(&entry[4..8], b"opus", "lowercase 'opus' is non-conformant");
assert_eq!(
entry.len(),
55,
"Opus sample entry should be 55 bytes for stereo + dOps minimum"
);
}
#[test]
fn opus_sample_entry_samplerate_is_48000_q16() {
let info = AudioInfo {
sample_rate: 44_100,
..opus_info_stereo_48k()
};
let entry = build_opus_sample_entry(&info);
let sr_q16 = u32::from_be_bytes([entry[32], entry[33], entry[34], entry[35]]);
assert_eq!(
sr_q16,
48_000u32 << 16,
"samplerate field MUST be 48000<<16 (Q16); got 0x{:08X}",
sr_q16
);
}
#[test]
fn dops_nests_inside_opus_sample_entry() {
let info = opus_info_stereo_48k();
let entry = build_opus_sample_entry(&info);
let dops_pos = entry
.windows(4)
.position(|w| w == b"dOps")
.expect("dOps child missing inside Opus sample entry");
assert!(
dops_pos > 28,
"dOps must come after the AudioSampleEntry preamble; got pos={}",
dops_pos
);
}
#[test]
fn stsd_dispatcher_routes_codec_to_correct_sample_entry() {
let aac = AudioInfo {
codec: "aac".into(),
sample_rate: 44_100,
channels: 2,
timescale: 44_100,
asc_bytes: vec![0x12, 0x10],
codec_private: Vec::new(),
};
let stsd_aac = build_audio_stsd(&aac);
assert!(
stsd_aac.windows(4).any(|w| w == b"mp4a"),
"AAC stsd must contain mp4a"
);
assert!(
!stsd_aac.windows(4).any(|w| w == b"Opus"),
"AAC stsd must NOT contain Opus"
);
assert!(
stsd_aac.windows(4).any(|w| w == b"esds"),
"AAC stsd must contain esds"
);
let opus = opus_info_stereo_48k();
let stsd_opus = build_audio_stsd(&opus);
assert!(
stsd_opus.windows(4).any(|w| w == b"Opus"),
"Opus stsd must contain Opus"
);
assert!(
!stsd_opus.windows(4).any(|w| w == b"mp4a"),
"Opus stsd must NOT contain mp4a"
);
assert!(
stsd_opus.windows(4).any(|w| w == b"dOps"),
"Opus stsd must contain dOps"
);
assert!(
!stsd_opus.windows(4).any(|w| w == b"esds"),
"Opus stsd must NOT contain esds"
);
}
#[test]
fn dops_handles_negative_output_gain() {
let mut head = opus_head_stereo_48k_preskip_312();
let gain: i16 = -768;
head[8..10].copy_from_slice(&gain.to_le_bytes());
let info = AudioInfo {
codec_private: head,
..opus_info_stereo_48k()
};
let dops = build_dops(&info);
let recovered = i16::from_be_bytes([dops[16], dops[17]]);
assert_eq!(
recovered, -768,
"negative OutputGain must survive LE→BE roundtrip"
);
}
#[test]
fn dops_preserves_arbitrary_preskip() {
for &expected in &[0u16, 156, 312, 480, 1024, 65535] {
let mut head = opus_head_stereo_48k_preskip_312();
head[2..4].copy_from_slice(&expected.to_le_bytes());
let info = AudioInfo {
codec_private: head,
..opus_info_stereo_48k()
};
let dops = build_dops(&info);
let got = u16::from_be_bytes([dops[10], dops[11]]);
assert_eq!(got, expected, "PreSkip {} must survive LE→BE", expected);
}
}
fn opus_head_surround(
channels: u8,
pre_skip: u16,
input_sample_rate: u32,
streams: u8,
coupled: u8,
mapping: &[u8],
) -> Vec<u8> {
assert_eq!(mapping.len(), channels as usize);
let mut h = Vec::with_capacity(11 + 2 + channels as usize);
h.push(1u8); h.push(channels);
h.extend_from_slice(&pre_skip.to_le_bytes());
h.extend_from_slice(&input_sample_rate.to_le_bytes());
h.extend_from_slice(&0i16.to_le_bytes()); h.push(1u8); h.push(streams);
h.push(coupled);
h.extend_from_slice(mapping);
h
}
fn opus_info_5_1() -> AudioInfo {
let cp = opus_head_surround(6, 312, 48_000, 4, 2, &[0, 4, 1, 2, 3, 5]);
AudioInfo {
codec: "opus".into(),
sample_rate: 48_000,
channels: 6,
timescale: 48_000,
asc_bytes: Vec::new(),
codec_private: cp,
}
}
#[test]
fn dops_box_5_1_payload_is_19_bytes_total_27() {
let info = opus_info_5_1();
let dops = build_dops(&info);
assert_eq!(
dops.len(),
27,
"5.1 dOps box = 8 header + 19 payload = 27 bytes; got {}",
dops.len()
);
let size = u32::from_be_bytes([dops[0], dops[1], dops[2], dops[3]]) as usize;
assert_eq!(size, dops.len());
assert_eq!(&dops[4..8], b"dOps");
assert_eq!(dops[8], 0, "Version");
assert_eq!(dops[9], 6, "OutputChannelCount = 6 for 5.1");
let pre_skip = u16::from_be_bytes([dops[10], dops[11]]);
assert_eq!(pre_skip, 312);
let isr = u32::from_be_bytes([dops[12], dops[13], dops[14], dops[15]]);
assert_eq!(isr, 48_000);
assert_eq!(i16::from_be_bytes([dops[16], dops[17]]), 0);
assert_eq!(dops[18], 1, "ChannelMappingFamily = 1 for surround");
assert_eq!(dops[19], 4, "StreamCount = 4 for 5.1");
assert_eq!(dops[20], 2, "CoupledCount = 2 for 5.1");
assert_eq!(
&dops[21..27],
&[0u8, 4, 1, 2, 3, 5][..],
"ChannelMapping for 5.1"
);
}
#[test]
fn dops_box_7_1_payload_is_21_bytes_total_29() {
let cp = opus_head_surround(8, 312, 48_000, 5, 3, &[0, 6, 1, 2, 3, 4, 5, 7]);
let info = AudioInfo {
codec: "opus".into(),
sample_rate: 48_000,
channels: 8,
timescale: 48_000,
asc_bytes: Vec::new(),
codec_private: cp,
};
let dops = build_dops(&info);
assert_eq!(dops.len(), 29);
assert_eq!(dops[18], 1, "Family = 1");
assert_eq!(dops[19], 5, "StreamCount = 5 for 7.1");
assert_eq!(dops[20], 3, "CoupledCount = 3 for 7.1");
assert_eq!(&dops[21..29], &[0u8, 6, 1, 2, 3, 4, 5, 7][..]);
}
#[test]
fn dops_box_5_1_hex_dump() {
let info = opus_info_5_1();
let dops = build_dops(&info);
let hex: String = dops.iter().map(|b| format!("{b:02x} ")).collect();
println!("5.1 dOps box hex (27 bytes total): {}", hex.trim_end());
}
#[test]
fn opus_sample_entry_5_1_size_and_dops_nesting() {
let info = opus_info_5_1();
let entry = build_opus_sample_entry(&info);
assert_eq!(
entry.len(),
36 + 27,
"Opus sample entry for 5.1 = 36 + 27 = 63 bytes; got {}",
entry.len()
);
let entry_channels = u16::from_be_bytes([entry[24], entry[25]]);
assert_eq!(
entry_channels, 6,
"channel_count in AudioSampleEntry must reflect 5.1"
);
assert!(entry[36..].windows(4).any(|w| w == b"dOps"));
assert_eq!(
entry[36 + 8 + 10],
1,
"dOps inside Opus sample entry must carry family=1 for 5.1"
);
}
#[test]
fn with_audio_rejects_family_1_with_truncated_codec_private() {
let mut muxer = Av1Mp4Muxer::new(640, 480, 30.0).unwrap();
let mut info = opus_info_5_1();
info.codec_private.truncate(13); let err = match muxer.with_audio(info) {
Ok(_) => panic!("truncated family=1 codec_private must reject"),
Err(e) => e,
};
let msg = format!("{}", err);
assert!(
msg.contains("≥") && msg.contains("preamble"),
"error message must explain the size requirement; got: {msg}"
);
}
#[test]
fn with_audio_rejects_family_1_with_zero_streams() {
let mut muxer = Av1Mp4Muxer::new(640, 480, 30.0).unwrap();
let mut info = opus_info_5_1();
info.codec_private[11] = 0;
let r = muxer.with_audio(info);
assert!(r.is_err(), "StreamCount = 0 must reject");
}
#[test]
fn with_audio_rejects_family_1_with_coupled_exceeding_streams() {
let mut muxer = Av1Mp4Muxer::new(640, 480, 30.0).unwrap();
let mut info = opus_info_5_1();
info.codec_private[11] = 2;
info.codec_private[12] = 5;
let r = muxer.with_audio(info);
assert!(r.is_err(), "CoupledCount > StreamCount must reject");
}
#[test]
fn with_audio_rejects_family_1_with_mapping_index_out_of_range() {
let mut muxer = Av1Mp4Muxer::new(640, 480, 30.0).unwrap();
let mut info = opus_info_5_1();
info.codec_private[13] = 99;
let r = muxer.with_audio(info);
assert!(r.is_err(), "ChannelMapping out-of-range must reject");
}
#[test]
fn with_audio_rejects_family_0_with_5_1_channels() {
let mut muxer = Av1Mp4Muxer::new(640, 480, 30.0).unwrap();
let mut head = Vec::with_capacity(11);
head.push(1u8);
head.push(6u8);
head.extend_from_slice(&312u16.to_le_bytes());
head.extend_from_slice(&48_000u32.to_le_bytes());
head.extend_from_slice(&0i16.to_le_bytes());
head.push(0u8); let info = AudioInfo {
codec: "opus".into(),
sample_rate: 48_000,
channels: 6,
timescale: 48_000,
asc_bytes: Vec::new(),
codec_private: head,
};
let r = muxer.with_audio(info);
assert!(r.is_err(), "family=0 + 6 channels must reject");
}
#[test]
fn with_audio_accepts_5_1_opus() {
let mut muxer = Av1Mp4Muxer::new(640, 480, 30.0).unwrap();
let info = opus_info_5_1();
muxer
.with_audio(info)
.expect("5.1 Opus with valid family=1 trailer must accept");
}
#[test]
fn with_audio_rejects_9_channel_opus() {
let mut muxer = Av1Mp4Muxer::new(640, 480, 30.0).unwrap();
let mut head = Vec::with_capacity(11 + 2 + 9);
head.push(1u8);
head.push(9u8);
head.extend_from_slice(&312u16.to_le_bytes());
head.extend_from_slice(&48_000u32.to_le_bytes());
head.extend_from_slice(&0i16.to_le_bytes());
head.push(1u8); head.push(5);
head.push(3);
head.extend_from_slice(&[0u8, 1, 2, 3, 4, 5, 6, 7, 0]);
let info = AudioInfo {
codec: "opus".into(),
sample_rate: 48_000,
channels: 9,
timescale: 48_000,
asc_bytes: Vec::new(),
codec_private: head,
};
let r = muxer.with_audio(info);
assert!(
r.is_err(),
"9-channel Opus must reject (no family-1 layout above 8)"
);
}
#[test]
fn chan_box_omitted_for_mono_and_stereo() {
assert!(build_chan_box(1).is_none(), "mono should not emit chan");
assert!(build_chan_box(2).is_none(), "stereo should not emit chan");
}
#[test]
fn chan_box_omitted_for_unsupported_counts() {
for &c in &[0u16, 3, 4, 5, 8, 9, 16] {
assert!(
build_chan_box(c).is_none(),
"channels={c} must not emit chan"
);
}
}
#[test]
fn chan_box_5_1_layout_and_size() {
let chan = build_chan_box(6).expect("5.1 must emit chan");
assert_eq!(
chan.len(),
20,
"5.1 chan box must be 20 bytes (8 header + 12 body)"
);
let size = u32::from_be_bytes([chan[0], chan[1], chan[2], chan[3]]);
assert_eq!(
size as usize,
chan.len(),
"size field must equal box length"
);
assert_eq!(&chan[4..8], b"chan", "fourcc must be 'chan'");
let tag = u32::from_be_bytes([chan[8], chan[9], chan[10], chan[11]]);
assert_eq!(
tag, 0x00720006u32,
"5.1 tag must be kAudioChannelLayoutTag_MPEG_5_1_C = 0x00720006; got 0x{tag:08X}"
);
let bitmap = u32::from_be_bytes([chan[12], chan[13], chan[14], chan[15]]);
assert_eq!(bitmap, 0, "mChannelBitmap must be 0 for tag form");
let ndescs = u32::from_be_bytes([chan[16], chan[17], chan[18], chan[19]]);
assert_eq!(
ndescs, 0,
"mNumberChannelDescriptions must be 0 for tag form"
);
}
#[test]
fn chan_box_7_1_layout_and_size() {
let chan = build_chan_box(7).expect("7.1 must emit chan");
assert_eq!(chan.len(), 20);
let tag = u32::from_be_bytes([chan[8], chan[9], chan[10], chan[11]]);
assert_eq!(
tag, 0x007F0008u32,
"7.1 tag must be kAudioChannelLayoutTag_MPEG_7_1_C = 0x007F0008; got 0x{tag:08X}"
);
}
#[test]
fn chan_nests_inside_mp4a_for_5_1() {
let info = AudioInfo {
codec: "aac".into(),
sample_rate: 48_000,
channels: 6,
timescale: 48_000,
asc_bytes: vec![0x11, 0xB0],
codec_private: Vec::new(),
};
let mp4a = build_mp4a(&info);
assert_eq!(&mp4a[4..8], b"mp4a", "outer box must be mp4a");
let chan_pos = mp4a
.windows(4)
.position(|w| w == b"chan")
.expect("multichannel mp4a must contain chan child");
let esds_pos = mp4a
.windows(4)
.position(|w| w == b"esds")
.expect("mp4a must always contain esds child");
assert!(
chan_pos > esds_pos,
"chan should come after esds in mp4a (esds @ {}, chan @ {})",
esds_pos,
chan_pos
);
}
#[test]
fn chan_absent_from_stereo_mp4a() {
let info = AudioInfo {
codec: "aac".into(),
sample_rate: 48_000,
channels: 2,
timescale: 48_000,
asc_bytes: vec![0x11, 0x90],
codec_private: Vec::new(),
};
let mp4a = build_mp4a(&info);
assert!(
mp4a.windows(4).all(|w| w != b"chan"),
"stereo mp4a must not contain a chan box"
);
}
use crate::ac3_sync::{Ac3SyncInfo, Eac3SyncInfo};
fn ac3_sync_5_1_384k_48k() -> Ac3SyncInfo {
Ac3SyncInfo {
fscod: 0,
bit_rate_code: 14,
bsid: 8,
bsmod: 0,
acmod: 7,
lfeon: true,
}
}
fn ac3_info_5_1_384k() -> AudioInfo {
let body = dac3_body_from_sync(&ac3_sync_5_1_384k_48k());
AudioInfo::ac3(48_000, 6, body.to_vec())
}
fn eac3_sync_5_1_48k() -> Eac3SyncInfo {
Eac3SyncInfo {
strmtyp: 0,
substreamid: 0,
frmsiz: 191,
fscod: 0,
fscod2: 0,
numblkscod: 3,
acmod: 7,
lfeon: true,
bsid: 16,
dialnorm: 0,
bsmod: 0,
}
}
fn eac3_info_5_1_384k() -> AudioInfo {
let body = dec3_body_from_sync(&eac3_sync_5_1_48k(), 192);
AudioInfo::eac3(48_000, 6, body.to_vec())
}
#[test]
fn dac3_box_3_byte_payload_layout() {
let info = ac3_info_5_1_384k();
let dac3 = build_dac3(&info);
assert_eq!(dac3.len(), 11, "dac3 = 8-byte header + 3-byte body");
let size = u32::from_be_bytes([dac3[0], dac3[1], dac3[2], dac3[3]]) as usize;
assert_eq!(size, dac3.len(), "size field equals box length");
assert_eq!(&dac3[4..8], b"dac3", "box type 'dac3'");
let raw = ((dac3[8] as u32) << 16) | ((dac3[9] as u32) << 8) | dac3[10] as u32;
assert_eq!((raw >> 22) & 0x03, 0, "fscod = 0 (48 kHz)");
assert_eq!((raw >> 17) & 0x1F, 8, "bsid = 8 (AC-3)");
assert_eq!((raw >> 14) & 0x07, 0, "bsmod = 0");
assert_eq!((raw >> 11) & 0x07, 7, "acmod = 7 (3/2 = 5.1 with LFE)");
assert_eq!((raw >> 10) & 0x01, 1, "lfeon = 1");
assert_eq!((raw >> 5) & 0x1F, 14, "bit_rate_code = 14 (= 384 kbps)");
assert_eq!(raw & 0x1F, 0, "reserved 5 bits = 0");
}
#[test]
fn ac3_sample_entry_size_and_fourcc() {
let info = ac3_info_5_1_384k();
let entry = build_ac3_sample_entry(&info);
let size = u32::from_be_bytes([entry[0], entry[1], entry[2], entry[3]]) as usize;
assert_eq!(size, entry.len(), "size field equals box length");
assert_eq!(&entry[4..8], b"ac-3", "4cc MUST be 'ac-3' (with hyphen)");
assert_ne!(
&entry[4..8],
b"ac3\0",
"4cc 'ac3' (3-char) is non-conformant"
);
assert_eq!(
entry.len(),
47,
"ac-3 sample entry = 36 (preamble) + 11 (dac3)"
);
let dac3_pos = entry
.windows(4)
.position(|w| w == b"dac3")
.expect("dac3 child missing");
assert!(
dac3_pos > 28,
"dac3 must come after AudioSampleEntry preamble"
);
let sr_q16 = u32::from_be_bytes([entry[32], entry[33], entry[34], entry[35]]);
assert_eq!(sr_q16, 48_000u32 << 16, "samplerate = 48000 << 16 (Q16)");
}
#[test]
fn dec3_box_5_byte_payload_layout() {
let info = eac3_info_5_1_384k();
let dec3 = build_dec3(&info);
assert_eq!(dec3.len(), 13, "dec3 = 8-byte header + 5-byte body");
let size = u32::from_be_bytes([dec3[0], dec3[1], dec3[2], dec3[3]]) as usize;
assert_eq!(size, dec3.len(), "size field equals box length");
assert_eq!(&dec3[4..8], b"dec3", "box type 'dec3'");
let header = ((dec3[8] as u16) << 8) | dec3[9] as u16;
let data_rate = (header >> 3) & 0x1FFF;
assert_eq!(data_rate, 192, "data_rate = 192 (= 384 kbps / 2)");
let num_ind_sub_minus_1 = header & 0x07;
assert_eq!(num_ind_sub_minus_1, 0, "single substream → field = 0");
let sub = ((dec3[10] as u32) << 16) | ((dec3[11] as u32) << 8) | dec3[12] as u32;
assert_eq!((sub >> 22) & 0x03, 0, "fscod = 0 (48 kHz)");
assert_eq!((sub >> 17) & 0x1F, 16, "bsid = 16 (E-AC-3 marker)");
assert_eq!((sub >> 12) & 0x07, 0, "bsmod = 0");
assert_eq!((sub >> 9) & 0x07, 7, "acmod = 7 (3/2 = 5.1 with LFE)");
assert_eq!((sub >> 8) & 0x01, 1, "lfeon = 1");
assert_eq!((sub >> 1) & 0x0F, 0, "num_dep_sub = 0 (single substream)");
}
#[test]
fn ec3_sample_entry_size_and_fourcc() {
let info = eac3_info_5_1_384k();
let entry = build_ec3_sample_entry(&info);
let size = u32::from_be_bytes([entry[0], entry[1], entry[2], entry[3]]) as usize;
assert_eq!(size, entry.len(), "size field equals box length");
assert_eq!(&entry[4..8], b"ec-3", "4cc MUST be 'ec-3' (with hyphen)");
assert_eq!(
entry.len(),
49,
"ec-3 sample entry = 36 (preamble) + 13 (dec3)"
);
let dec3_pos = entry
.windows(4)
.position(|w| w == b"dec3")
.expect("dec3 child missing");
assert!(
dec3_pos > 28,
"dec3 must come after AudioSampleEntry preamble"
);
}
#[test]
fn stsd_dispatcher_routes_ac3_eac3() {
let stsd_ac3 = build_audio_stsd(&ac3_info_5_1_384k());
assert!(
stsd_ac3.windows(4).any(|w| w == b"ac-3"),
"AC-3 stsd has 'ac-3'"
);
assert!(
stsd_ac3.windows(4).any(|w| w == b"dac3"),
"AC-3 stsd has 'dac3'"
);
assert!(
!stsd_ac3.windows(4).any(|w| w == b"mp4a"),
"AC-3 stsd MUST NOT have mp4a"
);
assert!(
!stsd_ac3.windows(4).any(|w| w == b"Opus"),
"AC-3 stsd MUST NOT have Opus"
);
assert!(
!stsd_ac3.windows(4).any(|w| w == b"esds"),
"AC-3 stsd MUST NOT have esds"
);
let stsd_eac3 = build_audio_stsd(&eac3_info_5_1_384k());
assert!(
stsd_eac3.windows(4).any(|w| w == b"ec-3"),
"E-AC-3 stsd has 'ec-3'"
);
assert!(
stsd_eac3.windows(4).any(|w| w == b"dec3"),
"E-AC-3 stsd has 'dec3'"
);
assert!(
!stsd_eac3.windows(4).any(|w| w == b"mp4a"),
"E-AC-3 stsd MUST NOT have mp4a"
);
assert!(
!stsd_eac3.windows(4).any(|w| w == b"esds"),
"E-AC-3 stsd MUST NOT have esds"
);
assert!(
!stsd_eac3.windows(4).any(|w| w == b"dac3"),
"E-AC-3 stsd MUST NOT have dac3"
);
}
#[test]
fn with_audio_accepts_ac3_5_1_and_rejects_bad_shape() {
let mut muxer = Av1Mp4Muxer::new(320, 240, 30.0).unwrap();
muxer
.with_audio(ac3_info_5_1_384k())
.expect("5.1 AC-3 must be accepted");
let mut muxer2 = Av1Mp4Muxer::new(320, 240, 30.0).unwrap();
let mut bad = ac3_info_5_1_384k();
bad.codec_private = vec![0u8; 2];
let err = muxer2
.with_audio(bad)
.err()
.expect("must reject 2-byte dac3");
assert!(format!("{err:#}").contains("3 bytes"));
let mut muxer3 = Av1Mp4Muxer::new(320, 240, 30.0).unwrap();
let bad_sr = AudioInfo {
sample_rate: 22_050,
timescale: 22_050,
..ac3_info_5_1_384k()
};
let err = muxer3
.with_audio(bad_sr)
.err()
.expect("must reject 22050 for AC-3");
assert!(format!("{err:#}").contains("32000"));
}
#[test]
fn with_audio_accepts_eac3_5_1_and_rejects_short_dec3() {
let mut muxer = Av1Mp4Muxer::new(320, 240, 30.0).unwrap();
muxer
.with_audio(eac3_info_5_1_384k())
.expect("5.1 E-AC-3 must be accepted");
let mut muxer2 = Av1Mp4Muxer::new(320, 240, 30.0).unwrap();
let mut bad = eac3_info_5_1_384k();
bad.codec_private = vec![0u8; 4];
let err = muxer2
.with_audio(bad)
.err()
.expect("must reject short dec3");
assert!(format!("{err:#}").contains("≥5"));
}
#[test]
fn with_audio_rejects_ac3_more_than_6_channels() {
let mut muxer = Av1Mp4Muxer::new(320, 240, 30.0).unwrap();
let bad = AudioInfo {
channels: 8,
..ac3_info_5_1_384k()
};
let err = muxer.with_audio(bad).err().expect("must reject 8 channels");
assert!(format!("{err:#}").contains("1..=6"));
}
#[test]
fn ac3_sync_to_dac3_to_sample_entry_roundtrip() {
let sync = ac3_sync_5_1_384k_48k();
let body = dac3_body_from_sync(&sync);
let info = AudioInfo::ac3(48_000, 6, body.to_vec());
let entry = build_ac3_sample_entry(&info);
let dac3_pos = entry.windows(4).position(|w| w == b"dac3").unwrap();
let dac3_body_start = dac3_pos + 4;
let raw = ((entry[dac3_body_start] as u32) << 16)
| ((entry[dac3_body_start + 1] as u32) << 8)
| entry[dac3_body_start + 2] as u32;
assert_eq!((raw >> 22) & 0x03, sync.fscod as u32);
assert_eq!((raw >> 17) & 0x1F, sync.bsid as u32);
assert_eq!((raw >> 11) & 0x07, sync.acmod as u32);
assert_eq!((raw >> 10) & 0x01, sync.lfeon as u32);
assert_eq!((raw >> 5) & 0x1F, sync.bit_rate_code as u32);
}
}