use std::io::{Cursor, Seek, SeekFrom, Write};
use oxideav_core::{Error, MediaType, Packet, Result, StreamInfo};
use oxideav_core::{Muxer, WriteSeek};
use crate::options::{BrandPreset, FragmentedOptions, Mp4MuxerOptions, TrackSampleGroups};
use crate::sample_entries::{
sample_entry_for, subtitle_handler_for, subtitle_uses_sthd, SampleEntry,
};
use crate::sample_groups::{build_sbgp, build_sgpd, SampleGroupDescription, SampleToGroup};
pub(crate) struct TrackState {
pub(crate) stream: StreamInfo,
pub(crate) media_time_scale: u32,
pub(crate) sample_entry: SampleEntry,
pub(crate) sample_sizes: Vec<u32>,
pub(crate) stts: Vec<(u32, u32)>,
pub(crate) keyframes: Vec<u32>,
pub(crate) chunk_offsets: Vec<u64>,
pub(crate) stsc: Vec<(u32, u32, u32)>,
current_chunk_samples: u32,
current_chunk_start_offset: u64,
pub(crate) samples_per_chunk_target: u32,
chunk_count: u32,
last_samples_per_chunk: u32,
next_sample_index: u32,
pub(crate) cumulative_duration: u64,
pub(crate) prev_pts_in_ts: Option<i64>,
pub(crate) first_pts_in_ts: Option<i64>,
}
impl TrackState {
pub(crate) fn new(stream: StreamInfo, sample_entry: SampleEntry) -> Self {
let media_time_scale = match stream.params.media_type {
MediaType::Audio => stream.params.sample_rate.unwrap_or(1000),
_ => 1000,
};
Self {
stream,
media_time_scale,
sample_entry,
sample_sizes: Vec::new(),
stts: Vec::new(),
keyframes: Vec::new(),
chunk_offsets: Vec::new(),
stsc: Vec::new(),
current_chunk_samples: 0,
current_chunk_start_offset: 0,
samples_per_chunk_target: 1,
chunk_count: 0,
last_samples_per_chunk: 0,
next_sample_index: 0,
cumulative_duration: 0,
prev_pts_in_ts: None,
first_pts_in_ts: None,
}
}
fn close_current_chunk(&mut self) {
if self.current_chunk_samples == 0 {
return;
}
let spc = self.current_chunk_samples;
self.chunk_count += 1;
if spc != self.last_samples_per_chunk {
self.stsc.push((self.chunk_count, spc, 1));
self.last_samples_per_chunk = spc;
}
self.current_chunk_samples = 0;
}
}
pub fn open(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
open_with_options(output, streams, Mp4MuxerOptions::default())
}
pub fn open_mov(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
let opts = Mp4MuxerOptions {
brand: BrandPreset::Mov,
..Mp4MuxerOptions::default()
};
open_with_options(output, streams, opts)
}
pub fn open_ismv(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
let opts = Mp4MuxerOptions {
brand: BrandPreset::Ismv,
fragmented: Some(FragmentedOptions::default()),
..Mp4MuxerOptions::default()
};
open_with_options(output, streams, opts)
}
pub fn open_dash(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
let opts = Mp4MuxerOptions {
brand: BrandPreset::Custom {
major: *b"iso6",
compatible: vec![*b"iso6", *b"mp41", *b"dash", *b"cmfc"],
},
fragmented: Some(FragmentedOptions::default()),
..Mp4MuxerOptions::default()
};
open_with_options(output, streams, opts)
}
pub fn open_with_options(
output: Box<dyn WriteSeek>,
streams: &[StreamInfo],
options: Mp4MuxerOptions,
) -> Result<Box<dyn Muxer>> {
if streams.is_empty() {
return Err(Error::invalid("mp4 muxer: need at least one stream"));
}
if options.faststart && options.fragmented.is_some() {
return Err(Error::invalid(
"mp4 muxer: faststart and fragmented are mutually exclusive",
));
}
if let Some(frag_opts) = options.fragmented.clone() {
return crate::frag::open_fragmented(output, streams, options, frag_opts);
}
let mut tracks = Vec::with_capacity(streams.len());
for s in streams {
let entry = sample_entry_for(&s.params)?;
tracks.push(TrackState::new(s.clone(), entry));
}
Ok(Box::new(Mp4Muxer {
output,
tracks,
options,
ftyp_bytes: Vec::new(),
mdat_size_offset: 0,
mdat_start_offset: 0,
mdat_bytes: 0,
mdat_buffer: None,
header_written: false,
trailer_written: false,
}))
}
struct Mp4Muxer {
output: Box<dyn WriteSeek>,
tracks: Vec<TrackState>,
options: Mp4MuxerOptions,
ftyp_bytes: Vec<u8>,
mdat_size_offset: u64,
mdat_start_offset: u64,
mdat_bytes: u64,
mdat_buffer: Option<Cursor<Vec<u8>>>,
header_written: bool,
trailer_written: bool,
}
impl Muxer for Mp4Muxer {
fn format_name(&self) -> &str {
match self.options.brand {
BrandPreset::Mov => "mov",
BrandPreset::Ismv => "ismv",
_ => "mp4",
}
}
fn write_header(&mut self) -> Result<()> {
if self.header_written {
return Err(Error::other("mp4 muxer: write_header called twice"));
}
let major = self.options.brand.major_brand();
let compat = self.options.brand.compatible_brands();
let mut ftyp_body = Vec::with_capacity(8 + 4 * compat.len());
ftyp_body.extend_from_slice(&major);
let minor_version: u32 = match self.options.brand {
BrandPreset::Mp4 => 0x0000_0200,
_ => 0,
};
ftyp_body.extend_from_slice(&minor_version.to_be_bytes());
for b in &compat {
ftyp_body.extend_from_slice(b);
}
let ftyp = wrap_box(b"ftyp", &ftyp_body);
self.ftyp_bytes = ftyp.clone();
if self.options.faststart {
self.output.write_all(&ftyp)?;
self.mdat_start_offset = 0;
self.mdat_size_offset = 0;
self.mdat_buffer = Some(Cursor::new(Vec::new()));
} else {
self.output.write_all(&ftyp)?;
let pos = self.output.stream_position()?;
self.mdat_size_offset = pos;
self.output.write_all(&[0, 0, 0, 0])?;
self.output.write_all(b"mdat")?;
self.mdat_start_offset = self.output.stream_position()?;
}
for t in &mut self.tracks {
t.samples_per_chunk_target = default_samples_per_chunk(&t.stream);
t.current_chunk_start_offset = self.mdat_start_offset;
}
self.header_written = true;
Ok(())
}
fn write_packet(&mut self, packet: &Packet) -> Result<()> {
if !self.header_written {
return Err(Error::other("mp4 muxer: write_header not called"));
}
let idx = packet.stream_index as usize;
if idx >= self.tracks.len() {
return Err(Error::invalid(format!(
"mp4 muxer: unknown stream index {idx}"
)));
}
let cur_offset = self.mdat_start_offset + self.mdat_bytes;
if let Some(buf) = self.mdat_buffer.as_mut() {
buf.get_mut().extend_from_slice(&packet.data);
} else {
self.output.write_all(&packet.data)?;
}
self.mdat_bytes += packet.data.len() as u64;
let t = &mut self.tracks[idx];
let pts_in_ts = packet
.pts
.map(|v| rescale_to_media_ts(v, packet.time_base, t.media_time_scale));
let delta = compute_delta(t, packet, pts_in_ts);
t.sample_sizes.push(packet.data.len() as u32);
match t.stts.last_mut() {
Some((count, d)) if *d == delta => *count += 1,
_ => t.stts.push((1, delta)),
}
t.cumulative_duration += delta as u64;
if packet.flags.keyframe {
t.keyframes.push(t.next_sample_index + 1);
}
if t.current_chunk_samples == 0 {
t.chunk_offsets.push(cur_offset);
t.current_chunk_start_offset = cur_offset;
}
t.current_chunk_samples += 1;
if t.current_chunk_samples >= t.samples_per_chunk_target {
t.close_current_chunk();
}
if let Some(p) = pts_in_ts {
if t.first_pts_in_ts.is_none() {
t.first_pts_in_ts = Some(p);
}
t.prev_pts_in_ts = Some(p);
} else {
let base = t.prev_pts_in_ts.unwrap_or(0);
t.prev_pts_in_ts = Some(base + delta as i64);
if t.first_pts_in_ts.is_none() {
t.first_pts_in_ts = Some(0);
}
}
t.next_sample_index += 1;
Ok(())
}
fn write_trailer(&mut self) -> Result<()> {
if self.trailer_written {
return Ok(());
}
if !self.header_written {
return Err(Error::other("mp4 muxer: write_trailer before write_header"));
}
for t in &mut self.tracks {
t.close_current_chunk();
}
if self.options.faststart {
self.write_trailer_faststart()?;
} else {
self.write_trailer_direct()?;
}
self.output.flush()?;
self.trailer_written = true;
Ok(())
}
}
impl Mp4Muxer {
fn write_trailer_direct(&mut self) -> Result<()> {
let end_pos = self.output.stream_position()?;
let mdat_total = end_pos - self.mdat_size_offset;
if mdat_total <= u32::MAX as u64 {
self.output.seek(SeekFrom::Start(self.mdat_size_offset))?;
self.output.write_all(&(mdat_total as u32).to_be_bytes())?;
self.output.seek(SeekFrom::Start(end_pos))?;
} else {
return Err(Error::unsupported(
"mp4 muxer: mdat > 4 GiB requires largesize header (not yet supported)",
));
}
let moov = build_moov(
&self.tracks,
self.options.write_edit_list,
&self.options.track_sample_groups,
)?;
self.output.write_all(&moov)?;
Ok(())
}
fn write_trailer_faststart(&mut self) -> Result<()> {
let ftyp_size = self.ftyp_bytes.len() as u64;
let mdat_header_size: u64 = 8;
let mdat_payload = self
.mdat_buffer
.take()
.map(|c| c.into_inner())
.unwrap_or_default();
let mdat_total = mdat_header_size + mdat_payload.len() as u64;
if mdat_total > u32::MAX as u64 {
return Err(Error::unsupported(
"mp4 muxer: faststart mdat > 4 GiB requires largesize header (not yet supported)",
));
}
let orig_offsets: Vec<Vec<u64>> = self
.tracks
.iter()
.map(|t| t.chunk_offsets.clone())
.collect();
let mut moov_size: u64 = 0;
let mut moov_bytes: Vec<u8> = Vec::new();
for attempt in 0..4 {
let base = ftyp_size + moov_size + mdat_header_size;
for (t, orig) in self.tracks.iter_mut().zip(orig_offsets.iter()) {
t.chunk_offsets = orig.iter().map(|o| *o + base).collect();
}
let candidate = build_moov(
&self.tracks,
self.options.write_edit_list,
&self.options.track_sample_groups,
)?;
let candidate_size = candidate.len() as u64;
let converged = candidate_size == moov_size;
moov_size = candidate_size;
moov_bytes = candidate;
if converged {
break;
}
if attempt == 3 {
return Err(Error::other(
"mp4 muxer: faststart moov size did not converge",
));
}
}
self.output.seek(SeekFrom::Start(ftyp_size))?;
self.output.write_all(&moov_bytes)?;
self.output.write_all(&(mdat_total as u32).to_be_bytes())?;
self.output.write_all(b"mdat")?;
self.output.write_all(&mdat_payload)?;
Ok(())
}
}
fn build_moov(
tracks: &[TrackState],
write_edit_list: bool,
track_sample_groups: &[TrackSampleGroups],
) -> Result<Vec<u8>> {
let movie_timescale: u32 = 1000;
let mut movie_duration: u64 = 0;
for (i, t) in tracks.iter().enumerate() {
let track_duration_movie =
rescale_u64(t.cumulative_duration, t.media_time_scale, movie_timescale);
if track_duration_movie > movie_duration {
movie_duration = track_duration_movie;
}
let _ = i;
}
let next_track_id = (tracks.len() as u32) + 1;
let mut moov_body = Vec::new();
moov_body.extend_from_slice(&build_mvhd(movie_timescale, movie_duration, next_track_id));
for (i, t) in tracks.iter().enumerate() {
let mut track_sbgp: Vec<&SampleToGroup> = Vec::new();
let mut track_sgpd: Vec<&SampleGroupDescription> = Vec::new();
for tsg in track_sample_groups.iter().filter(|g| g.stream_index == i) {
track_sbgp.extend(tsg.sbgp.iter());
track_sgpd.extend(tsg.sgpd.iter());
}
moov_body.extend_from_slice(&build_trak(
i as u32 + 1,
t,
movie_timescale,
write_edit_list,
&track_sbgp,
&track_sgpd,
)?);
}
Ok(wrap_box(b"moov", &moov_body))
}
pub(crate) fn build_mvhd(timescale: u32, duration: u64, next_track_id: u32) -> Vec<u8> {
let use_v1 = duration > u32::MAX as u64;
let mut body = Vec::with_capacity(120);
if use_v1 {
body.push(1); body.extend_from_slice(&[0, 0, 0]); body.extend_from_slice(&0u64.to_be_bytes()); body.extend_from_slice(&0u64.to_be_bytes()); body.extend_from_slice(×cale.to_be_bytes());
body.extend_from_slice(&duration.to_be_bytes());
} else {
body.push(0); body.extend_from_slice(&[0, 0, 0]); body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(×cale.to_be_bytes());
body.extend_from_slice(&(duration as u32).to_be_bytes());
}
body.extend_from_slice(&0x0001_0000u32.to_be_bytes()); body.extend_from_slice(&0x0100u16.to_be_bytes()); body.extend_from_slice(&[0, 0]); body.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0]); let identity: [u32; 9] = [0x0001_0000, 0, 0, 0, 0x0001_0000, 0, 0, 0, 0x4000_0000];
for v in identity {
body.extend_from_slice(&v.to_be_bytes());
}
body.extend_from_slice(&[0u8; 24]);
body.extend_from_slice(&next_track_id.to_be_bytes());
wrap_box(b"mvhd", &body)
}
fn build_trak(
track_id: u32,
t: &TrackState,
movie_timescale: u32,
write_edit_list: bool,
sbgp: &[&SampleToGroup],
sgpd: &[&SampleGroupDescription],
) -> Result<Vec<u8>> {
let mut body = Vec::new();
let track_duration_movie =
rescale_u64(t.cumulative_duration, t.media_time_scale, movie_timescale);
body.extend_from_slice(&build_tkhd(track_id, track_duration_movie, &t.stream));
if write_edit_list {
if let Some(edts) = build_edts(t, movie_timescale, track_duration_movie) {
body.extend_from_slice(&edts);
}
}
body.extend_from_slice(&build_mdia_with_sample_groups(t, sbgp, sgpd)?);
Ok(wrap_box(b"trak", &body))
}
fn build_edts(t: &TrackState, movie_timescale: u32, track_duration_movie: u64) -> Option<Vec<u8>> {
let first_pts = t.first_pts_in_ts?;
if first_pts <= 0 {
return None;
}
let delay_movie = rescale_u64(first_pts as u64, t.media_time_scale, movie_timescale);
if delay_movie == 0 {
return None;
}
let use_v1 = delay_movie > u32::MAX as u64 || track_duration_movie > u32::MAX as u64;
let media_rate: u32 = 0x0001_0000;
let mut body = Vec::new();
body.push(if use_v1 { 1 } else { 0 }); body.extend_from_slice(&[0, 0, 0]); body.extend_from_slice(&2u32.to_be_bytes());
push_elst_entry(&mut body, use_v1, delay_movie, -1, media_rate);
push_elst_entry(&mut body, use_v1, track_duration_movie, 0, media_rate);
let elst = wrap_box(b"elst", &body);
Some(wrap_box(b"edts", &elst))
}
fn push_elst_entry(
out: &mut Vec<u8>,
use_v1: bool,
segment_duration: u64,
media_time: i64,
media_rate: u32,
) {
if use_v1 {
out.extend_from_slice(&segment_duration.to_be_bytes());
out.extend_from_slice(&media_time.to_be_bytes());
} else {
out.extend_from_slice(&(segment_duration as u32).to_be_bytes());
out.extend_from_slice(&(media_time as i32).to_be_bytes());
}
out.extend_from_slice(&media_rate.to_be_bytes());
}
pub(crate) fn build_tkhd(track_id: u32, duration: u64, stream: &StreamInfo) -> Vec<u8> {
let use_v1 = duration > u32::MAX as u64;
let mut body = Vec::new();
let flags: u32 = 0x0000_0007; if use_v1 {
body.push(1);
body.extend_from_slice(&flags.to_be_bytes()[1..4]);
body.extend_from_slice(&0u64.to_be_bytes()); body.extend_from_slice(&0u64.to_be_bytes()); body.extend_from_slice(&track_id.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&duration.to_be_bytes());
} else {
body.push(0);
body.extend_from_slice(&flags.to_be_bytes()[1..4]);
body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&track_id.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&(duration as u32).to_be_bytes());
}
body.extend_from_slice(&[0u8; 8]); body.extend_from_slice(&0i16.to_be_bytes()); body.extend_from_slice(&0i16.to_be_bytes()); let volume: u16 = if stream.params.media_type == MediaType::Audio {
0x0100
} else {
0
};
body.extend_from_slice(&volume.to_be_bytes());
body.extend_from_slice(&[0, 0]); let identity: [u32; 9] = [0x0001_0000, 0, 0, 0, 0x0001_0000, 0, 0, 0, 0x4000_0000];
for v in identity {
body.extend_from_slice(&v.to_be_bytes());
}
let (w, h) = match stream.params.media_type {
MediaType::Video => (
(stream.params.width.unwrap_or(0)) << 16,
(stream.params.height.unwrap_or(0)) << 16,
),
_ => (0, 0),
};
body.extend_from_slice(&w.to_be_bytes());
body.extend_from_slice(&h.to_be_bytes());
wrap_box(b"tkhd", &body)
}
pub(crate) fn build_mdia(t: &TrackState) -> Result<Vec<u8>> {
build_mdia_with_sample_groups(t, &[], &[])
}
fn build_mdia_with_sample_groups(
t: &TrackState,
sbgp: &[&SampleToGroup],
sgpd: &[&SampleGroupDescription],
) -> Result<Vec<u8>> {
let mut body = Vec::new();
body.extend_from_slice(&build_mdhd(t));
body.extend_from_slice(&build_hdlr(&t.stream));
body.extend_from_slice(&build_minf_with_sample_groups(t, sbgp, sgpd)?);
Ok(wrap_box(b"mdia", &body))
}
fn build_mdhd(t: &TrackState) -> Vec<u8> {
let duration = t.cumulative_duration;
let use_v1 = duration > u32::MAX as u64;
let mut body = Vec::new();
if use_v1 {
body.push(1);
body.extend_from_slice(&[0, 0, 0]); body.extend_from_slice(&0u64.to_be_bytes()); body.extend_from_slice(&0u64.to_be_bytes()); body.extend_from_slice(&t.media_time_scale.to_be_bytes());
body.extend_from_slice(&duration.to_be_bytes());
} else {
body.push(0);
body.extend_from_slice(&[0, 0, 0]); body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&t.media_time_scale.to_be_bytes());
body.extend_from_slice(&(duration as u32).to_be_bytes());
}
let lang = pack_language(b"und");
body.extend_from_slice(&lang.to_be_bytes());
body.extend_from_slice(&0u16.to_be_bytes());
wrap_box(b"mdhd", &body)
}
fn pack_language(code: &[u8; 3]) -> u16 {
let a = (code[0].saturating_sub(0x60) & 0x1F) as u16;
let b = (code[1].saturating_sub(0x60) & 0x1F) as u16;
let c = (code[2].saturating_sub(0x60) & 0x1F) as u16;
(a << 10) | (b << 5) | c
}
fn build_hdlr(stream: &StreamInfo) -> Vec<u8> {
let (handler_type, name): ([u8; 4], &str) = match stream.params.media_type {
MediaType::Audio => (*b"soun", "SoundHandler"),
MediaType::Video => (*b"vide", "VideoHandler"),
MediaType::Subtitle => (
subtitle_handler_for(stream.params.codec_id.as_str()),
"SubtitleHandler",
),
_ => (*b"data", "DataHandler"),
};
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&handler_type);
body.extend_from_slice(&[0u8; 12]); body.extend_from_slice(name.as_bytes());
body.push(0); wrap_box(b"hdlr", &body)
}
fn build_minf_with_sample_groups(
t: &TrackState,
sbgp: &[&SampleToGroup],
sgpd: &[&SampleGroupDescription],
) -> Result<Vec<u8>> {
let mut body = Vec::new();
match t.stream.params.media_type {
MediaType::Audio => body.extend_from_slice(&build_smhd()),
MediaType::Video => body.extend_from_slice(&build_vmhd()),
MediaType::Subtitle => {
if subtitle_uses_sthd(t.stream.params.codec_id.as_str()) {
body.extend_from_slice(&build_sthd());
} else {
body.extend_from_slice(&build_nmhd());
}
}
_ => body.extend_from_slice(&build_nmhd()),
}
body.extend_from_slice(&build_dinf());
body.extend_from_slice(&build_stbl_with_sample_groups(t, sbgp, sgpd)?);
Ok(wrap_box(b"minf", &body))
}
fn build_sthd() -> Vec<u8> {
let body = [0u8; 4]; wrap_box(b"sthd", &body)
}
fn build_smhd() -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&0i16.to_be_bytes()); body.extend_from_slice(&[0, 0]); wrap_box(b"smhd", &body)
}
fn build_vmhd() -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0, 1]); body.extend_from_slice(&0u16.to_be_bytes()); body.extend_from_slice(&[0u8; 6]); wrap_box(b"vmhd", &body)
}
fn build_nmhd() -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0, 0]); wrap_box(b"nmhd", &body)
}
fn build_dinf() -> Vec<u8> {
let mut dref_body = Vec::new();
dref_body.extend_from_slice(&[0, 0, 0, 0]); dref_body.extend_from_slice(&1u32.to_be_bytes()); let mut url_body = Vec::new();
url_body.extend_from_slice(&[0, 0, 0, 1]);
dref_body.extend_from_slice(&wrap_box(b"url ", &url_body));
let dref = wrap_box(b"dref", &dref_body);
wrap_box(b"dinf", &dref)
}
fn build_stbl_with_sample_groups(
t: &TrackState,
sbgp: &[&SampleToGroup],
sgpd: &[&SampleGroupDescription],
) -> Result<Vec<u8>> {
let mut body = Vec::new();
body.extend_from_slice(&build_stsd(t));
body.extend_from_slice(&build_stts(&t.stts));
if !t.keyframes.is_empty() {
body.extend_from_slice(&build_stss(&t.keyframes));
}
body.extend_from_slice(&build_stsc(&t.stsc));
body.extend_from_slice(&build_stsz(&t.sample_sizes));
body.extend_from_slice(&build_chunk_offset_box(&t.chunk_offsets));
for sg in sgpd {
body.extend_from_slice(&build_sgpd(sg));
}
for sb in sbgp {
body.extend_from_slice(&build_sbgp(sb));
}
Ok(wrap_box(b"stbl", &body))
}
fn build_stsd(t: &TrackState) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&wrap_box(&t.sample_entry.fourcc, &t.sample_entry.body));
wrap_box(b"stsd", &body)
}
fn build_stts(runs: &[(u32, u32)]) -> Vec<u8> {
let mut body = Vec::with_capacity(8 + runs.len() * 8);
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&(runs.len() as u32).to_be_bytes());
for (count, delta) in runs {
body.extend_from_slice(&count.to_be_bytes());
body.extend_from_slice(&delta.to_be_bytes());
}
wrap_box(b"stts", &body)
}
fn build_stss(keyframes: &[u32]) -> Vec<u8> {
let mut body = Vec::with_capacity(8 + keyframes.len() * 4);
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&(keyframes.len() as u32).to_be_bytes());
for k in keyframes {
body.extend_from_slice(&k.to_be_bytes());
}
wrap_box(b"stss", &body)
}
fn build_stsc(runs: &[(u32, u32, u32)]) -> Vec<u8> {
let mut body = Vec::with_capacity(8 + runs.len() * 12);
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&(runs.len() as u32).to_be_bytes());
for (fc, spc, sdi) in runs {
body.extend_from_slice(&fc.to_be_bytes());
body.extend_from_slice(&spc.to_be_bytes());
body.extend_from_slice(&sdi.to_be_bytes());
}
wrap_box(b"stsc", &body)
}
fn build_stsz(sizes: &[u32]) -> Vec<u8> {
let mut body = Vec::with_capacity(12 + sizes.len() * 4);
body.extend_from_slice(&[0, 0, 0, 0]); let uniform = if !sizes.is_empty() && sizes.iter().all(|&s| s == sizes[0]) {
sizes[0]
} else {
0
};
body.extend_from_slice(&uniform.to_be_bytes());
body.extend_from_slice(&(sizes.len() as u32).to_be_bytes());
if uniform == 0 {
for s in sizes {
body.extend_from_slice(&s.to_be_bytes());
}
}
wrap_box(b"stsz", &body)
}
fn build_chunk_offset_box(offsets: &[u64]) -> Vec<u8> {
let needs_64 = offsets.iter().any(|&o| o > u32::MAX as u64);
if needs_64 {
let mut body = Vec::with_capacity(8 + offsets.len() * 8);
body.extend_from_slice(&[0, 0, 0, 0]);
body.extend_from_slice(&(offsets.len() as u32).to_be_bytes());
for o in offsets {
body.extend_from_slice(&o.to_be_bytes());
}
wrap_box(b"co64", &body)
} else {
let mut body = Vec::with_capacity(8 + offsets.len() * 4);
body.extend_from_slice(&[0, 0, 0, 0]);
body.extend_from_slice(&(offsets.len() as u32).to_be_bytes());
for o in offsets {
body.extend_from_slice(&(*o as u32).to_be_bytes());
}
wrap_box(b"stco", &body)
}
}
pub(crate) fn wrap_box(kind: &[u8; 4], body: &[u8]) -> Vec<u8> {
let total = (8 + body.len()) as u32;
let mut out = Vec::with_capacity(total as usize);
out.extend_from_slice(&total.to_be_bytes());
out.extend_from_slice(kind);
out.extend_from_slice(body);
out
}
pub(crate) fn default_samples_per_chunk(stream: &StreamInfo) -> u32 {
match stream.params.media_type {
MediaType::Audio => {
if stream.params.codec_id.as_str().starts_with("pcm_") {
1
} else {
50
}
}
MediaType::Video => 1,
_ => 1,
}
}
fn compute_delta(t: &TrackState, packet: &Packet, pts_in_ts: Option<i64>) -> u32 {
if let Some(d) = packet.duration {
if d > 0 {
let v = rescale_to_media_ts(d, packet.time_base, t.media_time_scale);
if v > 0 {
return v as u32;
}
}
}
if let (Some(prev), Some(cur)) = (t.prev_pts_in_ts, pts_in_ts) {
let d = (cur - prev).max(0);
if d > 0 {
return d as u32;
}
}
1
}
pub(crate) fn rescale_to_media_ts(value: i64, from_tb: oxideav_core::TimeBase, to_ts: u32) -> i64 {
let from_r = from_tb.as_rational();
if from_r.den == 0 || to_ts == 0 {
return value;
}
let num = from_r.num as i128 * to_ts as i128;
let den = from_r.den as i128;
let prod = value as i128 * num;
(prod / den) as i64
}
pub(crate) fn rescale_u64(value: u64, from_ts: u32, to_ts: u32) -> u64 {
if from_ts == 0 {
return value;
}
let prod = value as u128 * to_ts as u128;
(prod / from_ts as u128) as u64
}
#[cfg(test)]
mod tests {
use super::*;
use oxideav_core::{CodecId, CodecParameters};
#[test]
fn wrap_box_header_layout() {
let b = wrap_box(b"test", &[1, 2, 3]);
assert_eq!(&b[0..4], &[0, 0, 0, 11]);
assert_eq!(&b[4..8], b"test");
assert_eq!(&b[8..], &[1, 2, 3]);
}
#[test]
fn chunk_offset_auto_promotes_to_co64() {
let offsets = [(u32::MAX as u64) + 1];
let b = build_chunk_offset_box(&offsets);
assert_eq!(&b[4..8], b"co64");
}
#[test]
fn chunk_offset_stays_stco_when_small() {
let offsets = [42u64, 1234, 999_999];
let b = build_chunk_offset_box(&offsets);
assert_eq!(&b[4..8], b"stco");
}
#[test]
fn pack_language_und() {
assert_eq!(pack_language(b"und"), ((21u16) << 10) | ((14u16) << 5) | 4);
}
fn track_with_first_pts(first_pts: Option<i64>, media_time_scale: u32) -> TrackState {
let mut params = CodecParameters::audio(CodecId::new("pcm_s16le"));
params.sample_rate = Some(media_time_scale);
params.channels = Some(2);
let stream = StreamInfo {
index: 0,
time_base: oxideav_core::TimeBase::new(1, media_time_scale as i64),
duration: None,
start_time: None,
params,
};
let entry = SampleEntry {
fourcc: *b"sowt",
body: Vec::new(),
};
let mut t = TrackState::new(stream, entry);
t.media_time_scale = media_time_scale;
t.first_pts_in_ts = first_pts;
t.cumulative_duration = media_time_scale as u64; t
}
#[test]
fn edts_absent_when_first_pts_zero() {
let t = track_with_first_pts(Some(0), 48_000);
assert!(build_edts(&t, 1000, 1000).is_none());
let none = track_with_first_pts(None, 48_000);
assert!(build_edts(&none, 1000, 1000).is_none());
}
#[test]
fn edts_v0_layout_for_positive_delay() {
let t = track_with_first_pts(Some(24_000), 48_000);
let track_dur_movie = 1000u64;
let edts = build_edts(&t, 1000, track_dur_movie).expect("edts emitted");
assert_eq!(&edts[4..8], b"edts");
let elst = &edts[8..];
assert_eq!(&elst[4..8], b"elst");
let body = &elst[8..];
assert_eq!(body[0], 0, "version 0 for sub-32-bit fields");
let entry_count = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
assert_eq!(entry_count, 2);
let e1 = &body[8..20];
let seg1 = u32::from_be_bytes([e1[0], e1[1], e1[2], e1[3]]);
let mt1 = i32::from_be_bytes([e1[4], e1[5], e1[6], e1[7]]);
let rate1 = u32::from_be_bytes([e1[8], e1[9], e1[10], e1[11]]);
assert_eq!(seg1, 500, "0.5 s delay at movie timescale 1000");
assert_eq!(mt1, -1, "empty edit");
assert_eq!(rate1, 0x0001_0000, "media_rate 1.0");
let e2 = &body[20..32];
let seg2 = u32::from_be_bytes([e2[0], e2[1], e2[2], e2[3]]);
let mt2 = i32::from_be_bytes([e2[4], e2[5], e2[6], e2[7]]);
assert_eq!(seg2, track_dur_movie as u32);
assert_eq!(mt2, 0, "non-empty edit starts at media time 0");
}
#[test]
fn edts_v1_layout_for_large_duration() {
let mut t = track_with_first_pts(Some(1000), 1000);
t.media_time_scale = 1000;
let big_dur = (u32::MAX as u64) + 10;
let edts = build_edts(&t, 1000, big_dur).expect("edts emitted");
let body = &edts[8..][8..];
assert_eq!(body[0], 1, "version 1 for >32-bit duration");
let off = 8 + 20;
let seg2 = u64::from_be_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
body[off + 4],
body[off + 5],
body[off + 6],
body[off + 7],
]);
assert_eq!(seg2, big_dur);
}
}