use anyhow::{Context, Result};
use codec::frame::ColorMetadata;
use std::fs::{self, File};
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use crate::AudioInfo;
use crate::mux::{BoxBuilder, build_audio_stsd, build_av01, write_unity_matrix};
pub mod brand {
pub const CMFC: &[u8; 4] = b"cmfc";
pub const CMFA: &[u8; 4] = b"cmfa";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum CmafTrackKind {
Video,
Audio,
}
#[derive(Debug, Clone, Copy)]
pub struct SampleFlags {
pub is_sync: bool,
}
impl SampleFlags {
pub fn pack(self) -> u32 {
if self.is_sync {
0x02_00_00_00
} else {
0x01_01_00_00
}
}
pub fn keyframe() -> Self {
Self { is_sync: true }
}
pub fn delta_frame() -> Self {
Self { is_sync: false }
}
}
#[derive(Debug, Clone, Copy)]
pub struct CmafSample {
pub duration: u32,
pub size: u32,
pub flags: SampleFlags,
}
pub fn build_mfhd(sequence_number: u32) -> Vec<u8> {
let mut b = BoxBuilder::new(b"mfhd");
b.u8(0); b.extend(&[0, 0, 0]); b.u32(sequence_number);
b.finish()
}
pub fn build_tfhd(
track_id: u32,
default_duration: Option<u32>,
default_size: Option<u32>,
default_flags: Option<u32>,
) -> Vec<u8> {
let mut tf_flags: u32 = 0x020000; if default_duration.is_some() {
tf_flags |= 0x000008;
}
if default_size.is_some() {
tf_flags |= 0x000010;
}
if default_flags.is_some() {
tf_flags |= 0x000020;
}
let mut b = BoxBuilder::new(b"tfhd");
b.u8(0); let flag_bytes = tf_flags.to_be_bytes();
b.extend(&flag_bytes[1..]); b.u32(track_id);
if let Some(d) = default_duration {
b.u32(d);
}
if let Some(s) = default_size {
b.u32(s);
}
if let Some(f) = default_flags {
b.u32(f);
}
b.finish()
}
pub fn build_tfdt(base_media_decode_time: u64) -> Vec<u8> {
let mut b = BoxBuilder::new(b"tfdt");
b.u8(1); b.extend(&[0, 0, 0]); b.u64(base_media_decode_time);
b.finish()
}
fn build_trun_video(samples: &[CmafSample]) -> (Vec<u8>, usize) {
let mut b = BoxBuilder::new(b"trun");
b.u8(0); let flags: u32 = 0x000001 | 0x000004 | 0x000100 | 0x000200;
let flag_bytes = flags.to_be_bytes();
b.extend(&flag_bytes[1..]);
b.u32(samples.len() as u32);
let data_offset_pos_within_trun = b.current_len();
b.u32(0);
if let Some(first) = samples.first() {
b.u32(first.flags.pack());
} else {
b.u32(0);
}
for s in samples {
b.u32(s.duration);
b.u32(s.size);
}
let bytes = b.finish();
(bytes, data_offset_pos_within_trun)
}
fn build_trun_audio(samples: &[CmafSample]) -> (Vec<u8>, usize) {
let mut b = BoxBuilder::new(b"trun");
b.u8(0); let flags: u32 = 0x000001 | 0x000100 | 0x000200;
let flag_bytes = flags.to_be_bytes();
b.extend(&flag_bytes[1..]);
b.u32(samples.len() as u32);
let data_offset_pos_within_trun = b.current_len();
b.u32(0);
for s in samples {
b.u32(s.duration);
b.u32(s.size);
}
let bytes = b.finish();
(bytes, data_offset_pos_within_trun)
}
pub fn build_mehd(fragment_duration: u64) -> Vec<u8> {
let mut b = BoxBuilder::new(b"mehd");
b.u8(1); b.extend(&[0, 0, 0]); b.u64(fragment_duration);
b.finish()
}
pub fn build_trex(track_id: u32, default_sample_flags: u32) -> Vec<u8> {
let mut b = BoxBuilder::new(b"trex");
b.u8(0); b.extend(&[0, 0, 0]); b.u32(track_id);
b.u32(1); b.u32(0); b.u32(0); b.u32(default_sample_flags);
b.finish()
}
pub fn build_mvex(mehd: &[u8], trexes: &[Vec<u8>]) -> Vec<u8> {
let mut b = BoxBuilder::new(b"mvex");
b.extend(mehd);
for trex in trexes {
b.extend(trex);
}
b.finish()
}
fn build_traf(tfhd: &[u8], tfdt: &[u8], trun: &[u8]) -> Vec<u8> {
let mut b = BoxBuilder::new(b"traf");
b.extend(tfhd);
b.extend(tfdt);
b.extend(trun);
b.finish()
}
pub struct MoofData {
pub bytes: Vec<u8>,
pub data_offset_pos: usize,
}
impl MoofData {
pub fn patch_data_offset(&mut self, data_offset: u32) {
self.bytes[self.data_offset_pos..self.data_offset_pos + 4]
.copy_from_slice(&data_offset.to_be_bytes());
}
pub fn patch_default_no_gap(&mut self) {
let off = (self.bytes.len() + 8) as u32;
self.patch_data_offset(off);
}
}
pub fn build_moof_video(
sequence_number: u32,
track_id: u32,
base_media_decode_time: u64,
samples: &[CmafSample],
) -> MoofData {
let mfhd = build_mfhd(sequence_number);
let tfhd = build_tfhd(
track_id,
None,
None,
Some(SampleFlags::delta_frame().pack()),
);
let tfdt = build_tfdt(base_media_decode_time);
let (trun, data_offset_pos_within_trun) = build_trun_video(samples);
let moof_header = 8usize;
let traf_header = 8usize;
let pos_in_moof = moof_header
+ mfhd.len()
+ traf_header
+ tfhd.len()
+ tfdt.len()
+ data_offset_pos_within_trun;
let traf = build_traf(&tfhd, &tfdt, &trun);
let mut b = BoxBuilder::new(b"moof");
b.extend(&mfhd);
b.extend(&traf);
let bytes = b.finish();
MoofData {
bytes,
data_offset_pos: pos_in_moof,
}
}
pub fn build_moof_audio(
sequence_number: u32,
track_id: u32,
base_media_decode_time: u64,
samples: &[CmafSample],
) -> MoofData {
let mfhd = build_mfhd(sequence_number);
let tfhd = build_tfhd(track_id, None, None, Some(SampleFlags::keyframe().pack()));
let tfdt = build_tfdt(base_media_decode_time);
let (trun, data_offset_pos_within_trun) = build_trun_audio(samples);
let moof_header = 8usize;
let traf_header = 8usize;
let pos_in_moof = moof_header
+ mfhd.len()
+ traf_header
+ tfhd.len()
+ tfdt.len()
+ data_offset_pos_within_trun;
let traf = build_traf(&tfhd, &tfdt, &trun);
let mut b = BoxBuilder::new(b"moof");
b.extend(&mfhd);
b.extend(&traf);
let bytes = b.finish();
MoofData {
bytes,
data_offset_pos: pos_in_moof,
}
}
pub fn build_init_segment_video(
width: u32,
height: u32,
timescale: u32,
config_obus: &[u8],
color_metadata: &ColorMetadata,
) -> Vec<u8> {
let track_id = 1u32;
let ftyp = build_ftyp_video();
let mvhd = build_mvhd(timescale, 0, 2);
let trak = build_video_trak(
width,
height,
timescale,
track_id,
config_obus,
color_metadata,
);
let mvex_blob = {
let mehd = build_mehd(0);
let trex = build_trex(track_id, SampleFlags::delta_frame().pack());
build_mvex(&mehd, &[trex])
};
let mut moov = BoxBuilder::new(b"moov");
moov.extend(&mvhd);
moov.extend(&trak);
moov.extend(&mvex_blob);
let moov = moov.finish();
let mut out = Vec::with_capacity(ftyp.len() + moov.len());
out.extend_from_slice(&ftyp);
out.extend_from_slice(&moov);
out
}
pub fn build_init_segment_audio(audio_info: &AudioInfo) -> Vec<u8> {
let track_id = 1u32;
let ftyp = build_ftyp_audio();
let mvhd = build_mvhd(
audio_info.timescale,
0,
2,
);
let trak = build_audio_trak(audio_info, track_id);
let mvex_blob = {
let mehd = build_mehd(0);
let trex = build_trex(track_id, SampleFlags::keyframe().pack());
build_mvex(&mehd, &[trex])
};
let mut moov = BoxBuilder::new(b"moov");
moov.extend(&mvhd);
moov.extend(&trak);
moov.extend(&mvex_blob);
let moov = moov.finish();
let mut out = Vec::with_capacity(ftyp.len() + moov.len());
out.extend_from_slice(&ftyp);
out.extend_from_slice(&moov);
out
}
fn build_ftyp_video() -> Vec<u8> {
let mut b = BoxBuilder::new(b"ftyp");
b.extend(b"iso6"); b.u32(0); b.extend(b"iso6");
b.extend(b"iso2");
b.extend(b"mp42");
b.extend(brand::CMFC);
b.extend(b"av01");
b.finish()
}
fn build_ftyp_audio() -> Vec<u8> {
let mut b = BoxBuilder::new(b"ftyp");
b.extend(b"iso6"); b.u32(0); b.extend(b"iso6");
b.extend(b"iso2");
b.extend(b"mp42");
b.extend(brand::CMFA);
b.finish()
}
fn build_mvhd(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,
timescale: u32,
track_id: u32,
config_obus: &[u8],
color_metadata: &ColorMetadata,
) -> Vec<u8> {
let tkhd = build_video_tkhd(width, height, track_id);
let mdia = build_video_mdia(width, height, timescale, config_obus, 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, track_id: u32) -> 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(track_id);
b.u32(0); b.u32(0); 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,
config_obus: &[u8],
color_metadata: &ColorMetadata,
) -> Vec<u8> {
let mdhd = build_mdhd(timescale, 0);
let hdlr = build_hdlr(b"vide", "VideoHandler\0");
let minf = build_video_minf(width, height, config_obus, 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_hdlr(handler_type: &[u8; 4], name: &str) -> Vec<u8> {
let mut b = BoxBuilder::new(b"hdlr");
b.u8(0);
b.extend(&[0, 0, 0]);
b.u32(0); b.extend(handler_type);
b.u32(0);
b.u32(0);
b.u32(0); b.extend(name.as_bytes());
b.finish()
}
fn build_video_minf(
width: u32,
height: u32,
config_obus: &[u8],
color_metadata: &ColorMetadata,
) -> Vec<u8> {
let vmhd = build_vmhd();
let dinf = build_dinf();
let stbl = build_video_stbl_empty(width, height, config_obus, 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_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_dinf() -> Vec<u8> {
let url = {
let mut b = BoxBuilder::new(b"url ");
b.u8(0); b.extend(&[0, 0, 0x01]); b.finish()
};
let dref = {
let mut b = BoxBuilder::new(b"dref");
b.u8(0);
b.extend(&[0, 0, 0]);
b.u32(1); b.extend(&url);
b.finish()
};
let mut b = BoxBuilder::new(b"dinf");
b.extend(&dref);
b.finish()
}
fn build_video_stbl_empty(
width: u32,
height: u32,
config_obus: &[u8],
color_metadata: &ColorMetadata,
) -> Vec<u8> {
let av01 = build_av01(width, height, config_obus, color_metadata);
let stsd = {
let mut b = BoxBuilder::new(b"stsd");
b.u8(0);
b.extend(&[0, 0, 0]);
b.u32(1); b.extend(&av01);
b.finish()
};
let stts = build_empty_full_box(b"stts");
let stsc = build_empty_full_box(b"stsc");
let stsz = {
let mut b = BoxBuilder::new(b"stsz");
b.u8(0);
b.extend(&[0, 0, 0]);
b.u32(0); b.u32(0); b.finish()
};
let stco = build_empty_full_box(b"stco");
let mut b = BoxBuilder::new(b"stbl");
b.extend(&stsd);
b.extend(&stts);
b.extend(&stsc);
b.extend(&stsz);
b.extend(&stco);
b.finish()
}
fn build_audio_trak(info: &AudioInfo, track_id: u32) -> Vec<u8> {
let tkhd = build_audio_tkhd(track_id);
let mdia = build_audio_mdia(info);
let mut b = BoxBuilder::new(b"trak");
b.extend(&tkhd);
b.extend(&mdia);
b.finish()
}
fn build_audio_tkhd(track_id: u32) -> 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(track_id);
b.u32(0);
b.u32(0);
b.u32(0);
b.u32(0);
b.u16(0); b.u16(0); b.u16(0x0100); b.u16(0); write_unity_matrix(&mut b);
b.u32(0);
b.u32(0); b.finish()
}
fn build_audio_mdia(info: &AudioInfo) -> Vec<u8> {
let mdhd = build_mdhd(info.timescale, 0);
let hdlr = build_hdlr(b"soun", "SoundHandler\0");
let minf = build_audio_minf(info);
let mut b = BoxBuilder::new(b"mdia");
b.extend(&mdhd);
b.extend(&hdlr);
b.extend(&minf);
b.finish()
}
fn build_audio_minf(info: &AudioInfo) -> Vec<u8> {
let smhd = build_smhd();
let dinf = build_dinf();
let stbl = build_audio_stbl_empty(info);
let mut b = BoxBuilder::new(b"minf");
b.extend(&smhd);
b.extend(&dinf);
b.extend(&stbl);
b.finish()
}
fn build_audio_stbl_empty(info: &AudioInfo) -> Vec<u8> {
let stsd = build_audio_stsd(info);
let stts = build_empty_full_box(b"stts");
let stsc = build_empty_full_box(b"stsc");
let stsz = {
let mut b = BoxBuilder::new(b"stsz");
b.u8(0);
b.extend(&[0, 0, 0]);
b.u32(0);
b.u32(0);
b.finish()
};
let stco = build_empty_full_box(b"stco");
let mut b = BoxBuilder::new(b"stbl");
b.extend(&stsd);
b.extend(&stts);
b.extend(&stsc);
b.extend(&stsz);
b.extend(&stco);
b.finish()
}
fn build_empty_full_box(box_type: &[u8; 4]) -> Vec<u8> {
let mut b = BoxBuilder::new(box_type);
b.u8(0);
b.extend(&[0, 0, 0]);
b.u32(0);
b.finish()
}
#[derive(Debug, Clone)]
pub struct SegmentInfo {
pub sequence_number: u32,
pub path: PathBuf,
pub byte_size: u64,
pub duration_ticks: u64,
}
#[derive(Debug, Clone)]
pub struct CmafTrackManifest {
pub init_path: PathBuf,
pub segments: Vec<SegmentInfo>,
pub timescale: u32,
}
impl CmafTrackManifest {
pub fn duration_seconds(&self) -> f64 {
let total_ticks: u64 = self.segments.iter().map(|s| s.duration_ticks).sum();
total_ticks as f64 / self.timescale as f64
}
}
struct PendingVideoSample {
payload: Vec<u8>,
duration: u32,
is_keyframe: bool,
}
struct PendingAudioSample {
payload: Vec<u8>,
duration: u32,
}
pub struct CmafVideoMuxer {
output_dir: PathBuf,
width: u32,
height: u32,
timescale: u32,
color_metadata: ColorMetadata,
track_id: u32,
config_obus: Option<Vec<u8>>, init_path: PathBuf,
init_written: bool,
sequence_number: u32,
base_decode_time: u64,
pending: Vec<PendingVideoSample>,
segments: Vec<SegmentInfo>,
}
#[derive(Debug, Clone)]
pub struct CmafVideoMuxerOptions {
pub first_segment_index: u32,
pub first_segment_base_decode_time: u64,
pub write_init_segment: bool,
}
impl Default for CmafVideoMuxerOptions {
fn default() -> Self {
Self {
first_segment_index: 1,
first_segment_base_decode_time: 0,
write_init_segment: true,
}
}
}
impl CmafVideoMuxer {
pub fn new(
output_dir: impl AsRef<Path>,
width: u32,
height: u32,
timescale: u32,
color_metadata: ColorMetadata,
) -> Result<Self> {
Self::new_with_options(
output_dir,
width,
height,
timescale,
color_metadata,
CmafVideoMuxerOptions::default(),
)
}
pub fn new_with_options(
output_dir: impl AsRef<Path>,
width: u32,
height: u32,
timescale: u32,
color_metadata: ColorMetadata,
options: CmafVideoMuxerOptions,
) -> Result<Self> {
assert!(
options.first_segment_index >= 1,
"first_segment_index is 1-based; got {}",
options.first_segment_index,
);
let output_dir = output_dir.as_ref().to_path_buf();
fs::create_dir_all(&output_dir)
.with_context(|| format!("creating CMAF video output dir: {}", output_dir.display()))?;
let init_path = output_dir.join("init.mp4");
Ok(Self {
output_dir,
width,
height,
timescale,
color_metadata,
track_id: 1,
config_obus: None,
init_path,
init_written: !options.write_init_segment,
sequence_number: options.first_segment_index - 1,
base_decode_time: options.first_segment_base_decode_time,
pending: Vec::new(),
segments: Vec::new(),
})
}
pub fn add_packet(&mut self, payload: Vec<u8>, duration: u32, is_keyframe: bool) -> Result<()> {
if self.config_obus.is_none() {
self.config_obus = Some(crate::mux::extract_sequence_header(&payload).context(
"extracting AV1 sequence header from first packet for av1C config record",
)?);
}
self.pending.push(PendingVideoSample {
payload,
duration,
is_keyframe,
});
Ok(())
}
pub fn first_pending_is_keyframe(&self) -> bool {
self.pending.first().is_some_and(|s| s.is_keyframe)
}
pub fn pending_duration_ticks(&self) -> u64 {
self.pending.iter().map(|s| s.duration as u64).sum()
}
pub fn segments(&self) -> &[SegmentInfo] {
&self.segments
}
pub fn clear_pending(&mut self) {
self.pending.clear();
}
pub fn flush_segment(&mut self) -> Result<Option<SegmentInfo>> {
if self.pending.is_empty() {
return Ok(None);
}
if !self.first_pending_is_keyframe() {
anyhow::bail!(
"CMAF segment must start with a sync sample; first pending sample is not a keyframe \
(segment_number={}, pending_count={})",
self.sequence_number + 1,
self.pending.len()
);
}
self.ensure_init_written()?;
self.sequence_number += 1;
let seq = self.sequence_number;
let samples_meta: Vec<CmafSample> = self
.pending
.iter()
.map(|s| CmafSample {
duration: s.duration,
size: s.payload.len() as u32,
flags: if s.is_keyframe {
SampleFlags::keyframe()
} else {
SampleFlags::delta_frame()
},
})
.collect();
let segment_duration: u64 = samples_meta.iter().map(|s| s.duration as u64).sum();
let mut moof = build_moof_video(seq, self.track_id, self.base_decode_time, &samples_meta);
moof.patch_default_no_gap();
let payload_total: u64 = self.pending.iter().map(|s| s.payload.len() as u64).sum();
let mdat_box_size: u64 = 8 + payload_total;
if mdat_box_size > u32::MAX as u64 {
anyhow::bail!(
"CMAF media segment payload {} bytes exceeds 32-bit mdat size limit",
payload_total
);
}
let path = self.output_dir.join(format!("seg-{:05}.m4s", seq));
let file = File::create(&path)
.with_context(|| format!("creating CMAF segment file: {}", path.display()))?;
let mut writer = BufWriter::new(file);
writer.write_all(&moof.bytes).context("writing moof")?;
writer
.write_all(&(mdat_box_size as u32).to_be_bytes())
.context("writing mdat size")?;
writer.write_all(b"mdat").context("writing mdat type")?;
for sample in &self.pending {
writer
.write_all(&sample.payload)
.context("writing mdat payload")?;
}
writer.flush().context("flushing CMAF segment writer")?;
let byte_size = moof.bytes.len() as u64 + mdat_box_size;
self.base_decode_time += segment_duration;
self.pending.clear();
let info = SegmentInfo {
sequence_number: seq,
path,
byte_size,
duration_ticks: segment_duration,
};
self.segments.push(info.clone());
Ok(Some(info))
}
pub fn finalize(mut self) -> Result<CmafTrackManifest> {
if !self.pending.is_empty() {
self.flush_segment()?;
}
self.ensure_init_written()?;
Ok(CmafTrackManifest {
init_path: self.init_path,
segments: self.segments,
timescale: self.timescale,
})
}
fn ensure_init_written(&mut self) -> Result<()> {
if self.init_written {
return Ok(());
}
let config = self.config_obus.as_ref().ok_or_else(|| {
anyhow::anyhow!(
"cannot write CMAF video init segment: no AV1 sequence header has been observed yet \
(must call add_packet at least once before flush_segment / finalize)"
)
})?;
let init = build_init_segment_video(
self.width,
self.height,
self.timescale,
config,
&self.color_metadata,
);
let mut file = File::create(&self.init_path).with_context(|| {
format!(
"creating CMAF video init segment: {}",
self.init_path.display()
)
})?;
file.write_all(&init)
.context("writing CMAF video init segment bytes")?;
file.flush().context("flushing CMAF video init segment")?;
self.init_written = true;
Ok(())
}
}
pub struct CmafAudioMuxer {
output_dir: PathBuf,
info: AudioInfo,
track_id: u32,
init_path: PathBuf,
init_written: bool,
sequence_number: u32,
base_decode_time: u64,
pending: Vec<PendingAudioSample>,
segments: Vec<SegmentInfo>,
}
impl CmafAudioMuxer {
pub fn new(output_dir: impl AsRef<Path>, info: AudioInfo) -> Result<Self> {
let output_dir = output_dir.as_ref().to_path_buf();
fs::create_dir_all(&output_dir)
.with_context(|| format!("creating CMAF audio output dir: {}", output_dir.display()))?;
let init_path = output_dir.join("init.mp4");
Ok(Self {
output_dir,
info,
track_id: 1,
init_path,
init_written: false,
sequence_number: 0,
base_decode_time: 0,
pending: Vec::new(),
segments: Vec::new(),
})
}
pub fn add_packet(&mut self, payload: Vec<u8>, duration: u32) -> Result<()> {
self.pending.push(PendingAudioSample { payload, duration });
Ok(())
}
pub fn pending_duration_ticks(&self) -> u64 {
self.pending.iter().map(|s| s.duration as u64).sum()
}
pub fn flush_segment(&mut self) -> Result<Option<SegmentInfo>> {
if self.pending.is_empty() {
return Ok(None);
}
self.ensure_init_written()?;
self.sequence_number += 1;
let seq = self.sequence_number;
let samples_meta: Vec<CmafSample> = self
.pending
.iter()
.map(|s| CmafSample {
duration: s.duration,
size: s.payload.len() as u32,
flags: SampleFlags::keyframe(),
})
.collect();
let segment_duration: u64 = samples_meta.iter().map(|s| s.duration as u64).sum();
let mut moof = build_moof_audio(seq, self.track_id, self.base_decode_time, &samples_meta);
moof.patch_default_no_gap();
let payload_total: u64 = self.pending.iter().map(|s| s.payload.len() as u64).sum();
let mdat_box_size: u64 = 8 + payload_total;
if mdat_box_size > u32::MAX as u64 {
anyhow::bail!(
"CMAF audio media segment payload {} bytes exceeds 32-bit mdat size limit",
payload_total
);
}
let path = self.output_dir.join(format!("seg-{:05}.m4s", seq));
let file = File::create(&path)
.with_context(|| format!("creating CMAF audio segment file: {}", path.display()))?;
let mut writer = BufWriter::new(file);
writer
.write_all(&moof.bytes)
.context("writing audio moof")?;
writer
.write_all(&(mdat_box_size as u32).to_be_bytes())
.context("writing audio mdat size")?;
writer
.write_all(b"mdat")
.context("writing audio mdat type")?;
for sample in &self.pending {
writer
.write_all(&sample.payload)
.context("writing audio mdat payload")?;
}
writer
.flush()
.context("flushing CMAF audio segment writer")?;
let byte_size = moof.bytes.len() as u64 + mdat_box_size;
self.base_decode_time += segment_duration;
self.pending.clear();
let info = SegmentInfo {
sequence_number: seq,
path,
byte_size,
duration_ticks: segment_duration,
};
self.segments.push(info.clone());
Ok(Some(info))
}
pub fn finalize(mut self) -> Result<CmafTrackManifest> {
if !self.pending.is_empty() {
self.flush_segment()?;
}
self.ensure_init_written()?;
let timescale = self.info.timescale;
Ok(CmafTrackManifest {
init_path: self.init_path,
segments: self.segments,
timescale,
})
}
fn ensure_init_written(&mut self) -> Result<()> {
if self.init_written {
return Ok(());
}
let init = build_init_segment_audio(&self.info);
let mut file = File::create(&self.init_path).with_context(|| {
format!(
"creating CMAF audio init segment: {}",
self.init_path.display()
)
})?;
file.write_all(&init)
.context("writing CMAF audio init segment bytes")?;
file.flush().context("flushing CMAF audio init segment")?;
self.init_written = true;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn read_be_u32(buf: &[u8], pos: usize) -> u32 {
u32::from_be_bytes(buf[pos..pos + 4].try_into().unwrap())
}
fn read_be_u64(buf: &[u8], pos: usize) -> u64 {
u64::from_be_bytes(buf[pos..pos + 8].try_into().unwrap())
}
fn box_size_and_type(buf: &[u8]) -> (u32, &[u8]) {
let size = read_be_u32(buf, 0);
let kind = &buf[4..8];
(size, kind)
}
#[test]
fn mfhd_layout_is_16_bytes_with_sequence_number() {
let bytes = build_mfhd(42);
assert_eq!(bytes.len(), 16);
let (size, kind) = box_size_and_type(&bytes);
assert_eq!(size, 16);
assert_eq!(kind, b"mfhd");
assert_eq!(bytes[8], 0); assert_eq!(&bytes[9..12], &[0, 0, 0]); assert_eq!(read_be_u32(&bytes, 12), 42);
}
#[test]
fn tfhd_minimal_track_id_only_is_16_bytes() {
let bytes = build_tfhd(1, None, None, None);
assert_eq!(bytes.len(), 16);
let (size, kind) = box_size_and_type(&bytes);
assert_eq!(size, 16);
assert_eq!(kind, b"tfhd");
let flag_bytes = [0u8, bytes[9], bytes[10], bytes[11]];
let flags = u32::from_be_bytes(flag_bytes);
assert_eq!(flags, 0x020000);
assert_eq!(read_be_u32(&bytes, 12), 1);
}
#[test]
fn tfhd_with_default_flags_only_packs_correct_bits() {
let bytes = build_tfhd(1, None, None, Some(SampleFlags::delta_frame().pack()));
assert_eq!(bytes.len(), 20);
let flag_bytes = [0u8, bytes[9], bytes[10], bytes[11]];
let flags = u32::from_be_bytes(flag_bytes);
assert_eq!(flags, 0x020020);
assert_eq!(read_be_u32(&bytes, 12), 1);
assert_eq!(read_be_u32(&bytes, 16), SampleFlags::delta_frame().pack());
}
#[test]
fn tfhd_with_all_defaults_packs_in_spec_order() {
let bytes = build_tfhd(1, Some(1024), Some(2048), Some(0x01010000));
assert_eq!(bytes.len(), 28);
let flag_bytes = [0u8, bytes[9], bytes[10], bytes[11]];
let flags = u32::from_be_bytes(flag_bytes);
assert_eq!(flags, 0x020038);
assert_eq!(read_be_u32(&bytes, 12), 1);
assert_eq!(read_be_u32(&bytes, 16), 1024); assert_eq!(read_be_u32(&bytes, 20), 2048); assert_eq!(read_be_u32(&bytes, 24), 0x01010000); }
#[test]
fn tfdt_v1_carries_u64_decode_time() {
let bytes = build_tfdt(0x0123_4567_89AB_CDEF);
assert_eq!(bytes.len(), 20);
assert_eq!(box_size_and_type(&bytes), (20, b"tfdt".as_slice()));
assert_eq!(bytes[8], 1); assert_eq!(read_be_u64(&bytes, 12), 0x0123_4567_89AB_CDEF);
}
#[test]
fn mehd_v1_carries_u64_fragment_duration() {
let bytes = build_mehd(1_000_000);
assert_eq!(bytes.len(), 20);
assert_eq!(box_size_and_type(&bytes), (20, b"mehd".as_slice()));
assert_eq!(bytes[8], 1);
assert_eq!(read_be_u64(&bytes, 12), 1_000_000);
}
#[test]
fn trex_layout_is_32_bytes_with_track_id_and_flags() {
let default_flags = SampleFlags::delta_frame().pack();
let bytes = build_trex(2, default_flags);
assert_eq!(bytes.len(), 32);
assert_eq!(box_size_and_type(&bytes), (32, b"trex".as_slice()));
assert_eq!(read_be_u32(&bytes, 12), 2); assert_eq!(read_be_u32(&bytes, 16), 1); assert_eq!(read_be_u32(&bytes, 20), 0); assert_eq!(read_be_u32(&bytes, 24), 0); assert_eq!(read_be_u32(&bytes, 28), default_flags);
}
#[test]
fn sample_flags_pack_distinguishes_sync_from_delta() {
let sync = SampleFlags::keyframe().pack();
let delta = SampleFlags::delta_frame().pack();
assert_ne!(sync, delta);
assert_eq!(sync, 0x02_00_00_00);
assert_eq!(delta, 0x01_01_00_00);
}
#[test]
fn moof_video_one_keyframe_sample_round_trip() {
let samples = vec![CmafSample {
duration: 1500,
size: 4096,
flags: SampleFlags::keyframe(),
}];
let mut moof = build_moof_video(1, 1, 0, &samples);
moof.patch_default_no_gap();
let (size, kind) = box_size_and_type(&moof.bytes);
assert_eq!(size as usize, moof.bytes.len());
assert_eq!(kind, b"moof");
let (mfhd_size, mfhd_kind) = box_size_and_type(&moof.bytes[8..]);
assert_eq!(mfhd_size, 16);
assert_eq!(mfhd_kind, b"mfhd");
assert_eq!(read_be_u32(&moof.bytes, 8 + 12), 1);
let traf_start = 8 + mfhd_size as usize;
let (_, traf_kind) = box_size_and_type(&moof.bytes[traf_start..]);
assert_eq!(traf_kind, b"traf");
let patched = read_be_u32(&moof.bytes, moof.data_offset_pos);
assert_eq!(patched as usize, moof.bytes.len() + 8);
let first_flags = read_be_u32(&moof.bytes, moof.data_offset_pos + 4);
assert_eq!(first_flags, SampleFlags::keyframe().pack());
}
#[test]
fn moof_video_three_samples_records_per_sample_dur_and_size() {
let samples = vec![
CmafSample {
duration: 1500,
size: 4096,
flags: SampleFlags::keyframe(),
},
CmafSample {
duration: 1500,
size: 1024,
flags: SampleFlags::delta_frame(),
},
CmafSample {
duration: 1500,
size: 1024,
flags: SampleFlags::delta_frame(),
},
];
let mut moof = build_moof_video(2, 1, 6000, &samples);
moof.patch_default_no_gap();
let trun_start = 8 + 16 + 8 + 20 + 20;
let (_, trun_kind) = box_size_and_type(&moof.bytes[trun_start..]);
assert_eq!(trun_kind, b"trun");
let sample_count = read_be_u32(&moof.bytes, trun_start + 12);
assert_eq!(sample_count, 3);
let table_start = trun_start + 24;
assert_eq!(read_be_u32(&moof.bytes, table_start), 1500);
assert_eq!(read_be_u32(&moof.bytes, table_start + 4), 4096);
assert_eq!(read_be_u32(&moof.bytes, table_start + 8), 1500);
assert_eq!(read_be_u32(&moof.bytes, table_start + 12), 1024);
assert_eq!(read_be_u32(&moof.bytes, table_start + 16), 1500);
assert_eq!(read_be_u32(&moof.bytes, table_start + 20), 1024);
}
#[test]
fn moof_audio_does_not_emit_first_sample_flags() {
let samples = vec![
CmafSample {
duration: 1024,
size: 256,
flags: SampleFlags::keyframe(),
},
CmafSample {
duration: 1024,
size: 256,
flags: SampleFlags::keyframe(),
},
];
let mut moof = build_moof_audio(1, 2, 0, &samples);
moof.patch_default_no_gap();
let trun_start = 8 + 16 + 8 + 20 + 20;
let flag_bytes = [
0u8,
moof.bytes[trun_start + 9],
moof.bytes[trun_start + 10],
moof.bytes[trun_start + 11],
];
let flags = u32::from_be_bytes(flag_bytes);
assert_eq!(flags, 0x000001 | 0x000100 | 0x000200);
let table_start = trun_start + 20;
assert_eq!(read_be_u32(&moof.bytes, table_start), 1024); assert_eq!(read_be_u32(&moof.bytes, table_start + 4), 256); assert_eq!(read_be_u32(&moof.bytes, table_start + 8), 1024); assert_eq!(read_be_u32(&moof.bytes, table_start + 12), 256); }
#[test]
fn moof_data_offset_patch_is_at_correct_position() {
let samples = vec![CmafSample {
duration: 1500,
size: 1234,
flags: SampleFlags::keyframe(),
}];
let mut moof = build_moof_video(1, 1, 0, &samples);
moof.patch_data_offset(0xDEAD_BEEF);
let read_back = read_be_u32(&moof.bytes, moof.data_offset_pos);
assert_eq!(read_back, 0xDEAD_BEEF);
}
fn synthetic_seq_header_packet() -> Vec<u8> {
let header_byte: u8 = (1 << 3) | (1 << 1); vec![header_byte, 0x01, 0xAA]
}
fn find_box<'a>(buf: &'a [u8], box_type: &[u8; 4]) -> Option<&'a [u8]> {
let mut pos = 0;
while pos + 8 <= buf.len() {
let size = read_be_u32(buf, pos) as usize;
if size < 8 || pos + size > buf.len() {
return None;
}
let kind = &buf[pos + 4..pos + 8];
if kind == box_type {
return Some(&buf[pos..pos + size]);
}
pos += size;
}
None
}
fn ftyp_compatible_brands(ftyp: &[u8]) -> Vec<&[u8]> {
let mut brands = Vec::new();
let mut p = 16;
while p + 4 <= ftyp.len() {
brands.push(&ftyp[p..p + 4]);
p += 4;
}
brands
}
#[test]
fn init_segment_video_lists_cmfc_and_av01_brands() {
let init = build_init_segment_video(
1920,
1080,
30000,
&synthetic_seq_header_packet(),
&ColorMetadata::default(),
);
let ftyp = find_box(&init, b"ftyp").expect("init has ftyp");
let brands = ftyp_compatible_brands(ftyp);
assert!(
brands.contains(&b"cmfc".as_slice()),
"cmfc brand missing: {brands:?}"
);
assert!(
brands.contains(&b"av01".as_slice()),
"av01 brand missing: {brands:?}"
);
assert!(
brands.contains(&b"iso6".as_slice()),
"iso6 brand missing: {brands:?}"
);
}
#[test]
fn init_segment_audio_lists_cmfa_brand() {
let info = AudioInfo::aac_lc(48000, 2, vec![0x11, 0x90]);
let init = build_init_segment_audio(&info);
let ftyp = find_box(&init, b"ftyp").expect("init has ftyp");
let brands = ftyp_compatible_brands(ftyp);
assert!(
brands.contains(&b"cmfa".as_slice()),
"cmfa brand missing: {brands:?}"
);
assert!(
!brands.contains(&b"cmfc".as_slice()),
"cmfc should not appear in audio init"
);
}
#[test]
fn init_segment_video_moov_contains_mvex_with_trex() {
let init = build_init_segment_video(
1280,
720,
30000,
&synthetic_seq_header_packet(),
&ColorMetadata::default(),
);
let moov = find_box(&init, b"moov").expect("init has moov");
let mvex = find_box(&moov[8..], b"mvex").expect("moov has mvex");
assert!(
find_box(&mvex[8..], b"trex").is_some(),
"mvex must contain trex"
);
assert!(
find_box(&mvex[8..], b"mehd").is_some(),
"mvex must contain mehd"
);
}
#[test]
fn init_segment_video_stbl_has_empty_sample_tables() {
let init = build_init_segment_video(
1280,
720,
30000,
&synthetic_seq_header_packet(),
&ColorMetadata::default(),
);
let moov = find_box(&init, b"moov").expect("init has moov");
let trak = find_box(&moov[8..], b"trak").expect("moov has trak");
let mdia = find_box(&trak[8..], b"mdia").expect("trak has mdia");
let minf = find_box(&mdia[8..], b"minf").expect("mdia has minf");
let stbl = find_box(&minf[8..], b"stbl").expect("minf has stbl");
let stsz = find_box(&stbl[8..], b"stsz").expect("stbl has stsz");
assert_eq!(stsz.len(), 20);
assert_eq!(read_be_u32(stsz, 12), 0); assert_eq!(read_be_u32(stsz, 16), 0);
for box_type in [b"stts", b"stsc", b"stco"] {
let bx = find_box(&stbl[8..], box_type).expect("stbl has empty full box");
assert_eq!(
bx.len(),
16,
"{:?} should be 16-byte empty FullBox",
std::str::from_utf8(box_type).unwrap()
);
assert_eq!(read_be_u32(bx, 12), 0); }
let stsd = find_box(&stbl[8..], b"stsd").expect("stbl has stsd");
assert_eq!(read_be_u32(stsd, 12), 1); let av01 = &stsd[16..];
assert_eq!(&av01[4..8], b"av01");
}
#[test]
fn cmaf_video_muxer_emits_init_then_segment_files() {
let dir = tempfile::tempdir().unwrap();
let mut muxer =
CmafVideoMuxer::new(dir.path(), 1280, 720, 30000, ColorMetadata::default()).unwrap();
let mut k = synthetic_seq_header_packet();
k.extend_from_slice(&[0xDE, 0xAD]);
muxer.add_packet(k, 1500, true).unwrap();
muxer
.add_packet(synthetic_seq_header_packet(), 1500, false)
.unwrap();
let info = muxer
.flush_segment()
.unwrap()
.expect("flush emits a segment");
assert_eq!(info.sequence_number, 1);
assert_eq!(info.duration_ticks, 3000);
assert!(info.path.exists());
assert_eq!(info.path.file_name().unwrap(), "seg-00001.m4s");
let init_path = dir.path().join("init.mp4");
assert!(init_path.exists(), "init.mp4 must exist after first flush");
let seg_bytes = std::fs::read(&info.path).unwrap();
assert_eq!(&seg_bytes[4..8], b"moof");
let moof_size = read_be_u32(&seg_bytes, 0) as usize;
assert_eq!(&seg_bytes[moof_size + 4..moof_size + 8], b"mdat");
let manifest = muxer.finalize().unwrap();
assert_eq!(manifest.segments.len(), 1);
assert_eq!(manifest.timescale, 30000);
assert!((manifest.duration_seconds() - 0.1).abs() < 1e-6); }
#[test]
fn cmaf_video_muxer_options_default_matches_legacy_new() {
let dir_a = tempfile::tempdir().unwrap();
let dir_b = tempfile::tempdir().unwrap();
let mut ma = CmafVideoMuxer::new(
dir_a.path(),
1280,
720,
30000,
ColorMetadata::default(),
)
.unwrap();
let mut mb = CmafVideoMuxer::new_with_options(
dir_b.path(),
1280,
720,
30000,
ColorMetadata::default(),
CmafVideoMuxerOptions::default(),
)
.unwrap();
let mut kf = synthetic_seq_header_packet();
kf.extend_from_slice(&[0xDE, 0xAD]);
ma.add_packet(kf.clone(), 1500, true).unwrap();
mb.add_packet(kf, 1500, true).unwrap();
let info_a = ma.flush_segment().unwrap().unwrap();
let info_b = mb.flush_segment().unwrap().unwrap();
assert_eq!(info_a.sequence_number, info_b.sequence_number);
assert_eq!(info_a.duration_ticks, info_b.duration_ticks);
assert_eq!(
info_a.path.file_name().unwrap(),
info_b.path.file_name().unwrap(),
);
let bytes_a = std::fs::read(&info_a.path).unwrap();
let bytes_b = std::fs::read(&info_b.path).unwrap();
assert_eq!(bytes_a, bytes_b);
assert!(dir_a.path().join("init.mp4").exists());
assert!(dir_b.path().join("init.mp4").exists());
}
#[test]
fn cmaf_video_muxer_first_segment_index_offset_writes_correct_filename() {
let dir = tempfile::tempdir().unwrap();
let mut muxer = CmafVideoMuxer::new_with_options(
dir.path(),
1280,
720,
30000,
ColorMetadata::default(),
CmafVideoMuxerOptions {
first_segment_index: 5,
first_segment_base_decode_time: 4 * 3000, write_init_segment: true,
},
)
.unwrap();
let mut kf = synthetic_seq_header_packet();
kf.extend_from_slice(&[0xCA, 0xFE]);
muxer.add_packet(kf, 1500, true).unwrap();
muxer
.add_packet(synthetic_seq_header_packet(), 1500, false)
.unwrap();
let info = muxer.flush_segment().unwrap().unwrap();
assert_eq!(
info.sequence_number, 5,
"first flush of an offset muxer must produce segment number 5",
);
assert_eq!(info.path.file_name().unwrap(), "seg-00005.m4s");
let mut kf2 = synthetic_seq_header_packet();
kf2.extend_from_slice(&[0xBE, 0xEF]);
muxer.add_packet(kf2, 1500, true).unwrap();
let info2 = muxer.flush_segment().unwrap().unwrap();
assert_eq!(info2.sequence_number, 6);
assert_eq!(info2.path.file_name().unwrap(), "seg-00006.m4s");
}
#[test]
fn cmaf_video_muxer_offset_base_decode_time_propagates_to_tfdt() {
let dir = tempfile::tempdir().unwrap();
let mut muxer = CmafVideoMuxer::new_with_options(
dir.path(),
1280,
720,
30000,
ColorMetadata::default(),
CmafVideoMuxerOptions {
first_segment_index: 5,
first_segment_base_decode_time: 4 * 3000,
write_init_segment: true,
},
)
.unwrap();
let mut kf = synthetic_seq_header_packet();
kf.extend_from_slice(&[0x01, 0x02]);
muxer.add_packet(kf, 1500, true).unwrap();
let info = muxer.flush_segment().unwrap().unwrap();
let bytes = std::fs::read(&info.path).unwrap();
let moof_size = read_be_u32(&bytes, 0) as usize;
let moof = &bytes[..moof_size];
let traf = find_box(&moof[8..], b"traf").expect("moof has traf");
let tfdt = find_box(&traf[8..], b"tfdt").expect("traf has tfdt");
let version = tfdt[8];
assert_eq!(version, 1, "tfdt should be version 1 (u64 decode time)");
let dt = u64::from_be_bytes([
tfdt[12], tfdt[13], tfdt[14], tfdt[15], tfdt[16], tfdt[17], tfdt[18], tfdt[19],
]);
assert_eq!(
dt, 12000,
"tfdt base_media_decode_time must equal configured offset (4×3000)",
);
}
#[test]
fn cmaf_video_muxer_write_init_false_skips_init_file() {
let dir = tempfile::tempdir().unwrap();
let mut muxer = CmafVideoMuxer::new_with_options(
dir.path(),
1280,
720,
30000,
ColorMetadata::default(),
CmafVideoMuxerOptions {
first_segment_index: 5,
first_segment_base_decode_time: 4 * 3000,
write_init_segment: false,
},
)
.unwrap();
let mut kf = synthetic_seq_header_packet();
kf.extend_from_slice(&[0x03, 0x04]);
muxer.add_packet(kf, 1500, true).unwrap();
let info = muxer.flush_segment().unwrap().unwrap();
assert!(
info.path.exists(),
"segment file must be written even when init is skipped",
);
let init_path = dir.path().join("init.mp4");
assert!(
!init_path.exists(),
"init.mp4 must NOT be written when write_init_segment=false",
);
let _ = muxer.finalize().unwrap();
assert!(
!init_path.exists(),
"finalize must not retroactively write init.mp4 when disabled",
);
}
#[test]
fn cmaf_video_muxer_two_writers_share_output_dir_with_distinct_indices() {
let dir = tempfile::tempdir().unwrap();
let mut primary = CmafVideoMuxer::new(
dir.path(),
1280,
720,
30000,
ColorMetadata::default(),
)
.unwrap();
let mut helper = CmafVideoMuxer::new_with_options(
dir.path(),
1280,
720,
30000,
ColorMetadata::default(),
CmafVideoMuxerOptions {
first_segment_index: 3,
first_segment_base_decode_time: 2 * 3000,
write_init_segment: false,
},
)
.unwrap();
for _ in 0..2 {
let mut kf = synthetic_seq_header_packet();
kf.extend_from_slice(&[0xAA, 0xBB]);
primary.add_packet(kf, 1500, true).unwrap();
primary
.add_packet(synthetic_seq_header_packet(), 1500, false)
.unwrap();
primary.flush_segment().unwrap().unwrap();
}
for _ in 0..2 {
let mut kf = synthetic_seq_header_packet();
kf.extend_from_slice(&[0xCC, 0xDD]);
helper.add_packet(kf, 1500, true).unwrap();
helper
.add_packet(synthetic_seq_header_packet(), 1500, false)
.unwrap();
helper.flush_segment().unwrap().unwrap();
}
primary.finalize().unwrap();
helper.finalize().unwrap();
for seg_idx in 1..=4 {
let p = dir.path().join(format!("seg-{seg_idx:05}.m4s"));
assert!(p.exists(), "segment {seg_idx} missing at {}", p.display());
}
let init_path = dir.path().join("init.mp4");
assert!(init_path.exists(), "primary's init.mp4 must be present");
}
#[test]
#[should_panic(expected = "first_segment_index is 1-based")]
fn cmaf_video_muxer_first_segment_index_zero_panics() {
let dir = tempfile::tempdir().unwrap();
let _ = CmafVideoMuxer::new_with_options(
dir.path(),
1280,
720,
30000,
ColorMetadata::default(),
CmafVideoMuxerOptions {
first_segment_index: 0,
first_segment_base_decode_time: 0,
write_init_segment: true,
},
);
}
#[test]
fn cmaf_video_muxer_rejects_segment_starting_on_non_keyframe() {
let dir = tempfile::tempdir().unwrap();
let mut muxer =
CmafVideoMuxer::new(dir.path(), 640, 360, 30000, ColorMetadata::default()).unwrap();
muxer
.add_packet(synthetic_seq_header_packet(), 1500, false)
.unwrap();
let err = muxer
.flush_segment()
.expect_err("must fail when first sample is not sync");
assert!(err.to_string().contains("must start with a sync sample"));
}
#[test]
fn cmaf_audio_muxer_emits_init_and_segments_with_correct_durations() {
let info = AudioInfo {
codec: "aac".into(),
sample_rate: 48000,
channels: 2,
timescale: 48000,
asc_bytes: vec![0x12, 0x10],
codec_private: vec![],
};
let dir = tempfile::tempdir().unwrap();
let mut muxer = CmafAudioMuxer::new(dir.path(), info).unwrap();
for _ in 0..5 {
muxer.add_packet(vec![0xDE; 256], 1024).unwrap();
}
let seg = muxer
.flush_segment()
.unwrap()
.expect("audio segment emitted");
assert_eq!(seg.duration_ticks, 5 * 1024);
assert!(seg.path.exists());
let init_path = dir.path().join("init.mp4");
assert!(init_path.exists());
let bytes = std::fs::read(&seg.path).unwrap();
assert_eq!(&bytes[4..8], b"moof");
let manifest = muxer.finalize().unwrap();
assert_eq!(manifest.timescale, 48000);
assert!((manifest.duration_seconds() - (5.0 * 1024.0 / 48000.0)).abs() < 1e-6);
}
#[test]
fn mvex_wraps_mehd_and_one_or_more_trex_in_order() {
let mehd = build_mehd(10_000);
let trex_v = build_trex(1, SampleFlags::delta_frame().pack());
let trex_a = build_trex(2, SampleFlags::keyframe().pack());
let mvex = build_mvex(&mehd, &[trex_v.clone(), trex_a.clone()]);
let (size, kind) = box_size_and_type(&mvex);
assert_eq!(size as usize, mvex.len());
assert_eq!(kind, b"mvex");
assert_eq!(mvex.len(), 8 + mehd.len() + trex_v.len() + trex_a.len());
let (_, child0_kind) = box_size_and_type(&mvex[8..]);
assert_eq!(child0_kind, b"mehd");
let (_, child1_kind) = box_size_and_type(&mvex[8 + mehd.len()..]);
assert_eq!(child1_kind, b"trex");
}
}