use std::io::{Seek, SeekFrom, Write};
use oxideav_core::{Error, Muxer, Packet, Result, StreamInfo, WriteSeek};
use crate::muxer::{
build_mdia, build_mvhd, build_tkhd, default_samples_per_chunk, rescale_to_media_ts, wrap_box,
TrackState,
};
use crate::options::{BrandPreset, FragmentCadence, FragmentedOptions, Mp4MuxerOptions};
use crate::sample_entries::sample_entry_for;
#[derive(Clone, Copy, Debug)]
pub struct PrftRequest {
pub reference_track_id: u32,
pub ntp_timestamp: u64,
pub media_time: u64,
pub flags: u32,
pub force_v1: bool,
}
#[derive(Clone, Debug)]
struct PendingSample {
data: Vec<u8>,
duration: u32,
flags: u32,
composition_time_offset: i32,
}
struct FragTrackState {
base: TrackState,
track_id: u32,
protection: Option<crate::cenc::CencSchemeDecision>,
pending_senc: Vec<crate::cenc::SencSample>,
pending_seig: Vec<Option<crate::cenc::SeigEntry>>,
trex_default_sample_duration: u32,
trex_default_sample_size: u32,
trex_default_sample_flags: u32,
trex_locked: bool,
pending: Vec<PendingSample>,
next_bmdt: u64,
packets_total: u64,
tfra_entries: Vec<TfraEmitEntry>,
}
#[derive(Clone, Copy, Debug)]
struct TfraEmitEntry {
time: u64,
moof_offset: u64,
traf_number: u32,
trun_number: u32,
sample_number: u32,
}
impl FragTrackState {
fn new(
base: TrackState,
track_id: u32,
protection: Option<crate::cenc::CencSchemeDecision>,
) -> Self {
Self {
base,
track_id,
protection,
pending_senc: Vec::new(),
pending_seig: Vec::new(),
trex_default_sample_duration: 0,
trex_default_sample_size: 0,
trex_default_sample_flags: 0,
trex_locked: false,
pending: Vec::new(),
next_bmdt: 0,
packets_total: 0,
tfra_entries: Vec::new(),
}
}
fn lock_trex(&mut self, duration: u32, size: u32, flags: u32) {
if self.trex_locked {
return;
}
self.trex_default_sample_duration = duration;
self.trex_default_sample_size = size;
self.trex_default_sample_flags = flags;
self.trex_locked = true;
}
}
const SAMPLE_IS_NON_SYNC: u32 = 0x0001_0000;
const SAMPLE_DEPENDS_ON_NONE: u32 = 0x0200_0000;
fn sample_flags_for(keyframe: bool) -> u32 {
if keyframe {
SAMPLE_DEPENDS_ON_NONE
} else {
SAMPLE_IS_NON_SYNC
}
}
pub(crate) fn open_fragmented(
output: Box<dyn WriteSeek>,
streams: &[StreamInfo],
options: Mp4MuxerOptions,
frag_options: FragmentedOptions,
) -> Result<Box<dyn Muxer>> {
let m = open_fragmented_typed(output, streams, options, frag_options)?;
Ok(Box::new(m))
}
pub fn open_fragmented_typed(
output: Box<dyn WriteSeek>,
streams: &[StreamInfo],
options: Mp4MuxerOptions,
frag_options: FragmentedOptions,
) -> Result<FragmentedMuxer> {
if streams.is_empty() {
return Err(Error::invalid("mp4 muxer: need at least one stream"));
}
let mut tracks = Vec::with_capacity(streams.len());
for (i, s) in streams.iter().enumerate() {
let mut entry = sample_entry_for(&s.params)?;
let mut protection = None;
if let Some(prot) = options
.track_protection
.iter()
.find(|p| p.stream_index == i)
{
entry = crate::sample_entries::apply_protection(entry, s.params.media_type, prot)?;
protection = Some(crate::cenc::CencSchemeDecision::new(
crate::cenc::CencScheme::from_fourcc(&prot.scheme_type),
prot.tenc.clone(),
)?);
}
let mut base = TrackState::new(s.clone(), entry);
base.samples_per_chunk_target = default_samples_per_chunk(&base.stream);
tracks.push(FragTrackState::new(base, (i as u32) + 1, protection));
}
for tel in &options.track_edit_lists {
if tel.stream_index >= streams.len() {
return Err(Error::invalid(format!(
"mp4 muxer: track_edit_lists stream_index {} out of range ({} streams)",
tel.stream_index,
streams.len()
)));
}
crate::demux::build_elst_box(&tel.entries)?;
}
Ok(FragmentedMuxer {
output,
tracks,
options,
frag_options,
sequence_number: 0,
header_written: false,
trailer_written: false,
styp_override: None,
pending_prft: None,
pending_moof_pssh: Vec::new(),
pending_emsg: Vec::new(),
mehd_patch_pos: None,
})
}
pub struct FragmentedMuxer {
output: Box<dyn WriteSeek>,
tracks: Vec<FragTrackState>,
options: Mp4MuxerOptions,
frag_options: FragmentedOptions,
sequence_number: u32,
header_written: bool,
trailer_written: bool,
styp_override: Option<([u8; 4], Vec<[u8; 4]>)>,
pending_prft: Option<PrftRequest>,
pending_moof_pssh: Vec<crate::cenc::PsshBox>,
pending_emsg: Vec<crate::emsg::EmsgBox>,
mehd_patch_pos: Option<u64>,
}
impl Muxer for FragmentedMuxer {
fn format_name(&self) -> &str {
match (&self.options.brand, self.options.fragmented.is_some()) {
(BrandPreset::Ismv, true) => "ismv",
(BrandPreset::Mov, _) => "mov",
_ => "mp4",
}
}
fn write_header(&mut self) -> Result<()> {
if self.header_written {
return Err(Error::other("mp4 muxer: write_header called twice"));
}
let ftyp = build_ftyp(&self.options.brand);
self.output.write_all(&ftyp)?;
let (moov, mehd_off) = build_init_moov(
&self.tracks,
&self.options.track_edit_lists,
&self.frag_options.levels,
&self.frag_options.treps,
&self.options.pssh,
self.frag_options.write_mehd,
)?;
self.mehd_patch_pos = mehd_off.map(|o| ftyp.len() as u64 + o as u64);
self.output.write_all(&moov)?;
self.header_written = true;
Ok(())
}
fn write_packet(&mut self, packet: &Packet) -> Result<()> {
self.queue_packet(packet, None)
}
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"));
}
if self.tracks.iter().any(|t| !t.pending.is_empty()) {
self.flush_fragment_inner(false)?;
}
if self.frag_options.emit_random_access_indexes
&& self.tracks.iter().any(|t| !t.tfra_entries.is_empty())
{
self.write_mfra()?;
}
if let Some(pos) = self.mehd_patch_pos {
let mut fragment_duration: u64 = 0;
for t in &self.tracks {
let ts = t.base.media_time_scale as u128;
if ts == 0 {
continue;
}
let scaled = ((t.next_bmdt as u128) * 1000).div_ceil(ts);
fragment_duration = fragment_duration.max(scaled.min(u64::MAX as u128) as u64);
}
let end = self.output.stream_position()?;
self.output.seek(std::io::SeekFrom::Start(pos))?;
self.output.write_all(&fragment_duration.to_be_bytes())?;
self.output.seek(std::io::SeekFrom::Start(end))?;
}
self.output.flush()?;
self.trailer_written = true;
Ok(())
}
}
impl FragmentedMuxer {
pub fn write_protected_packet(
&mut self,
packet: &Packet,
senc: crate::cenc::SencSample,
) -> Result<()> {
self.queue_packet_protected(packet, senc, None)
}
pub fn write_protected_packet_grouped(
&mut self,
packet: &Packet,
senc: crate::cenc::SencSample,
seig: Option<crate::cenc::SeigEntry>,
) -> Result<()> {
self.queue_packet_protected(packet, senc, seig)
}
fn queue_packet_protected(
&mut self,
packet: &Packet,
senc: crate::cenc::SencSample,
seig: Option<crate::cenc::SeigEntry>,
) -> Result<()> {
if let Some(entry) = &seig {
let idx = packet.stream_index as usize;
crate::cenc::build_seig_entry(entry)?;
if let Some(track) = self.tracks.get(idx) {
if let Some(decision) = &track.protection {
if entry.is_protected == 1
&& entry.per_sample_iv_size != decision.tenc.default_per_sample_iv_size
{
return Err(Error::invalid(format!(
"mp4 muxer: stream {idx} seig override changes \
Per_Sample_IV_Size from {} to {} — the fragment's senc stores \
one IV width for all samples (§7.2.3)",
decision.tenc.default_per_sample_iv_size, entry.per_sample_iv_size
)));
}
}
}
}
self.queue_packet_impl(packet, Some(senc), seig)
}
fn queue_packet(
&mut self,
packet: &Packet,
senc: Option<crate::cenc::SencSample>,
) -> Result<()> {
self.queue_packet_impl(packet, senc, None)
}
fn queue_packet_impl(
&mut self,
packet: &Packet,
senc: Option<crate::cenc::SencSample>,
seig: Option<crate::cenc::SeigEntry>,
) -> 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}"
)));
}
match &senc {
Some(entry) => {
let track = &self.tracks[idx];
let decision = track.protection.as_ref().ok_or_else(|| {
Error::invalid(format!(
"mp4 muxer: write_protected_packet on stream {idx} without a \
track_protection directive"
))
})?;
if track.pending_senc.len() != track.pending.len() {
return Err(Error::invalid(format!(
"mp4 muxer: stream {idx} mixes write_protected_packet with plain \
write_packet within one fragment (senc covers all samples or none, \
ISO/IEC 23001-7 §7.2.3)"
)));
}
match decision.iv_supply() {
crate::cenc::IvSupply::PerSample { size } => {
if entry.initialization_vector.len() != size as usize {
return Err(Error::invalid(format!(
"mp4 muxer: stream {idx} per-sample IV is {} bytes but \
tenc.default_Per_Sample_IV_Size is {size} (§9.2)",
entry.initialization_vector.len()
)));
}
}
crate::cenc::IvSupply::Constant => {
if !entry.initialization_vector.is_empty() {
return Err(Error::invalid(format!(
"mp4 muxer: stream {idx} uses a constant IV — senc entries \
must not carry per-sample IV bytes (§9.2)"
)));
}
}
crate::cenc::IvSupply::None => {
return Err(Error::invalid(format!(
"mp4 muxer: stream {idx} tenc default is unprotected \
(isProtected == 0) — write_protected_packet has no IV context"
)));
}
}
if !entry.subsamples.is_empty() {
let mut total: u64 = 0;
for s in &entry.subsamples {
total += s.bytes_of_clear_data as u64 + s.bytes_of_protected_data as u64;
}
if total != packet.data.len() as u64 {
return Err(Error::invalid(format!(
"mp4 muxer: stream {idx} subsample map covers {total} bytes but \
the sample is {} bytes (§9.5.1)",
packet.data.len()
)));
}
}
}
None => {
if !self.tracks[idx].pending_senc.is_empty() {
return Err(Error::invalid(format!(
"mp4 muxer: stream {idx} mixes plain write_packet with \
write_protected_packet within one fragment (senc covers all samples \
or none, ISO/IEC 23001-7 §7.2.3)"
)));
}
}
}
let media_ts = self.tracks[idx].base.media_time_scale;
let dur = if let Some(d) = packet.duration {
let v = rescale_to_media_ts(d, packet.time_base, media_ts);
if v > 0 {
v as u32
} else {
1
}
} else if let (Some(prev), Some(cur)) = (
self.tracks[idx].base.prev_pts_in_ts,
packet
.pts
.map(|v| rescale_to_media_ts(v, packet.time_base, media_ts)),
) {
((cur - prev).max(0) as u32).max(1)
} else {
1
};
let cts_off = match (packet.pts, packet.dts) {
(Some(p), Some(d)) => {
let pp = rescale_to_media_ts(p, packet.time_base, media_ts);
let dd = rescale_to_media_ts(d, packet.time_base, media_ts);
(pp - dd) as i32
}
_ => 0,
};
let flags = sample_flags_for(packet.flags.keyframe);
let size = packet.data.len() as u32;
let track = &mut self.tracks[idx];
track.lock_trex(dur, size, flags);
track.pending.push(PendingSample {
data: packet.data.clone(),
duration: dur,
flags,
composition_time_offset: cts_off,
});
if let Some(entry) = senc {
track.pending_senc.push(entry);
track.pending_seig.push(seig);
}
let pts_in_ts = packet
.pts
.map(|v| rescale_to_media_ts(v, packet.time_base, media_ts));
if let Some(p) = pts_in_ts {
if track.base.first_pts_in_ts.is_none() {
track.base.first_pts_in_ts = Some(p);
}
track.base.prev_pts_in_ts = Some(p);
} else {
let base = track.base.prev_pts_in_ts.unwrap_or(0);
track.base.prev_pts_in_ts = Some(base + dur as i64);
if track.base.first_pts_in_ts.is_none() {
track.base.first_pts_in_ts = Some(0);
}
}
track.base.cumulative_duration += dur as u64;
track.packets_total += 1;
if self.should_flush(idx, packet.flags.keyframe) {
self.flush_fragment()?;
}
Ok(())
}
pub fn write_fragmented_segment_with_styp(
&mut self,
major_brand: [u8; 4],
compat_brands: &[[u8; 4]],
) {
self.styp_override = Some((major_brand, compat_brands.to_vec()));
}
pub fn set_next_segment_prft(
&mut self,
reference_track_id: u32,
ntp_timestamp: u64,
media_time: u64,
flags: u32,
) {
self.pending_prft = Some(PrftRequest {
reference_track_id,
ntp_timestamp,
media_time,
flags,
force_v1: false,
});
}
pub fn set_next_segment_prft_v1(
&mut self,
reference_track_id: u32,
ntp_timestamp: u64,
media_time: u64,
flags: u32,
) {
self.pending_prft = Some(PrftRequest {
reference_track_id,
ntp_timestamp,
media_time,
flags,
force_v1: true,
});
}
pub fn set_next_segment_pssh(&mut self, pssh: impl IntoIterator<Item = crate::cenc::PsshBox>) {
self.pending_moof_pssh.extend(pssh);
}
pub fn set_next_segment_emsg(
&mut self,
events: impl IntoIterator<Item = crate::emsg::EmsgBox>,
) {
self.pending_emsg.extend(events);
}
pub fn insert_empty_time(&mut self, stream_index: usize, duration: u32) -> Result<()> {
if !self.header_written {
return Err(Error::other(
"mp4 muxer: insert_empty_time before write_header",
));
}
if self.trailer_written {
return Err(Error::other(
"mp4 muxer: insert_empty_time after write_trailer",
));
}
if stream_index >= self.tracks.len() {
return Err(Error::invalid(format!(
"mp4 muxer: insert_empty_time stream_index {} out of range ({} streams)",
stream_index,
self.tracks.len()
)));
}
if !self.options.track_edit_lists.is_empty() {
return Err(Error::invalid(
"mp4 muxer: §8.8.7.1 forbids combining empty-duration fragments \
with edit lists in the Movie Box (track_edit_lists is set)",
));
}
if duration == 0 {
return Ok(());
}
if self.tracks.iter().any(|t| !t.pending.is_empty()) {
self.flush_fragment_inner(false)?;
}
self.sequence_number += 1;
let seq = self.sequence_number;
let t = &self.tracks[stream_index];
let tfhd_flags: u32 = 0x010000 | 0x000008;
let mut tfhd_body = Vec::with_capacity(12);
tfhd_body.push(0); tfhd_body.extend_from_slice(&tfhd_flags.to_be_bytes()[1..4]);
tfhd_body.extend_from_slice(&t.track_id.to_be_bytes());
tfhd_body.extend_from_slice(&duration.to_be_bytes());
let mut traf_body = wrap_box(b"tfhd", &tfhd_body);
traf_body.extend_from_slice(&build_tfdt(t.next_bmdt));
let traf = wrap_box(b"traf", &traf_body);
let mut moof_body = build_mfhd(seq);
moof_body.extend_from_slice(&traf);
let moof = wrap_box(b"moof", &moof_body);
self.output.write_all(&moof)?;
self.tracks[stream_index].next_bmdt += duration as u64;
Ok(())
}
fn should_flush(&self, current_track_idx: usize, is_keyframe: bool) -> bool {
match self.frag_options.cadence {
FragmentCadence::EverySeconds(secs) => {
let anchor = 0usize;
if self.tracks[anchor].pending.is_empty() {
return false;
}
let media_ts = self.tracks[anchor].base.media_time_scale as f64;
let pending_ticks: u64 = self.tracks[anchor]
.pending
.iter()
.map(|s| s.duration as u64)
.sum();
(pending_ticks as f64 / media_ts) >= secs
}
FragmentCadence::EveryKeyframe => {
let anchor = 0usize;
if current_track_idx != anchor {
return false;
}
if !is_keyframe {
return false;
}
self.tracks[anchor].pending.len() >= 2
}
FragmentCadence::EveryNPackets(n) => {
if n == 0 {
return false;
}
let anchor = 0usize;
if current_track_idx != anchor {
return false;
}
self.tracks[anchor].pending.len() as u32 >= n
}
}
}
fn flush_fragment(&mut self) -> Result<()> {
self.flush_fragment_inner(true)
}
fn flush_fragment_inner(&mut self, detach_trailing_keyframe: bool) -> Result<()> {
type Detached = (
usize,
PendingSample,
Option<(crate::cenc::SencSample, Option<crate::cenc::SeigEntry>)>,
);
let mut detached: Vec<Detached> = Vec::new();
if detach_trailing_keyframe
&& matches!(self.frag_options.cadence, FragmentCadence::EveryKeyframe)
{
let anchor = 0usize;
if let Some(last) = self.tracks[anchor].pending.last() {
if last.flags & SAMPLE_IS_NON_SYNC == 0 {
let s = self.tracks[anchor].pending.pop().unwrap();
let cenc = if self.tracks[anchor].pending_senc.len()
> self.tracks[anchor].pending.len()
{
let senc = self.tracks[anchor].pending_senc.pop().unwrap();
let seig = self.tracks[anchor].pending_seig.pop().flatten();
Some((senc, seig))
} else {
None
};
self.tracks[anchor].base.cumulative_duration = self.tracks[anchor]
.base
.cumulative_duration
.saturating_sub(s.duration as u64);
detached.push((anchor, s, cenc));
}
}
}
if self.tracks.iter().all(|t| t.pending.is_empty()) {
for (idx, s, cenc) in detached {
self.tracks[idx].base.cumulative_duration += s.duration as u64;
self.tracks[idx].pending.push(s);
if let Some((senc, seig)) = cenc {
self.tracks[idx].pending_senc.push(senc);
self.tracks[idx].pending_seig.push(seig);
}
}
return Ok(());
}
self.sequence_number += 1;
let seq = self.sequence_number;
let moof_pssh = std::mem::take(&mut self.pending_moof_pssh);
let moof = build_moof(seq, &self.tracks, &moof_pssh)?;
let moof_size = moof.len() as u64;
let pending_emsg = std::mem::take(&mut self.pending_emsg);
let emsg_boxes: Vec<Vec<u8>> = pending_emsg
.iter()
.map(crate::emsg::build_emsg_box)
.collect::<Result<_>>()?;
let emsg_size: u64 = emsg_boxes.iter().map(|b| b.len() as u64).sum();
let mut mdat_payload: Vec<u8> = Vec::new();
for t in &self.tracks {
for s in &t.pending {
mdat_payload.extend_from_slice(&s.data);
}
}
let mdat = wrap_box(b"mdat", &mdat_payload);
let mdat_size = mdat.len() as u64;
if self.frag_options.emit_random_access_indexes {
let styp_size: u64 = if let Some((major, compat)) = &self.styp_override {
crate::styp::build_styp(*major, compat).len() as u64
} else {
self.frag_options
.styp
.as_ref()
.map(|b| build_styp(b).len() as u64)
.unwrap_or(0)
};
let prft_size: u64 = self
.pending_prft
.as_ref()
.map(|p| build_prft(p).len() as u64)
.unwrap_or(0);
let subsegment_size = styp_size + emsg_size + prft_size + moof_size + mdat_size;
let anchor_idx = self
.tracks
.iter()
.position(|t| !t.pending.is_empty())
.unwrap_or(0);
let ept = self.tracks[anchor_idx].next_bmdt;
let timescale = self.tracks[anchor_idx].base.media_time_scale;
let frag_dur_anchor: u64 = self.tracks[anchor_idx]
.pending
.iter()
.map(|s| s.duration as u64)
.sum();
let subseg_dur_u32 = frag_dur_anchor.min(u32::MAX as u64) as u32;
let starts_sap = self.tracks[anchor_idx]
.pending
.first()
.map(|s| s.flags & SAMPLE_IS_NON_SYNC == 0)
.unwrap_or(false);
let sidx = build_sidx(
self.tracks[anchor_idx].track_id,
timescale,
ept,
subsegment_size,
subseg_dur_u32,
starts_sap,
);
self.output.write_all(&sidx)?;
if self.frag_options.emit_ssix {
let (meta_level, media_level) = self.frag_options.ssix_levels;
let meta_size = styp_size + emsg_size + prft_size + moof_size;
if meta_size > u32::MAX as u64 || mdat_size > u32::MAX as u64 {
return Err(Error::invalid("MP4: ssix subsegment range exceeds 32 bits"));
}
let record = crate::demux::SsixRecord {
subsegments: vec![crate::demux::SsixSubsegment {
ranges: vec![
crate::demux::SsixRange {
level: meta_level,
range_size: meta_size as u32,
},
crate::demux::SsixRange {
level: media_level,
range_size: mdat_size as u32,
},
],
}],
};
let ssix = crate::demux::build_ssix_box(&record)?;
self.output.write_all(&ssix)?;
}
}
if let Some((major, compat)) = self.styp_override.take() {
crate::styp::write_styp(&mut self.output, major, &compat)?;
} else if let Some(brand) = &self.frag_options.styp {
let styp = build_styp(brand);
self.output.write_all(&styp)?;
}
for b in &emsg_boxes {
self.output.write_all(b)?;
}
if let Some(prft) = self.pending_prft.take() {
let bytes = build_prft(&prft);
self.output.write_all(&bytes)?;
}
let moof_offset = self.output.stream_position()?;
self.output.write_all(&moof)?;
self.output.write_all(&mdat)?;
for t in self.tracks.iter_mut() {
if t.pending.is_empty() {
continue;
}
let mut dts_in_frag: u64 = 0;
for (k, s) in t.pending.iter().enumerate() {
let is_sync = s.flags & SAMPLE_IS_NON_SYNC == 0;
if is_sync {
t.tfra_entries.push(TfraEmitEntry {
time: t.next_bmdt + dts_in_frag,
moof_offset,
traf_number: 1,
trun_number: 1,
sample_number: (k as u32) + 1,
});
}
dts_in_frag += s.duration as u64;
}
}
for t in self.tracks.iter_mut() {
let frag_dur: u64 = t.pending.iter().map(|s| s.duration as u64).sum();
t.next_bmdt += frag_dur;
t.pending.clear();
t.pending_senc.clear();
t.pending_seig.clear();
}
for (idx, s, cenc) in detached {
self.tracks[idx].base.cumulative_duration += s.duration as u64;
self.tracks[idx].pending.push(s);
if let Some((senc, seig)) = cenc {
self.tracks[idx].pending_senc.push(senc);
self.tracks[idx].pending_seig.push(seig);
}
}
let _ = moof_size;
Ok(())
}
fn write_mfra(&mut self) -> Result<()> {
let mut mfra_body: Vec<u8> = Vec::new();
for t in &self.tracks {
if t.tfra_entries.is_empty() {
continue;
}
mfra_body.extend_from_slice(&build_tfra(t.track_id, &t.tfra_entries));
}
let mfra_total_size: u64 = 8 + mfra_body.len() as u64 + 16;
let mfro = build_mfro(mfra_total_size as u32);
mfra_body.extend_from_slice(&mfro);
let mfra = wrap_box(b"mfra", &mfra_body);
debug_assert_eq!(mfra.len() as u64, mfra_total_size);
self.output.seek(SeekFrom::End(0))?;
self.output.write_all(&mfra)?;
Ok(())
}
}
fn build_ftyp(brand: &BrandPreset) -> Vec<u8> {
let major = brand.major_brand();
let compat = brand.compatible_brands();
let mut body = Vec::with_capacity(8 + 4 * compat.len());
body.extend_from_slice(&major);
let minor: u32 = match brand {
BrandPreset::Mp4 => 0x0000_0200,
_ => 0,
};
body.extend_from_slice(&minor.to_be_bytes());
for b in &compat {
body.extend_from_slice(b);
}
wrap_box(b"ftyp", &body)
}
fn build_styp(brand: &BrandPreset) -> Vec<u8> {
let major = brand.major_brand();
let compat = brand.compatible_brands();
let mut body = Vec::with_capacity(8 + 4 * compat.len());
body.extend_from_slice(&major);
body.extend_from_slice(&0u32.to_be_bytes()); for b in &compat {
body.extend_from_slice(b);
}
wrap_box(b"styp", &body)
}
fn build_init_moov(
tracks: &[FragTrackState],
track_edit_lists: &[crate::options::TrackEditList],
levels: &[crate::demux::LevaEntry],
treps: &[crate::demux::TrepRecord],
pssh: &[crate::cenc::PsshBox],
write_mehd: bool,
) -> Result<(Vec<u8>, Option<usize>)> {
let movie_timescale: u32 = 1000;
let mut moov_body = Vec::new();
moov_body.extend_from_slice(&build_mvhd(movie_timescale, 0, (tracks.len() as u32) + 1));
for t in tracks {
let explicit_elst = track_edit_lists
.iter()
.find(|e| e.stream_index as u32 + 1 == t.track_id)
.map(|e| e.entries.as_slice());
moov_body.extend_from_slice(&build_trak_init(
t.track_id,
&t.base,
movie_timescale,
explicit_elst,
)?);
}
let mvex_off_in_body = moov_body.len();
let (mvex, mehd_off_in_mvex) = build_mvex(tracks, levels, treps, write_mehd)?;
moov_body.extend_from_slice(&mvex);
for record in pssh {
moov_body.extend_from_slice(&crate::cenc::build_pssh_box(record)?);
}
let mehd_off_in_moov = mehd_off_in_mvex.map(|o| 8 + mvex_off_in_body + o);
Ok((wrap_box(b"moov", &moov_body), mehd_off_in_moov))
}
fn build_trak_init(
track_id: u32,
t: &TrackState,
movie_timescale: u32,
explicit_elst: Option<&[crate::demux::EditListEntry]>,
) -> Result<Vec<u8>> {
let mut body = Vec::new();
body.extend_from_slice(&build_tkhd(track_id, 0, &t.stream));
if let Some(entries) = explicit_elst {
let elst = crate::demux::build_elst_box(entries)?;
body.extend_from_slice(&wrap_box(b"edts", &elst));
}
body.extend_from_slice(&build_mdia(t)?);
let _ = movie_timescale;
Ok(wrap_box(b"trak", &body))
}
fn build_mvex(
tracks: &[FragTrackState],
levels: &[crate::demux::LevaEntry],
treps: &[crate::demux::TrepRecord],
write_mehd: bool,
) -> Result<(Vec<u8>, Option<usize>)> {
let mut body = Vec::new();
let mut mehd_field_off = None;
if write_mehd {
mehd_field_off = Some(8 + 8 + 4);
let mut mehd = Vec::with_capacity(12);
mehd.push(1); mehd.extend_from_slice(&[0u8; 3]); mehd.extend_from_slice(&0u64.to_be_bytes()); body.extend_from_slice(&wrap_box(b"mehd", &mehd));
}
for t in tracks {
body.extend_from_slice(&build_trex(
t.track_id,
t.trex_default_sample_duration,
t.trex_default_sample_size,
t.trex_default_sample_flags,
));
}
if !levels.is_empty() {
let record = crate::demux::LevaRecord {
entries: levels.to_vec(),
};
body.extend_from_slice(&crate::demux::build_leva_box(&record)?);
}
for trep in treps {
body.extend_from_slice(&crate::demux::build_trep_box(trep)?);
}
Ok((wrap_box(b"mvex", &body), mehd_field_off))
}
fn build_trex(track_id: u32, ddur: u32, dsiz: u32, dflg: u32) -> Vec<u8> {
let mut body = Vec::with_capacity(24);
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&track_id.to_be_bytes());
body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&ddur.to_be_bytes());
body.extend_from_slice(&dsiz.to_be_bytes());
body.extend_from_slice(&dflg.to_be_bytes());
wrap_box(b"trex", &body)
}
fn build_moof(
seq: u32,
tracks: &[FragTrackState],
pssh: &[crate::cenc::PsshBox],
) -> Result<Vec<u8>> {
let placeholder = build_moof_inner(seq, tracks, pssh, |_track_idx, _byte_in_mdat| 0)?;
let moof_size = placeholder.len() as u64;
let mdat_header_size: u64 = 8;
let final_moof = build_moof_inner(seq, tracks, pssh, |_track_idx, byte_in_mdat| {
(moof_size + mdat_header_size + byte_in_mdat) as i32
})?;
debug_assert_eq!(final_moof.len() as u64, moof_size, "moof size shifted");
Ok(final_moof)
}
fn build_moof_inner<F>(
seq: u32,
tracks: &[FragTrackState],
pssh: &[crate::cenc::PsshBox],
offset_fn: F,
) -> Result<Vec<u8>>
where
F: Fn(usize, u64) -> i32,
{
let mut moof_body = Vec::new();
moof_body.extend_from_slice(&build_mfhd(seq));
for record in pssh {
moof_body.extend_from_slice(&crate::cenc::build_pssh_box(record)?);
}
let mut byte_in_mdat: u64 = 0;
for (i, t) in tracks.iter().enumerate() {
if t.pending.is_empty() {
continue;
}
let track_first_byte = byte_in_mdat;
let trun_data_offset = offset_fn(i, track_first_byte);
let traf = build_traf(t, trun_data_offset)?;
let traf_pos_in_body = moof_body.len();
moof_body.extend_from_slice(&traf.bytes);
if let Some(patch) = traf.saio_patch {
let absolute = 8u64 + traf_pos_in_body as u64 + patch.aux_data_pos as u64;
let value = u32::try_from(absolute).map_err(|_| {
Error::invalid("MP4: saio offset exceeds u32 (moof too large for v0 saio)")
})?;
let field = traf_pos_in_body + patch.field_pos;
moof_body[field..field + 4].copy_from_slice(&value.to_be_bytes());
}
for s in &t.pending {
byte_in_mdat += s.data.len() as u64;
}
}
Ok(wrap_box(b"moof", &moof_body))
}
struct SaioPatch {
field_pos: usize,
aux_data_pos: usize,
}
struct TrafBuild {
bytes: Vec<u8>,
saio_patch: Option<SaioPatch>,
}
fn build_mfhd(seq: u32) -> Vec<u8> {
let mut body = Vec::with_capacity(8);
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&seq.to_be_bytes());
wrap_box(b"mfhd", &body)
}
fn build_traf(t: &FragTrackState, trun_data_offset: i32) -> Result<TrafBuild> {
let defaults = FragmentDefaults::for_track(t);
let mut body = Vec::new();
body.extend_from_slice(&build_tfhd(t, defaults));
body.extend_from_slice(&build_tfdt(t.next_bmdt));
let saio_patch = append_traf_cenc_boxes(t, &mut body)?;
append_traf_seig_groups(t, &mut body)?;
body.extend_from_slice(&build_trun(t, trun_data_offset, defaults));
Ok(TrafBuild {
bytes: wrap_box(b"traf", &body),
saio_patch: saio_patch.map(|p| SaioPatch {
field_pos: p.field_pos + 8,
aux_data_pos: p.aux_data_pos + 8,
}),
})
}
fn append_traf_cenc_boxes(t: &FragTrackState, body: &mut Vec<u8>) -> Result<Option<SaioPatch>> {
if t.pending_senc.is_empty() {
return Ok(None);
}
if t.pending_senc.len() != t.pending.len() {
return Err(Error::invalid(
"MP4: pending senc entries out of step with pending samples",
));
}
let any_subsamples = t.pending_senc.iter().any(|s| !s.subsamples.is_empty());
let iv_size = t
.pending_senc
.first()
.map(|s| s.initialization_vector.len())
.unwrap_or(0);
if iv_size == 0 && !any_subsamples {
return Ok(None);
}
let mut sizes: Vec<u64> = Vec::with_capacity(t.pending_senc.len());
for s in &t.pending_senc {
let mut sz = s.initialization_vector.len() as u64;
if any_subsamples {
sz += 2 + 6 * s.subsamples.len() as u64;
}
sizes.push(sz);
}
for (i, &sz) in sizes.iter().enumerate() {
if sz > u8::MAX as u64 {
return Err(Error::invalid(format!(
"MP4: sample {i} CENC auxiliary info is {sz} bytes — exceeds the 8-bit \
saiz sample_info_size field (§8.7.8.3)"
)));
}
}
let senc = crate::cenc::SencBox {
flags: if any_subsamples { 0x0000_0002 } else { 0 },
samples: t.pending_senc.clone(),
};
let senc_bytes = crate::cenc::build_senc_box(&senc)?;
let senc_pos = body.len();
body.extend_from_slice(&senc_bytes);
let aux_data_pos = senc_pos + 16;
let all_same = sizes.windows(2).all(|w| w[0] == w[1]);
let saiz_record = crate::demux::SaizBox {
aux_info_type: None,
aux_info_type_parameter: None,
default_sample_info_size: if all_same { sizes[0] as u8 } else { 0 },
sample_count: sizes.len() as u32,
per_sample: if all_same {
Vec::new()
} else {
sizes.iter().map(|&s| s as u8).collect()
},
};
let saiz_bytes = crate::demux::build_saiz_box(&saiz_record)
.ok_or_else(|| Error::invalid("MP4: saiz record failed to serialise"))?;
body.extend_from_slice(&saiz_bytes);
let saio_record = crate::demux::SaioBox {
version: 0,
aux_info_type: None,
aux_info_type_parameter: None,
offsets: vec![0],
};
let saio_bytes = crate::demux::build_saio_box(&saio_record)
.ok_or_else(|| Error::invalid("MP4: saio record failed to serialise"))?;
let saio_pos = body.len();
body.extend_from_slice(&saio_bytes);
let field_pos = saio_pos + 16;
Ok(Some(SaioPatch {
field_pos,
aux_data_pos,
}))
}
fn append_traf_seig_groups(t: &FragTrackState, body: &mut Vec<u8>) -> Result<()> {
if t.pending_seig.iter().all(|s| s.is_none()) {
return Ok(());
}
if t.pending_seig.len() != t.pending.len() {
return Err(Error::invalid(
"MP4: pending seig overrides out of step with pending samples",
));
}
let mut uniques: Vec<&crate::cenc::SeigEntry> = Vec::new();
let mut indices: Vec<u32> = Vec::with_capacity(t.pending_seig.len());
for seig in &t.pending_seig {
match seig {
None => indices.push(0),
Some(entry) => {
let k = match uniques.iter().position(|u| *u == entry) {
Some(k) => k,
None => {
uniques.push(entry);
uniques.len() - 1
}
};
indices.push(0x10001 + k as u32);
}
}
}
let mut sgpd_entries: Vec<Vec<u8>> = Vec::with_capacity(uniques.len());
for entry in &uniques {
sgpd_entries.push(crate::cenc::build_seig_entry(entry)?);
}
let sgpd = crate::sample_groups::SampleGroupDescription {
grouping_type: *b"seig",
default_sample_description_index: None,
entries: sgpd_entries,
};
body.extend_from_slice(&crate::sample_groups::build_sgpd(&sgpd));
let mut entries: Vec<(u32, u32)> = Vec::new();
for &idx in &indices {
match entries.last_mut() {
Some((count, last)) if *last == idx => *count += 1,
_ => entries.push((1, idx)),
}
}
let sbgp = crate::sample_groups::SampleToGroup {
grouping_type: *b"seig",
grouping_type_parameter: None,
entries,
};
body.extend_from_slice(&crate::sample_groups::build_sbgp(&sbgp));
Ok(())
}
const TFHD_BASE_DATA_OFFSET_PRESENT: u32 = 0x000001;
const TFHD_SAMPLE_DESCRIPTION_INDEX_PRESENT: u32 = 0x000002;
const TFHD_DEFAULT_SAMPLE_DURATION_PRESENT: u32 = 0x000008;
const TFHD_DEFAULT_SAMPLE_SIZE_PRESENT: u32 = 0x000010;
const TFHD_DEFAULT_SAMPLE_FLAGS_PRESENT: u32 = 0x000020;
const TFHD_DEFAULT_BASE_IS_MOOF: u32 = 0x020000;
#[derive(Clone, Copy)]
struct FragmentDefaults {
homogeneous_size: Option<u32>,
homogeneous_duration: Option<u32>,
homogeneous_flags: Option<u32>,
first_sample_distinct: bool,
first_sample_flags: u32,
}
impl FragmentDefaults {
fn for_track(t: &FragTrackState) -> Self {
let homogeneous_size = t
.pending
.first()
.map(|s| s.data.len() as u32)
.filter(|&sz| t.pending.iter().all(|s| s.data.len() as u32 == sz));
let homogeneous_duration = t
.pending
.first()
.map(|s| s.duration)
.filter(|&d| t.pending.iter().all(|s| s.duration == d));
let (homogeneous_flags, first_sample_distinct, first_sample_flags) = match t.pending.len() {
0 => (None, false, 0),
1 => (t.pending.first().map(|s| s.flags), false, 0),
_ => {
let all_same = t
.pending
.first()
.map(|s| s.flags)
.filter(|&f| t.pending.iter().all(|s| s.flags == f));
if all_same.is_some() {
(all_same, false, 0)
} else {
let tail_first = t.pending[1].flags;
let tail_same = t.pending[1..].iter().all(|s| s.flags == tail_first);
if tail_same {
(Some(tail_first), true, t.pending[0].flags)
} else {
(None, false, 0)
}
}
}
};
Self {
homogeneous_size,
homogeneous_duration,
homogeneous_flags,
first_sample_distinct,
first_sample_flags,
}
}
}
fn build_tfhd(t: &FragTrackState, defaults: FragmentDefaults) -> Vec<u8> {
let mut flags = TFHD_DEFAULT_BASE_IS_MOOF;
if defaults.homogeneous_duration.is_some() {
flags |= TFHD_DEFAULT_SAMPLE_DURATION_PRESENT;
}
if defaults.homogeneous_size.is_some() {
flags |= TFHD_DEFAULT_SAMPLE_SIZE_PRESENT;
}
if defaults.homogeneous_flags.is_some() {
flags |= TFHD_DEFAULT_SAMPLE_FLAGS_PRESENT;
}
let mut body = Vec::new();
body.push(0); body.extend_from_slice(&flags.to_be_bytes()[1..4]);
body.extend_from_slice(&t.track_id.to_be_bytes());
let _ = TFHD_BASE_DATA_OFFSET_PRESENT;
let _ = TFHD_SAMPLE_DESCRIPTION_INDEX_PRESENT;
if let Some(d) = defaults.homogeneous_duration {
body.extend_from_slice(&d.to_be_bytes());
}
if let Some(sz) = defaults.homogeneous_size {
body.extend_from_slice(&sz.to_be_bytes());
}
if let Some(f) = defaults.homogeneous_flags {
body.extend_from_slice(&f.to_be_bytes());
}
wrap_box(b"tfhd", &body)
}
fn build_prft(req: &PrftRequest) -> Vec<u8> {
let version: u8 = if req.force_v1 || req.media_time > u32::MAX as u64 {
1
} else {
0
};
let mut body = Vec::with_capacity(if version == 0 { 20 } else { 24 });
body.push(version);
let f = req.flags.to_be_bytes();
body.extend_from_slice(&[f[1], f[2], f[3]]);
body.extend_from_slice(&req.reference_track_id.to_be_bytes());
body.extend_from_slice(&req.ntp_timestamp.to_be_bytes());
if version == 0 {
body.extend_from_slice(&(req.media_time as u32).to_be_bytes());
} else {
body.extend_from_slice(&req.media_time.to_be_bytes());
}
wrap_box(b"prft", &body)
}
fn build_tfdt(bmdt: u64) -> Vec<u8> {
let mut body = Vec::with_capacity(12);
body.push(1); body.extend_from_slice(&[0u8; 3]);
body.extend_from_slice(&bmdt.to_be_bytes());
wrap_box(b"tfdt", &body)
}
const TRUN_DATA_OFFSET_PRESENT: u32 = 0x000001;
const TRUN_FIRST_SAMPLE_FLAGS_PRESENT: u32 = 0x000004;
const TRUN_SAMPLE_DURATION_PRESENT: u32 = 0x000100;
const TRUN_SAMPLE_SIZE_PRESENT: u32 = 0x000200;
const TRUN_SAMPLE_FLAGS_PRESENT: u32 = 0x000400;
const TRUN_SAMPLE_COMPOSITION_TIME_OFFSETS_PRESENT: u32 = 0x000800;
fn build_trun(t: &FragTrackState, data_offset: i32, defaults: FragmentDefaults) -> Vec<u8> {
let need_per_sample_dur = defaults.homogeneous_duration.is_none();
let need_per_sample_size = defaults.homogeneous_size.is_none();
let need_per_sample_flags = defaults.homogeneous_flags.is_none();
let need_cts = t.pending.iter().any(|s| s.composition_time_offset != 0);
let mut flags = TRUN_DATA_OFFSET_PRESENT;
if defaults.first_sample_distinct {
flags |= TRUN_FIRST_SAMPLE_FLAGS_PRESENT;
}
if need_per_sample_dur {
flags |= TRUN_SAMPLE_DURATION_PRESENT;
}
if need_per_sample_size {
flags |= TRUN_SAMPLE_SIZE_PRESENT;
}
if need_per_sample_flags {
flags |= TRUN_SAMPLE_FLAGS_PRESENT;
}
if need_cts {
flags |= TRUN_SAMPLE_COMPOSITION_TIME_OFFSETS_PRESENT;
}
let mut body = Vec::new();
body.push(1);
body.extend_from_slice(&flags.to_be_bytes()[1..4]);
body.extend_from_slice(&(t.pending.len() as u32).to_be_bytes());
body.extend_from_slice(&data_offset.to_be_bytes());
if defaults.first_sample_distinct {
body.extend_from_slice(&defaults.first_sample_flags.to_be_bytes());
}
for s in &t.pending {
if need_per_sample_dur {
body.extend_from_slice(&s.duration.to_be_bytes());
}
if need_per_sample_size {
body.extend_from_slice(&(s.data.len() as u32).to_be_bytes());
}
if need_per_sample_flags {
body.extend_from_slice(&s.flags.to_be_bytes());
}
if need_cts {
body.extend_from_slice(&s.composition_time_offset.to_be_bytes());
}
}
wrap_box(b"trun", &body)
}
fn build_sidx(
reference_id: u32,
timescale: u32,
earliest_presentation_time: u64,
referenced_size: u64,
subsegment_duration: u32,
starts_with_sap: bool,
) -> Vec<u8> {
let mut body = Vec::with_capacity(28 + 12);
body.push(1); body.extend_from_slice(&[0u8; 3]); body.extend_from_slice(&reference_id.to_be_bytes());
body.extend_from_slice(×cale.to_be_bytes());
body.extend_from_slice(&earliest_presentation_time.to_be_bytes());
body.extend_from_slice(&0u64.to_be_bytes()); body.extend_from_slice(&0u16.to_be_bytes()); body.extend_from_slice(&1u16.to_be_bytes()); let r0 = (referenced_size.min(0x7FFF_FFFF) as u32) & 0x7FFF_FFFF; body.extend_from_slice(&r0.to_be_bytes());
body.extend_from_slice(&subsegment_duration.to_be_bytes());
let sap_type: u32 = if starts_with_sap { 1 } else { 0 }; let r2 = (if starts_with_sap { 0x8000_0000u32 } else { 0 }) | (sap_type << 28);
body.extend_from_slice(&r2.to_be_bytes());
wrap_box(b"sidx", &body)
}
fn build_tfra(track_id: u32, entries: &[TfraEmitEntry]) -> Vec<u8> {
let mut body = Vec::with_capacity(16 + entries.len() * 19);
body.push(1); body.extend_from_slice(&[0u8; 3]); body.extend_from_slice(&track_id.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes());
body.extend_from_slice(&(entries.len() as u32).to_be_bytes());
for e in entries {
body.extend_from_slice(&e.time.to_be_bytes());
body.extend_from_slice(&e.moof_offset.to_be_bytes());
body.push(e.traf_number.min(u8::MAX as u32) as u8);
body.push(e.trun_number.min(u8::MAX as u32) as u8);
body.push(e.sample_number.min(u8::MAX as u32) as u8);
}
wrap_box(b"tfra", &body)
}
fn build_mfro(mfra_total_size: u32) -> Vec<u8> {
let mut body = Vec::with_capacity(8);
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&mfra_total_size.to_be_bytes());
wrap_box(b"mfro", &body)
}