#![forbid(unsafe_code)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
use oximedia_core::{CodecId, MediaType, OxiError, OxiResult, Rational};
use super::av1c::build_av1c_from_extradata;
use crate::mux::cmaf::{
build_empty_stco, build_empty_stsc, build_empty_stsz, build_empty_stts, build_mfhd, build_traf,
write_box, write_full_box, write_u32_be, write_u64_be, FragSampleEntry, FragTrackRun,
};
use crate::{Packet, StreamInfo};
const MOVIE_TIMESCALE: u32 = 1000;
const MAX_TRACKS: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mp4FragmentMode {
Progressive,
Fragmented {
fragment_duration_ms: u32,
},
}
impl Default for Mp4FragmentMode {
fn default() -> Self {
Self::Progressive
}
}
pub type Mp4Mode = Mp4FragmentMode;
#[derive(Debug, Clone)]
pub struct Mp4Config {
pub mode: Mp4FragmentMode,
pub major_brand: [u8; 4],
pub minor_version: u32,
pub compatible_brands: Vec<[u8; 4]>,
pub creation_time: u64,
pub modification_time: u64,
}
impl Default for Mp4Config {
fn default() -> Self {
Self {
mode: Mp4FragmentMode::Progressive,
major_brand: *b"isom",
minor_version: 0x200,
compatible_brands: vec![*b"isom", *b"iso6", *b"mp41"],
creation_time: 0,
modification_time: 0,
}
}
}
impl Mp4Config {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_mode(mut self, mode: Mp4FragmentMode) -> Self {
self.mode = mode;
self
}
#[must_use]
pub fn with_fragmented(mut self, fragment_duration_ms: u32) -> Self {
self.mode = Mp4FragmentMode::Fragmented {
fragment_duration_ms,
};
self
}
#[must_use]
pub const fn with_major_brand(mut self, brand: [u8; 4]) -> Self {
self.major_brand = brand;
self
}
#[must_use]
pub const fn with_minor_version(mut self, version: u32) -> Self {
self.minor_version = version;
self
}
#[must_use]
pub fn with_compatible_brand(mut self, brand: [u8; 4]) -> Self {
self.compatible_brands.push(brand);
self
}
}
#[derive(Debug, Clone)]
pub struct Mp4SampleEntry {
pub size: u32,
pub duration: u32,
pub composition_offset: i32,
pub is_sync: bool,
pub chunk_index: u32,
}
#[derive(Debug, Clone)]
pub struct Mp4TrackState {
pub stream_info: StreamInfo,
pub track_id: u32,
pub timescale: u32,
pub samples: Vec<Mp4SampleEntry>,
pub mdat_data: Vec<u8>,
pub chunk_boundaries: Vec<u32>,
pub current_chunk_samples: u32,
pub max_samples_per_chunk: u32,
pub total_duration: u64,
pub handler_type: [u8; 4],
pub handler_name: String,
}
impl Mp4TrackState {
fn new(stream_info: StreamInfo, track_id: u32) -> Self {
let (timescale, handler_type, handler_name) = match stream_info.media_type {
MediaType::Video => {
let ts = if stream_info.timebase.den != 0 {
stream_info.timebase.den as u32
} else {
90000
};
(ts, *b"vide", "OxiMedia Video Handler".to_string())
}
MediaType::Audio => {
let ts = stream_info.codec_params.sample_rate.unwrap_or(48000);
(ts, *b"soun", "OxiMedia Audio Handler".to_string())
}
_ => (1000, *b"text", "OxiMedia Text Handler".to_string()),
};
Self {
stream_info,
track_id,
timescale,
samples: Vec::new(),
mdat_data: Vec::new(),
chunk_boundaries: vec![0],
current_chunk_samples: 0,
max_samples_per_chunk: 10,
total_duration: 0,
handler_type,
handler_name,
}
}
fn add_sample(&mut self, data: &[u8], duration: u32, comp_offset: i32, is_sync: bool) {
let chunk_index = self.chunk_boundaries.len().saturating_sub(1) as u32;
self.samples.push(Mp4SampleEntry {
size: data.len() as u32,
duration,
composition_offset: comp_offset,
is_sync,
chunk_index,
});
self.mdat_data.extend_from_slice(data);
self.total_duration += u64::from(duration);
self.current_chunk_samples += 1;
if self.current_chunk_samples >= self.max_samples_per_chunk {
self.chunk_boundaries.push(self.samples.len() as u32);
self.current_chunk_samples = 0;
}
}
}
#[derive(Debug)]
pub struct Mp4Muxer {
config: Mp4Config,
tracks: Vec<Mp4TrackState>,
header_written: bool,
fragment_sequence: u32,
fragments: Vec<Vec<u8>>,
}
impl Mp4Muxer {
#[must_use]
pub fn new(config: Mp4Config) -> Self {
Self {
config,
tracks: Vec::new(),
header_written: false,
fragment_sequence: 1,
fragments: Vec::new(),
}
}
pub fn add_stream(&mut self, info: StreamInfo) -> OxiResult<usize> {
if self.header_written {
return Err(OxiError::parse(
0,
"Cannot add streams after header is written",
));
}
if self.tracks.len() >= MAX_TRACKS {
return Err(OxiError::parse(0, "Maximum number of tracks exceeded"));
}
validate_codec(info.codec)?;
let track_id = (self.tracks.len() + 1) as u32;
self.tracks.push(Mp4TrackState::new(info, track_id));
Ok(self.tracks.len() - 1)
}
pub fn write_header(&mut self) -> OxiResult<()> {
if self.tracks.is_empty() {
return Err(OxiError::parse(0, "No streams added"));
}
self.header_written = true;
Ok(())
}
pub fn write_packet(&mut self, packet: &Packet) -> OxiResult<()> {
if !self.header_written {
return Err(OxiError::parse(0, "Header not written yet"));
}
let track_idx = packet.stream_index;
if track_idx >= self.tracks.len() {
return Err(OxiError::parse(
0,
format!("Invalid stream index {track_idx}"),
));
}
let track = &self.tracks[track_idx];
let timescale = track.timescale;
let duration = packet
.duration()
.map(|d| convert_duration(d, &track.stream_info.timebase, timescale))
.unwrap_or(1);
let comp_offset = if let Some(dts) = packet.dts() {
let pts_ticks = convert_duration(packet.pts(), &track.stream_info.timebase, timescale);
let dts_ticks = convert_duration(dts, &track.stream_info.timebase, timescale);
(pts_ticks as i64 - dts_ticks as i64) as i32
} else {
0
};
let is_sync = packet.is_keyframe();
let track = &mut self.tracks[track_idx];
track.add_sample(&packet.data, duration, comp_offset, is_sync);
Ok(())
}
pub fn finalize(&self) -> OxiResult<Vec<u8>> {
if !self.header_written {
return Err(OxiError::parse(0, "Header not written"));
}
let mut output = Vec::new();
output.extend(self.build_ftyp());
match self.config.mode {
Mp4FragmentMode::Progressive => {
let (mdat_box, chunk_offsets) = self.build_mdat_progressive();
let ftyp_size = output.len() as u64;
let moov_estimate = self.build_moov_progressive(&chunk_offsets, ftyp_size);
let moov_size = moov_estimate.len() as u64;
let mdat_start = ftyp_size + moov_size + 8; let adjusted_offsets = adjust_chunk_offsets(&chunk_offsets, mdat_start);
let moov_final = self.build_moov_progressive(&adjusted_offsets, ftyp_size);
output.extend(moov_final);
output.extend(mdat_box);
}
Mp4FragmentMode::Fragmented { .. } => {
let moov = self.build_moov_fragmented();
output.extend(moov);
for frag in &self.fragments {
output.extend(frag);
}
let frags = self.build_fragments_from_tracks();
for frag in frags {
output.extend(frag);
}
}
}
Ok(output)
}
#[must_use]
pub fn track_count(&self) -> usize {
self.tracks.len()
}
#[must_use]
pub fn track(&self, index: usize) -> Option<&Mp4TrackState> {
self.tracks.get(index)
}
#[must_use]
pub fn total_samples(&self) -> usize {
self.tracks.iter().map(|t| t.samples.len()).sum()
}
fn build_ftyp(&self) -> Vec<u8> {
let mut content = Vec::new();
content.extend_from_slice(&self.config.major_brand);
content.extend_from_slice(&write_u32_be(self.config.minor_version));
for brand in &self.config.compatible_brands {
content.extend_from_slice(brand);
}
write_box(b"ftyp", &content)
}
fn build_mdat_progressive(&self) -> (Vec<u8>, Vec<Vec<u64>>) {
let mut mdat_payload = Vec::new();
let mut all_chunk_offsets: Vec<Vec<u64>> = Vec::new();
for track in &self.tracks {
let mut track_offsets = Vec::new();
let mut sample_idx: u32 = 0;
let mut current_offset = mdat_payload.len() as u64;
for (chunk_idx, &chunk_start) in track.chunk_boundaries.iter().enumerate() {
let chunk_end = track
.chunk_boundaries
.get(chunk_idx + 1)
.copied()
.unwrap_or(track.samples.len() as u32);
track_offsets.push(current_offset);
for si in chunk_start..chunk_end {
if let Some(sample) = track.samples.get(si as usize) {
let start = sample_data_offset(track, sample_idx);
let end = start + sample.size as usize;
if end <= track.mdat_data.len() {
mdat_payload.extend_from_slice(&track.mdat_data[start..end]);
current_offset += u64::from(sample.size);
}
}
sample_idx += 1;
}
}
all_chunk_offsets.push(track_offsets);
}
let mdat = write_box(b"mdat", &mdat_payload);
(mdat, all_chunk_offsets)
}
fn build_moov_progressive(&self, chunk_offsets: &[Vec<u64>], _ftyp_size: u64) -> Vec<u8> {
let mut content = Vec::new();
content.extend(self.build_mvhd());
for (i, track) in self.tracks.iter().enumerate() {
let offsets = chunk_offsets.get(i).map_or(&[][..], |v| v.as_slice());
content.extend(self.build_trak(track, offsets));
}
write_box(b"moov", &content)
}
fn build_moov_fragmented(&self) -> Vec<u8> {
let mut content = Vec::new();
content.extend(self.build_mvhd_fragmented());
let mut mvex_content = Vec::new();
for track in &self.tracks {
mvex_content.extend(build_trex(track.track_id));
}
content.extend(write_box(b"mvex", &mvex_content));
for track in &self.tracks {
content.extend(self.build_trak_fragmented(track));
}
write_box(b"moov", &content)
}
fn build_mvhd_fragmented(&self) -> Vec<u8> {
let mut c = Vec::new();
c.extend_from_slice(&write_u32_be(self.config.creation_time as u32));
c.extend_from_slice(&write_u32_be(self.config.modification_time as u32));
c.extend_from_slice(&write_u32_be(MOVIE_TIMESCALE));
c.extend_from_slice(&write_u32_be(0));
c.extend_from_slice(&write_u32_be(0x0001_0000)); c.extend_from_slice(&[0x01, 0x00]); c.extend_from_slice(&[0u8; 10]); c.extend_from_slice(&IDENTITY_MATRIX);
c.extend_from_slice(&[0u8; 24]); c.extend_from_slice(&write_u32_be((self.tracks.len() + 1) as u32)); write_full_box(b"mvhd", 0, 0, &c)
}
fn build_trak_fragmented(&self, track: &Mp4TrackState) -> Vec<u8> {
let mut content = Vec::new();
content.extend(build_tkhd(track));
content.extend(self.build_mdia_fragmented(track));
write_box(b"trak", &content)
}
fn build_mdia_fragmented(&self, track: &Mp4TrackState) -> Vec<u8> {
let mut content = Vec::new();
content.extend(build_mdhd(track));
content.extend(build_hdlr(track));
content.extend(self.build_minf_fragmented(track));
write_box(b"mdia", &content)
}
fn build_minf_fragmented(&self, track: &Mp4TrackState) -> Vec<u8> {
let mut content = Vec::new();
match track.stream_info.media_type {
MediaType::Video => content.extend(build_vmhd()),
MediaType::Audio => content.extend(build_smhd()),
_ => content.extend(write_full_box(b"nmhd", 0, 0, &[])),
}
content.extend(build_dinf());
content.extend(self.build_stbl_fragmented(track));
write_box(b"minf", &content)
}
fn build_stbl_fragmented(&self, track: &Mp4TrackState) -> Vec<u8> {
let mut content = Vec::new();
content.extend(build_stsd(track));
content.extend(build_empty_stts());
content.extend(build_empty_stsc());
content.extend(build_empty_stsz());
content.extend(build_empty_stco());
write_box(b"stbl", &content)
}
fn build_mvhd(&self) -> Vec<u8> {
let mut c = Vec::new();
c.extend_from_slice(&write_u32_be(self.config.creation_time as u32));
c.extend_from_slice(&write_u32_be(self.config.modification_time as u32));
c.extend_from_slice(&write_u32_be(MOVIE_TIMESCALE));
let max_dur = self
.tracks
.iter()
.map(|t| {
if t.timescale == 0 {
0
} else {
t.total_duration * u64::from(MOVIE_TIMESCALE) / u64::from(t.timescale)
}
})
.max()
.unwrap_or(0);
c.extend_from_slice(&write_u32_be(max_dur as u32));
c.extend_from_slice(&write_u32_be(0x0001_0000));
c.extend_from_slice(&[0x01, 0x00]);
c.extend_from_slice(&[0u8; 10]);
c.extend_from_slice(&IDENTITY_MATRIX);
c.extend_from_slice(&[0u8; 24]);
c.extend_from_slice(&write_u32_be((self.tracks.len() + 1) as u32));
write_full_box(b"mvhd", 0, 0, &c)
}
fn build_trak(&self, track: &Mp4TrackState, chunk_offsets: &[u64]) -> Vec<u8> {
let mut content = Vec::new();
content.extend(build_tkhd(track));
content.extend(self.build_mdia(track, chunk_offsets));
write_box(b"trak", &content)
}
fn build_mdia(&self, track: &Mp4TrackState, chunk_offsets: &[u64]) -> Vec<u8> {
let mut content = Vec::new();
content.extend(build_mdhd(track));
content.extend(build_hdlr(track));
content.extend(self.build_minf(track, chunk_offsets));
write_box(b"mdia", &content)
}
fn build_minf(&self, track: &Mp4TrackState, chunk_offsets: &[u64]) -> Vec<u8> {
let mut content = Vec::new();
match track.stream_info.media_type {
MediaType::Video => {
content.extend(build_vmhd());
}
MediaType::Audio => {
content.extend(build_smhd());
}
_ => {
content.extend(write_full_box(b"nmhd", 0, 0, &[]));
}
}
content.extend(build_dinf());
content.extend(self.build_stbl(track, chunk_offsets));
write_box(b"minf", &content)
}
fn build_stbl(&self, track: &Mp4TrackState, chunk_offsets: &[u64]) -> Vec<u8> {
let mut content = Vec::new();
content.extend(build_stsd(track));
content.extend(build_stts(track));
if track.samples.iter().any(|s| s.composition_offset != 0) {
content.extend(build_ctts(track));
}
content.extend(build_stsc(track));
content.extend(build_stsz(track));
content.extend(build_stco(chunk_offsets));
if track.stream_info.media_type == MediaType::Video
&& track.samples.iter().any(|s| !s.is_sync)
{
content.extend(build_stss(track));
}
write_box(b"stbl", &content)
}
fn build_fragments_from_tracks(&self) -> Vec<Vec<u8>> {
let ref_track = match self.tracks.iter().find(|t| !t.samples.is_empty()) {
Some(t) => t,
None => return Vec::new(),
};
let config_frag_ms = match self.config.mode {
Mp4FragmentMode::Fragmented {
fragment_duration_ms,
} => fragment_duration_ms,
Mp4FragmentMode::Progressive => 0,
};
let frag_duration_ticks = if ref_track.timescale > 0 && config_frag_ms > 0 {
u64::from(config_frag_ms) * u64::from(ref_track.timescale) / 1000
} else {
0
};
let boundaries = compute_fragment_boundaries(ref_track, frag_duration_ticks);
let mut result = Vec::new();
let mut seq = self.fragment_sequence;
for &(frag_start, frag_end) in &boundaries {
let fragment = self.build_one_fragment(seq, frag_start, frag_end);
result.push(fragment);
seq += 1;
}
result
}
fn build_one_fragment(&self, seq: u32, frag_start: usize, frag_end: usize) -> Vec<u8> {
let ref_track = match self.tracks.first() {
Some(t) => t,
None => return Vec::new(),
};
let frag_base_dts = dts_at(ref_track, frag_start);
let frag_end_dts = dts_at(ref_track, frag_end);
let mut mdat_payload: Vec<u8> = Vec::new();
let mut track_runs: Vec<FragTrackRun> = Vec::new();
for track in &self.tracks {
if track.samples.is_empty() {
continue;
}
let (slice_start, slice_end) = if track.track_id == ref_track.track_id {
(frag_start, frag_end)
} else {
select_time_range(track, frag_base_dts, frag_end_dts)
};
if slice_start >= slice_end {
continue;
}
let base_dts = dts_at(track, slice_start);
let mdat_offset_start = mdat_payload.len() as u32;
let mut entries: Vec<FragSampleEntry> = Vec::new();
let mut byte_offset = 0usize;
for s_idx in 0..slice_start {
byte_offset += track.samples.get(s_idx).map_or(0, |s| s.size as usize);
}
for s_idx in slice_start..slice_end {
let s = match track.samples.get(s_idx) {
Some(s) => s,
None => break,
};
let end = byte_offset + s.size as usize;
if end <= track.mdat_data.len() {
mdat_payload.extend_from_slice(&track.mdat_data[byte_offset..end]);
}
byte_offset = end;
let flags: u32 = if s.is_sync { 0x0200_0000 } else { 0x0101_0000 };
#[allow(clippy::cast_possible_wrap)]
let pts_offset = s.composition_offset;
entries.push(FragSampleEntry {
duration: s.duration,
size: s.size,
flags,
pts_offset,
});
}
track_runs.push(FragTrackRun {
track_id: track.track_id,
base_dts,
mdat_offset_start,
entries,
});
}
let build_moof = |data_offset_base: i32, runs: &[FragTrackRun]| -> Vec<u8> {
let mut moof_content = Vec::new();
moof_content.extend(build_mfhd(seq));
for tr in runs {
moof_content.extend(build_traf(
tr.track_id,
tr.base_dts,
data_offset_base + tr.mdat_offset_start as i32,
&tr.entries,
));
}
write_box(b"moof", &moof_content)
};
let moof_placeholder = build_moof(0, &track_runs);
let moof_size = moof_placeholder.len() as i32;
let moof = build_moof(moof_size + 8, &track_runs);
let mdat = write_box(b"mdat", &mdat_payload);
let subseg_dur = track_runs
.iter()
.find(|tr| tr.track_id == ref_track.track_id)
.map(|tr| {
tr.entries
.iter()
.map(|e| u64::from(e.duration))
.sum::<u64>()
})
.unwrap_or(0);
let is_sap = track_runs
.iter()
.find(|tr| tr.track_id == ref_track.track_id)
.and_then(|tr| tr.entries.first())
.is_some_and(|e| e.flags == 0x0200_0000);
let referenced_size = (moof.len() + mdat.len()) as u32;
let sidx = build_sidx(
ref_track.track_id,
ref_track.timescale,
frag_base_dts,
referenced_size,
subseg_dur as u32,
is_sap,
);
let mut out = sidx;
out.extend(moof);
out.extend(mdat);
out
}
}
const IDENTITY_MATRIX: [u8; 36] = [
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, ];
fn build_tkhd(track: &Mp4TrackState) -> Vec<u8> {
let mut c = Vec::new();
c.extend_from_slice(&write_u32_be(0));
c.extend_from_slice(&write_u32_be(0));
c.extend_from_slice(&write_u32_be(track.track_id));
c.extend_from_slice(&write_u32_be(0));
let dur_ms = if track.timescale == 0 {
0
} else {
track.total_duration * u64::from(MOVIE_TIMESCALE) / u64::from(track.timescale)
};
c.extend_from_slice(&write_u32_be(dur_ms as u32));
c.extend_from_slice(&[0u8; 8]);
c.extend_from_slice(&[0u8; 2]);
c.extend_from_slice(&[0u8; 2]);
if track.stream_info.media_type == MediaType::Audio {
c.extend_from_slice(&[0x01, 0x00]);
} else {
c.extend_from_slice(&[0x00, 0x00]);
}
c.extend_from_slice(&[0u8; 2]);
c.extend_from_slice(&IDENTITY_MATRIX);
let w = track.stream_info.codec_params.width.unwrap_or(0);
c.extend_from_slice(&write_u32_be(w << 16));
let h = track.stream_info.codec_params.height.unwrap_or(0);
c.extend_from_slice(&write_u32_be(h << 16));
write_full_box(b"tkhd", 0, 3, &c)
}
fn build_mdhd(track: &Mp4TrackState) -> Vec<u8> {
let mut c = Vec::new();
c.extend_from_slice(&write_u32_be(0));
c.extend_from_slice(&write_u32_be(0));
c.extend_from_slice(&write_u32_be(track.timescale));
c.extend_from_slice(&write_u32_be(track.total_duration as u32));
c.extend_from_slice(&[0x55, 0xC4]);
c.extend_from_slice(&[0u8; 2]);
write_full_box(b"mdhd", 0, 0, &c)
}
fn build_hdlr(track: &Mp4TrackState) -> Vec<u8> {
let mut c = Vec::new();
c.extend_from_slice(&write_u32_be(0));
c.extend_from_slice(&track.handler_type);
c.extend_from_slice(&[0u8; 12]);
c.extend_from_slice(track.handler_name.as_bytes());
c.push(0);
write_full_box(b"hdlr", 0, 0, &c)
}
fn build_vmhd() -> Vec<u8> {
let mut c = Vec::new();
c.extend_from_slice(&[0u8; 2]);
c.extend_from_slice(&[0u8; 6]);
write_full_box(b"vmhd", 0, 1, &c)
}
fn build_smhd() -> Vec<u8> {
let mut c = Vec::new();
c.extend_from_slice(&[0u8; 2]);
c.extend_from_slice(&[0u8; 2]);
write_full_box(b"smhd", 0, 0, &c)
}
fn build_dinf() -> Vec<u8> {
let url_box = write_full_box(b"url ", 0, 1, &[]); let mut dref_content = Vec::new();
dref_content.extend_from_slice(&write_u32_be(1)); dref_content.extend(url_box);
let dref = write_full_box(b"dref", 0, 0, &dref_content);
write_box(b"dinf", &dref)
}
fn build_stsd(track: &Mp4TrackState) -> Vec<u8> {
let mut c = Vec::new();
c.extend_from_slice(&write_u32_be(1));
match track.stream_info.media_type {
MediaType::Video => {
c.extend(build_video_sample_entry(track));
}
MediaType::Audio => {
c.extend(build_audio_sample_entry(track));
}
_ => {
c.extend(build_generic_sample_entry(track));
}
}
write_full_box(b"stsd", 0, 0, &c)
}
fn build_video_sample_entry(track: &Mp4TrackState) -> Vec<u8> {
let fourcc = codec_to_fourcc(track.stream_info.codec);
let mut c = Vec::new();
c.extend_from_slice(&[0u8; 6]);
c.extend_from_slice(&[0x00, 0x01]);
c.extend_from_slice(&[0u8; 16]);
let w = track.stream_info.codec_params.width.unwrap_or(0) as u16;
c.extend_from_slice(&w.to_be_bytes());
let h = track.stream_info.codec_params.height.unwrap_or(0) as u16;
c.extend_from_slice(&h.to_be_bytes());
c.extend_from_slice(&write_u32_be(0x0048_0000));
c.extend_from_slice(&write_u32_be(0x0048_0000));
c.extend_from_slice(&write_u32_be(0));
c.extend_from_slice(&[0x00, 0x01]);
let mut comp_name = [0u8; 32];
let name = b"OxiMedia";
let len = name.len().min(31);
comp_name[0] = len as u8;
comp_name[1..1 + len].copy_from_slice(&name[..len]);
c.extend_from_slice(&comp_name);
c.extend_from_slice(&[0x00, 0x18]); c.extend_from_slice(&[0xFF, 0xFF]);
match track.stream_info.codec {
CodecId::Av1 => {
let av1c_payload = track
.stream_info
.codec_params
.extradata
.as_deref()
.and_then(|data| build_av1c_from_extradata(data))
.unwrap_or_else(|| {
vec![0x81u8, 0x00, 0x04, 0x00]
});
c.extend(write_box(b"av1C", &av1c_payload));
}
_ => {
if let Some(extradata) = &track.stream_info.codec_params.extradata {
let config_fourcc = codec_config_fourcc(track.stream_info.codec);
c.extend(write_box(&config_fourcc, extradata));
}
}
}
write_box(&fourcc, &c)
}
fn build_audio_sample_entry(track: &Mp4TrackState) -> Vec<u8> {
let fourcc = codec_to_fourcc(track.stream_info.codec);
let mut c = Vec::new();
c.extend_from_slice(&[0u8; 6]);
c.extend_from_slice(&[0x00, 0x01]);
c.extend_from_slice(&[0u8; 8]);
let channels = track.stream_info.codec_params.channels.unwrap_or(2) as u16;
c.extend_from_slice(&channels.to_be_bytes());
c.extend_from_slice(&[0x00, 0x10]);
c.extend_from_slice(&[0u8; 4]);
let sr = track.stream_info.codec_params.sample_rate.unwrap_or(48000);
c.extend_from_slice(&write_u32_be(sr << 16));
if let Some(extradata) = &track.stream_info.codec_params.extradata {
let config_fourcc = codec_config_fourcc(track.stream_info.codec);
c.extend(write_box(&config_fourcc, extradata));
}
write_box(&fourcc, &c)
}
fn build_generic_sample_entry(track: &Mp4TrackState) -> Vec<u8> {
let fourcc = codec_to_fourcc(track.stream_info.codec);
let mut c = Vec::new();
c.extend_from_slice(&[0u8; 6]); c.extend_from_slice(&[0x00, 0x01]); write_box(&fourcc, &c)
}
fn build_stts(track: &Mp4TrackState) -> Vec<u8> {
let mut entries: Vec<(u32, u32)> = Vec::new();
for sample in &track.samples {
if let Some(last) = entries.last_mut() {
if last.1 == sample.duration {
last.0 += 1;
continue;
}
}
entries.push((1, sample.duration));
}
let mut c = Vec::new();
c.extend_from_slice(&write_u32_be(entries.len() as u32));
for (count, delta) in &entries {
c.extend_from_slice(&write_u32_be(*count));
c.extend_from_slice(&write_u32_be(*delta));
}
write_full_box(b"stts", 0, 0, &c)
}
fn build_ctts(track: &Mp4TrackState) -> Vec<u8> {
let mut entries: Vec<(u32, i32)> = Vec::new();
for sample in &track.samples {
if let Some(last) = entries.last_mut() {
if last.1 == sample.composition_offset {
last.0 += 1;
continue;
}
}
entries.push((1, sample.composition_offset));
}
let mut c = Vec::new();
c.extend_from_slice(&write_u32_be(entries.len() as u32));
for (count, offset) in &entries {
c.extend_from_slice(&write_u32_be(*count));
c.extend_from_slice(&((*offset) as u32).to_be_bytes());
}
write_full_box(b"ctts", 1, 0, &c)
}
fn build_stsc(track: &Mp4TrackState) -> Vec<u8> {
let mut entries: Vec<(u32, u32, u32)> = Vec::new();
for (chunk_idx, &chunk_start) in track.chunk_boundaries.iter().enumerate() {
let chunk_end = track
.chunk_boundaries
.get(chunk_idx + 1)
.copied()
.unwrap_or(track.samples.len() as u32);
let samples_in_chunk = chunk_end.saturating_sub(chunk_start);
if samples_in_chunk == 0 {
continue;
}
let first_chunk = (chunk_idx + 1) as u32; if let Some(last) = entries.last() {
if last.1 == samples_in_chunk {
continue; }
}
entries.push((first_chunk, samples_in_chunk, 1));
}
if entries.is_empty() {
entries.push((1, 1, 1)); }
let mut c = Vec::new();
c.extend_from_slice(&write_u32_be(entries.len() as u32));
for (first_chunk, spc, sdi) in &entries {
c.extend_from_slice(&write_u32_be(*first_chunk));
c.extend_from_slice(&write_u32_be(*spc));
c.extend_from_slice(&write_u32_be(*sdi));
}
write_full_box(b"stsc", 0, 0, &c)
}
fn build_stsz(track: &Mp4TrackState) -> Vec<u8> {
let mut c = Vec::new();
let first_size = track.samples.first().map_or(0, |s| s.size);
let all_same = track.samples.iter().all(|s| s.size == first_size);
if all_same && !track.samples.is_empty() {
c.extend_from_slice(&write_u32_be(first_size));
c.extend_from_slice(&write_u32_be(track.samples.len() as u32));
} else {
c.extend_from_slice(&write_u32_be(0));
c.extend_from_slice(&write_u32_be(track.samples.len() as u32));
for sample in &track.samples {
c.extend_from_slice(&write_u32_be(sample.size));
}
}
write_full_box(b"stsz", 0, 0, &c)
}
fn build_stco(chunk_offsets: &[u64]) -> Vec<u8> {
let use_64bit = chunk_offsets.iter().any(|&o| o > u64::from(u32::MAX));
if use_64bit {
let mut c = Vec::new();
c.extend_from_slice(&write_u32_be(chunk_offsets.len() as u32));
for &offset in chunk_offsets {
c.extend_from_slice(&write_u64_be(offset));
}
write_full_box(b"co64", 0, 0, &c)
} else {
let mut c = Vec::new();
c.extend_from_slice(&write_u32_be(chunk_offsets.len() as u32));
for &offset in chunk_offsets {
c.extend_from_slice(&write_u32_be(offset as u32));
}
write_full_box(b"stco", 0, 0, &c)
}
}
fn build_stss(track: &Mp4TrackState) -> Vec<u8> {
let sync_samples: Vec<u32> = track
.samples
.iter()
.enumerate()
.filter(|(_, s)| s.is_sync)
.map(|(i, _)| (i + 1) as u32) .collect();
let mut c = Vec::new();
c.extend_from_slice(&write_u32_be(sync_samples.len() as u32));
for &idx in &sync_samples {
c.extend_from_slice(&write_u32_be(idx));
}
write_full_box(b"stss", 0, 0, &c)
}
fn build_trex(track_id: u32) -> Vec<u8> {
let mut c = Vec::new();
c.extend_from_slice(&write_u32_be(track_id));
c.extend_from_slice(&write_u32_be(1)); c.extend_from_slice(&write_u32_be(0)); c.extend_from_slice(&write_u32_be(0)); c.extend_from_slice(&write_u32_be(0)); write_full_box(b"trex", 0, 0, &c)
}
fn validate_codec(codec: CodecId) -> OxiResult<()> {
match codec {
CodecId::Av1
| CodecId::Vp9
| CodecId::Vp8
| CodecId::Opus
| CodecId::Flac
| CodecId::Vorbis
| CodecId::Apv | CodecId::Mjpeg => Ok(()),
_ => Err(OxiError::PatentViolation(format!(
"Codec {:?} is not supported in MP4 muxer (patent-free codecs only)",
codec
))),
}
}
fn codec_to_fourcc(codec: CodecId) -> [u8; 4] {
match codec {
CodecId::Av1 => *b"av01",
CodecId::Vp9 => *b"vp09",
CodecId::Vp8 => *b"vp08",
CodecId::Opus => *b"Opus",
CodecId::Flac => *b"fLaC",
CodecId::Vorbis => *b"vorb",
CodecId::Apv => *b"apv1", CodecId::Mjpeg => *b"jpeg", _ => *b"unkn",
}
}
fn codec_config_fourcc(codec: CodecId) -> [u8; 4] {
match codec {
CodecId::Av1 => *b"av1C",
CodecId::Vp9 => *b"vpcC",
CodecId::Vp8 => *b"vpcC",
CodecId::Opus => *b"dOps",
CodecId::Flac => *b"dfLa",
_ => *b"conf",
}
}
#[allow(clippy::cast_precision_loss)]
fn convert_duration(ticks: i64, timebase: &Rational, target_timescale: u32) -> u32 {
if timebase.den == 0 || target_timescale == 0 {
return 1;
}
let seconds = (ticks as f64 * timebase.num as f64) / timebase.den as f64;
let result = seconds * target_timescale as f64;
if result <= 0.0 {
1
} else {
result as u32
}
}
fn sample_data_offset(track: &Mp4TrackState, sample_idx: u32) -> usize {
let mut offset = 0usize;
for i in 0..sample_idx as usize {
if let Some(s) = track.samples.get(i) {
offset += s.size as usize;
}
}
offset
}
fn adjust_chunk_offsets(offsets: &[Vec<u64>], mdat_data_start: u64) -> Vec<Vec<u64>> {
offsets
.iter()
.map(|track_offsets| track_offsets.iter().map(|&o| o + mdat_data_start).collect())
.collect()
}
fn compute_fragment_boundaries(
track: &Mp4TrackState,
frag_duration_ticks: u64,
) -> Vec<(usize, usize)> {
if track.samples.is_empty() {
return Vec::new();
}
let mut boundaries: Vec<(usize, usize)> = Vec::new();
let mut frag_start = 0usize;
let mut accumulated: u64 = 0;
for (i, sample) in track.samples.iter().enumerate() {
if i > frag_start && sample.is_sync {
if frag_duration_ticks == 0 || accumulated >= frag_duration_ticks {
boundaries.push((frag_start, i));
frag_start = i;
accumulated = 0;
}
}
accumulated += u64::from(sample.duration);
}
boundaries.push((frag_start, track.samples.len()));
boundaries
}
fn dts_at(track: &Mp4TrackState, idx: usize) -> u64 {
track
.samples
.iter()
.take(idx)
.map(|s| u64::from(s.duration))
.sum()
}
fn select_time_range(track: &Mp4TrackState, base_dts: u64, end_dts: u64) -> (usize, usize) {
let mut cursor: u64 = 0;
let mut start: Option<usize> = None;
let mut end = 0usize;
for (i, s) in track.samples.iter().enumerate() {
let sample_dts = cursor;
cursor += u64::from(s.duration);
if sample_dts >= end_dts {
break;
}
if sample_dts >= base_dts {
if start.is_none() {
start = Some(i);
}
end = i + 1;
}
}
match start {
Some(s) => (s, end),
None => (0, 0),
}
}
fn build_sidx(
reference_id: u32,
timescale: u32,
earliest_pts: u64,
referenced_size: u32,
subsegment_duration: u32,
is_sap: bool,
) -> Vec<u8> {
let mut c = Vec::new();
c.extend_from_slice(&write_u32_be(reference_id));
c.extend_from_slice(&write_u32_be(timescale));
c.extend_from_slice(&write_u64_be(earliest_pts));
c.extend_from_slice(&write_u64_be(0)); c.extend_from_slice(&[0u8; 2]); c.extend_from_slice(&1u16.to_be_bytes());
let ref_type_size: u32 = referenced_size & 0x7FFF_FFFF; c.extend_from_slice(&write_u32_be(ref_type_size));
c.extend_from_slice(&write_u32_be(subsegment_duration));
let sap_word: u32 = if is_sap { 0x9000_0000u32 } else { 0 };
c.extend_from_slice(&write_u32_be(sap_word));
write_full_box(b"sidx", 1, 0, &c)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::PacketFlags;
use bytes::Bytes;
use oximedia_core::{CodecId, Rational, Timestamp};
fn make_video_stream() -> StreamInfo {
let mut info = StreamInfo::new(0, CodecId::Av1, Rational::new(1, 90000));
info.codec_params = crate::CodecParams::video(1920, 1080);
info
}
fn make_audio_stream() -> StreamInfo {
let mut info = StreamInfo::new(1, CodecId::Opus, Rational::new(1, 48000));
info.codec_params = crate::CodecParams::audio(48000, 2);
info
}
fn make_video_packet(stream_index: usize, pts: i64, keyframe: bool) -> Packet {
let mut ts = Timestamp::new(pts, Rational::new(1, 90000));
ts.duration = Some(3000); Packet::new(
stream_index,
Bytes::from(vec![0xAA; 100]),
ts,
if keyframe {
PacketFlags::KEYFRAME
} else {
PacketFlags::empty()
},
)
}
fn make_audio_packet(stream_index: usize, pts: i64) -> Packet {
let mut ts = Timestamp::new(pts, Rational::new(1, 48000));
ts.duration = Some(960); Packet::new(
stream_index,
Bytes::from(vec![0xBB; 50]),
ts,
PacketFlags::KEYFRAME,
)
}
#[test]
fn test_mp4_config_default() {
let config = Mp4Config::new();
assert_eq!(config.mode, Mp4FragmentMode::Progressive);
assert_eq!(config.major_brand, *b"isom");
}
#[test]
fn test_mp4_config_builder() {
let config = Mp4Config::new()
.with_fragmented(4000)
.with_major_brand(*b"av01")
.with_minor_version(0x100)
.with_compatible_brand(*b"dash");
assert_eq!(
config.mode,
Mp4FragmentMode::Fragmented {
fragment_duration_ms: 4000
}
);
assert_eq!(config.major_brand, *b"av01");
assert_eq!(config.minor_version, 0x100);
assert!(config.compatible_brands.contains(b"dash"));
}
#[test]
fn test_add_stream_returns_index() {
let mut muxer = Mp4Muxer::new(Mp4Config::new());
let idx0 = muxer
.add_stream(make_video_stream())
.expect("should succeed");
let idx1 = muxer
.add_stream(make_audio_stream())
.expect("should succeed");
assert_eq!(idx0, 0);
assert_eq!(idx1, 1);
assert_eq!(muxer.track_count(), 2);
}
#[test]
fn test_add_stream_after_header_fails() {
let mut muxer = Mp4Muxer::new(Mp4Config::new());
muxer
.add_stream(make_video_stream())
.expect("should succeed");
muxer.write_header().expect("should succeed");
let result = muxer.add_stream(make_audio_stream());
assert!(result.is_err());
}
#[test]
fn test_write_header_no_streams_fails() {
let mut muxer = Mp4Muxer::new(Mp4Config::new());
let result = muxer.write_header();
assert!(result.is_err());
}
#[test]
fn test_write_packet_before_header_fails() {
let mut muxer = Mp4Muxer::new(Mp4Config::new());
muxer
.add_stream(make_video_stream())
.expect("should succeed");
let pkt = make_video_packet(0, 0, true);
let result = muxer.write_packet(&pkt);
assert!(result.is_err());
}
#[test]
fn test_write_packet_invalid_index_fails() {
let mut muxer = Mp4Muxer::new(Mp4Config::new());
muxer
.add_stream(make_video_stream())
.expect("should succeed");
muxer.write_header().expect("should succeed");
let pkt = make_video_packet(99, 0, true);
let result = muxer.write_packet(&pkt);
assert!(result.is_err());
}
#[test]
fn test_patent_encumbered_codec_rejected() {
let mut muxer = Mp4Muxer::new(Mp4Config::new());
let info = StreamInfo::new(0, CodecId::Pcm, Rational::new(1, 44100));
let result = muxer.add_stream(info);
assert!(result.is_err());
}
#[test]
fn test_progressive_ftyp_present() {
let mut muxer = Mp4Muxer::new(Mp4Config::new());
muxer
.add_stream(make_video_stream())
.expect("should succeed");
muxer.write_header().expect("should succeed");
let pkt = make_video_packet(0, 0, true);
muxer.write_packet(&pkt).expect("should succeed");
let output = muxer.finalize().expect("should succeed");
assert_eq!(&output[4..8], b"ftyp");
assert_eq!(&output[8..12], b"isom");
}
#[test]
fn test_progressive_contains_moov_and_mdat() {
let mut muxer = Mp4Muxer::new(Mp4Config::new());
muxer
.add_stream(make_video_stream())
.expect("should succeed");
muxer.write_header().expect("should succeed");
muxer
.write_packet(&make_video_packet(0, 0, true))
.expect("should succeed");
muxer
.write_packet(&make_video_packet(0, 3000, false))
.expect("should succeed");
let output = muxer.finalize().expect("should succeed");
let has_moov = output.windows(4).any(|w| w == b"moov");
let has_mdat = output.windows(4).any(|w| w == b"mdat");
assert!(has_moov, "output must contain moov box");
assert!(has_mdat, "output must contain mdat box");
}
#[test]
fn test_progressive_contains_trak_boxes() {
let mut muxer = Mp4Muxer::new(Mp4Config::new());
muxer
.add_stream(make_video_stream())
.expect("should succeed");
muxer
.add_stream(make_audio_stream())
.expect("should succeed");
muxer.write_header().expect("should succeed");
muxer
.write_packet(&make_video_packet(0, 0, true))
.expect("should succeed");
muxer
.write_packet(&make_audio_packet(1, 0))
.expect("should succeed");
let output = muxer.finalize().expect("should succeed");
let trak_count = output.windows(4).filter(|w| *w == b"trak").count();
assert_eq!(trak_count, 2, "output must contain 2 trak boxes");
}
#[test]
fn test_progressive_contains_stbl_tables() {
let mut muxer = Mp4Muxer::new(Mp4Config::new());
muxer
.add_stream(make_video_stream())
.expect("should succeed");
muxer.write_header().expect("should succeed");
for i in 0..5 {
muxer
.write_packet(&make_video_packet(0, i * 3000, i == 0))
.expect("should succeed");
}
let output = muxer.finalize().expect("should succeed");
let has_stsd = output.windows(4).any(|w| w == b"stsd");
let has_stts = output.windows(4).any(|w| w == b"stts");
let has_stsz = output.windows(4).any(|w| w == b"stsz");
let has_stco = output.windows(4).any(|w| w == b"stco");
let has_stss = output.windows(4).any(|w| w == b"stss");
assert!(has_stsd, "must have stsd");
assert!(has_stts, "must have stts");
assert!(has_stsz, "must have stsz");
assert!(has_stco, "must have stco");
assert!(has_stss, "must have stss (video with non-sync frames)");
}
#[test]
fn test_progressive_mdat_contains_sample_data() {
let mut muxer = Mp4Muxer::new(Mp4Config::new());
muxer
.add_stream(make_video_stream())
.expect("should succeed");
muxer.write_header().expect("should succeed");
muxer
.write_packet(&make_video_packet(0, 0, true))
.expect("should succeed");
let output = muxer.finalize().expect("should succeed");
let mdat_pos = output
.windows(4)
.position(|w| w == b"mdat")
.expect("mdat must exist");
let mdat_start = mdat_pos + 4;
assert!(
output[mdat_start..].windows(4).any(|w| w == [0xAA; 4]),
"mdat must contain sample payload"
);
}
#[test]
fn test_total_samples_count() {
let mut muxer = Mp4Muxer::new(Mp4Config::new());
muxer
.add_stream(make_video_stream())
.expect("should succeed");
muxer
.add_stream(make_audio_stream())
.expect("should succeed");
muxer.write_header().expect("should succeed");
muxer
.write_packet(&make_video_packet(0, 0, true))
.expect("should succeed");
muxer
.write_packet(&make_video_packet(0, 3000, false))
.expect("should succeed");
muxer
.write_packet(&make_audio_packet(1, 0))
.expect("should succeed");
assert_eq!(muxer.total_samples(), 3);
}
#[test]
fn test_fragmented_contains_mvex() {
let config = Mp4Config::new().with_fragmented(2000);
let mut muxer = Mp4Muxer::new(config);
muxer
.add_stream(make_video_stream())
.expect("should succeed");
muxer.write_header().expect("should succeed");
muxer
.write_packet(&make_video_packet(0, 0, true))
.expect("should succeed");
let output = muxer.finalize().expect("should succeed");
let has_mvex = output.windows(4).any(|w| w == b"mvex");
let has_trex = output.windows(4).any(|w| w == b"trex");
assert!(has_mvex, "fragmented must contain mvex");
assert!(has_trex, "fragmented must contain trex");
}
#[test]
fn test_fragmented_contains_moof_mdat() {
let config = Mp4Config::new().with_fragmented(2000);
let mut muxer = Mp4Muxer::new(config);
muxer
.add_stream(make_video_stream())
.expect("should succeed");
muxer.write_header().expect("should succeed");
muxer
.write_packet(&make_video_packet(0, 0, true))
.expect("should succeed");
let output = muxer.finalize().expect("should succeed");
let has_moof = output.windows(4).any(|w| w == b"moof");
let has_mdat = output.windows(4).any(|w| w == b"mdat");
assert!(has_moof, "fragmented must contain moof");
assert!(has_mdat, "fragmented must contain mdat");
}
#[test]
fn test_fragmented_contains_traf_trun() {
let config = Mp4Config::new().with_fragmented(2000);
let mut muxer = Mp4Muxer::new(config);
muxer
.add_stream(make_video_stream())
.expect("should succeed");
muxer.write_header().expect("should succeed");
muxer
.write_packet(&make_video_packet(0, 0, true))
.expect("should succeed");
let output = muxer.finalize().expect("should succeed");
let has_traf = output.windows(4).any(|w| w == b"traf");
let has_trun = output.windows(4).any(|w| w == b"trun");
assert!(has_traf, "fragmented must contain traf");
assert!(has_trun, "fragmented must contain trun");
}
#[test]
fn test_validate_codec_accepted() {
assert!(validate_codec(CodecId::Av1).is_ok());
assert!(validate_codec(CodecId::Vp9).is_ok());
assert!(validate_codec(CodecId::Opus).is_ok());
assert!(validate_codec(CodecId::Flac).is_ok());
}
#[test]
fn test_validate_codec_rejected() {
assert!(validate_codec(CodecId::Pcm).is_err());
}
#[test]
fn test_codec_to_fourcc() {
assert_eq!(codec_to_fourcc(CodecId::Av1), *b"av01");
assert_eq!(codec_to_fourcc(CodecId::Vp9), *b"vp09");
assert_eq!(codec_to_fourcc(CodecId::Opus), *b"Opus");
}
#[test]
fn test_convert_duration_basic() {
let tb = Rational::new(1, 90000);
let result = convert_duration(90000, &tb, 1000);
assert_eq!(result, 1000); }
#[test]
fn test_convert_duration_zero_target() {
let tb = Rational::new(1, 90000);
let result = convert_duration(100, &tb, 0);
assert_eq!(result, 1); }
#[test]
fn test_sample_data_offset() {
let mut track = Mp4TrackState::new(make_video_stream(), 1);
track.add_sample(&[0xAA; 100], 3000, 0, true);
track.add_sample(&[0xBB; 200], 3000, 0, false);
track.add_sample(&[0xCC; 150], 3000, 0, false);
assert_eq!(sample_data_offset(&track, 0), 0);
assert_eq!(sample_data_offset(&track, 1), 100);
assert_eq!(sample_data_offset(&track, 2), 300);
}
#[test]
fn test_adjust_chunk_offsets() {
let offsets = vec![vec![0, 100, 200], vec![0, 50]];
let adjusted = adjust_chunk_offsets(&offsets, 1000);
assert_eq!(adjusted[0], vec![1000, 1100, 1200]);
assert_eq!(adjusted[1], vec![1000, 1050]);
}
#[test]
fn test_build_stts_run_length() {
let mut track = Mp4TrackState::new(make_video_stream(), 1);
for _ in 0..5 {
track.add_sample(&[0u8; 10], 3000, 0, true);
}
let stts = build_stts(&track);
assert_eq!(stts.len(), 24);
}
#[test]
fn test_build_stsz_uniform() {
let mut track = Mp4TrackState::new(make_video_stream(), 1);
for _ in 0..3 {
track.add_sample(&[0u8; 42], 3000, 0, true);
}
let stsz = build_stsz(&track);
assert_eq!(stsz.len(), 20);
}
#[test]
fn test_build_stsz_variable() {
let mut track = Mp4TrackState::new(make_video_stream(), 1);
track.add_sample(&[0u8; 100], 3000, 0, true);
track.add_sample(&[0u8; 200], 3000, 0, false);
let stsz = build_stsz(&track);
assert_eq!(stsz.len(), 28);
}
#[test]
fn test_build_stss_sync_samples() {
let mut track = Mp4TrackState::new(make_video_stream(), 1);
track.add_sample(&[0u8; 10], 3000, 0, true);
track.add_sample(&[0u8; 10], 3000, 0, false);
track.add_sample(&[0u8; 10], 3000, 0, false);
track.add_sample(&[0u8; 10], 3000, 0, true);
let stss = build_stss(&track);
assert_eq!(stss.len(), 24);
}
#[test]
fn test_track_state_handler_types() {
let video_track = Mp4TrackState::new(make_video_stream(), 1);
assert_eq!(video_track.handler_type, *b"vide");
let audio_track = Mp4TrackState::new(make_audio_stream(), 2);
assert_eq!(audio_track.handler_type, *b"soun");
}
#[test]
fn test_progressive_multi_packet_output_valid() {
let mut muxer = Mp4Muxer::new(Mp4Config::new());
muxer
.add_stream(make_video_stream())
.expect("should succeed");
muxer
.add_stream(make_audio_stream())
.expect("should succeed");
muxer.write_header().expect("should succeed");
for i in 0..10 {
muxer
.write_packet(&make_video_packet(0, i * 3000, i == 0 || i == 5))
.expect("should succeed");
muxer
.write_packet(&make_audio_packet(1, i * 960))
.expect("should succeed");
}
let output = muxer.finalize().expect("should succeed");
assert!(!output.is_empty());
assert_eq!(&output[4..8], b"ftyp");
let has_mvhd = output.windows(4).any(|w| w == b"mvhd");
let has_tkhd = output.windows(4).any(|w| w == b"tkhd");
let has_mdhd = output.windows(4).any(|w| w == b"mdhd");
let has_hdlr = output.windows(4).any(|w| w == b"hdlr");
assert!(has_mvhd);
assert!(has_tkhd);
assert!(has_mdhd);
assert!(has_hdlr);
}
#[test]
fn test_finalize_before_header_fails() {
let muxer = Mp4Muxer::new(Mp4Config::new());
assert!(muxer.finalize().is_err());
}
#[test]
fn test_empty_tracks_finalize() {
let mut muxer = Mp4Muxer::new(Mp4Config::new());
muxer
.add_stream(make_video_stream())
.expect("should succeed");
muxer.write_header().expect("should succeed");
let output = muxer.finalize().expect("should succeed");
assert!(!output.is_empty());
assert_eq!(&output[4..8], b"ftyp");
}
}