use alloc::vec;
use alloc::vec::Vec;
use broadcast_common::{Package, Parse, Serialize};
use crate::box_types::{BoxHeader, BoxType, find_top_box};
use crate::error::{Error, Result};
use crate::init_segment::{
ChunkLargeOffsetBox, ChunkOffsetBox, EditBox, MovieBox, SampleSizeBox, SampleToChunkBox,
StblChild, StscEntry, SyncSampleBox,
};
use crate::media::{Media, TimelineOrigin, Track, relative_decode_times};
use crate::pipeline::{Sample, build_init_segment};
use crate::segments::FileTypeBox;
use crate::timing::{
CompositionOffsetBox, CttsEntry, EditListBox, EditListEntry, SttsEntry, TimeToSampleBox,
};
const DEFAULT_MOVIE_TIMESCALE: u32 = 1000;
const FTYP_MAJOR_BRAND: [u8; 4] = *b"isom";
const FTYP_MINOR_VERSION: u32 = 512;
const MDAT_TYPE: [u8; 4] = *b"mdat";
const SAMPLE_DESCRIPTION_INDEX: u32 = 1;
const ELST_EMPTY_EDIT: i64 = -1;
const ELST_NORMAL_RATE: i16 = 1;
#[derive(Debug, Clone, Default)]
pub struct ProgressiveMux {
pub faststart: bool,
}
impl ProgressiveMux {
pub fn new(faststart: bool) -> Self {
Self { faststart }
}
}
fn decode_deltas(samples: &[Sample], times: &[i64]) -> Vec<u32> {
samples
.iter()
.enumerate()
.map(|(i, s)| {
let own = s.duration.unwrap_or(0);
match (times.get(i), times.get(i + 1)) {
(Some(&now), Some(&next)) if next >= now => {
u32::try_from(next - now).unwrap_or(own)
}
_ => own,
}
})
.collect()
}
fn build_stts(deltas: &[u32]) -> TimeToSampleBox {
let mut entries: Vec<SttsEntry> = Vec::new();
for &delta in deltas {
match entries.last_mut() {
Some(last) if last.sample_delta == delta => last.sample_count += 1,
_ => entries.push(SttsEntry {
sample_count: 1,
sample_delta: delta,
}),
}
}
TimeToSampleBox {
version: 0,
flags: 0,
entries,
}
}
fn build_ctts(samples: &[Sample]) -> Option<CompositionOffsetBox> {
if samples.iter().all(|s| s.composition_offset() == 0) {
return None;
}
let mut entries: Vec<CttsEntry> = Vec::new();
for s in samples {
match entries.last_mut() {
Some(last) if last.sample_offset == s.composition_offset() => last.sample_count += 1,
_ => entries.push(CttsEntry {
sample_count: 1,
sample_offset: s.composition_offset(),
}),
}
}
Some(CompositionOffsetBox {
version: 1,
flags: 0,
entries,
})
}
fn build_stss(samples: &[Sample]) -> Option<SyncSampleBox> {
if samples.iter().all(|s| s.flags.is_sync) {
return None;
}
let entries: Vec<u32> = samples
.iter()
.enumerate()
.filter_map(|(i, s)| {
if s.flags.is_sync {
Some(i as u32 + 1)
} else {
None
}
})
.collect();
Some(SyncSampleBox {
version: 0,
flags: 0,
entries,
})
}
fn build_stbl_children(
stsd: StblChild,
samples: &[Sample],
deltas: &[u32],
chunk_offsets: &[u64],
samples_per_chunk: &[u32],
use_co64: bool,
) -> Result<Vec<StblChild>> {
let stsz_entries: Vec<u32> = samples.iter().map(|s| s.data.len() as u32).collect();
let stsz = SampleSizeBox {
version: 0,
flags: 0,
sample_size: 0,
sample_count: stsz_entries.len() as u32,
entries: stsz_entries,
};
let mut stsc_entries: Vec<StscEntry> = Vec::new();
for (i, &count) in samples_per_chunk.iter().enumerate() {
let first_chunk = broadcast_common::len::fit_u32(i + 1, "first_chunk")?;
match stsc_entries.last_mut() {
Some(last) if last.samples_per_chunk == count => {}
_ => stsc_entries.push(StscEntry {
first_chunk,
samples_per_chunk: count,
sample_description_index: SAMPLE_DESCRIPTION_INDEX,
}),
}
}
let stsc = SampleToChunkBox {
version: 0,
flags: 0,
entries: stsc_entries,
};
let mut children = vec![
stsd,
StblChild::Stts(build_stts(deltas)),
StblChild::Stsc(stsc),
StblChild::Stsz(stsz),
];
if let Some(ctts) = build_ctts(samples) {
children.insert(2, StblChild::Ctts(ctts));
}
if use_co64 {
children.push(StblChild::Co64(ChunkLargeOffsetBox {
version: 0,
flags: 0,
entries: chunk_offsets.to_vec(),
}));
} else {
let entries: Vec<u32> = chunk_offsets
.iter()
.map(|&o| {
u32::try_from(o).map_err(|_| {
Error::InvalidInput("progressive: a chunk offset exceeds the 32-bit stco field")
})
})
.collect::<Result<_>>()?;
children.push(StblChild::Stco(ChunkOffsetBox {
version: 0,
flags: 0,
entries,
}));
}
if let Some(stss) = build_stss(samples) {
children.push(StblChild::Stss(stss));
}
Ok(children)
}
fn interleave_order(pending: Vec<(i64, usize, usize, usize)>) -> Vec<(i64, usize, usize, usize)> {
let mut per_track: alloc::collections::BTreeMap<usize, Vec<(i64, usize, usize, usize)>> =
alloc::collections::BTreeMap::new();
for entry in pending {
per_track.entry(entry.1).or_default().push(entry);
}
let mut heads: alloc::vec::Vec<usize> = alloc::vec![0; per_track.len()];
let mut out = Vec::new();
loop {
let mut best: Option<(usize, i64)> = None;
for (&track, chunks) in &per_track {
let Some(head) = chunks.get(heads[track]) else {
continue;
};
if best.is_none_or(|(_, tick)| head.0 < tick) {
best = Some((track, head.0));
}
}
let Some((track, _)) = best else {
break;
};
out.push(per_track[&track][heads[track]]);
heads[track] += 1;
}
out
}
struct Chunk {
first_sample: usize,
sample_count: u32,
rel_offset: u64,
}
struct MdatLayout {
track_chunks: Vec<Vec<Chunk>>,
payload_len: u64,
}
const INTERLEAVE_CHUNK_MS: u64 = 500;
const MS_PER_SECOND: u64 = 1_000;
fn plan_mdat_layout(media: &Media, movie_timescale: u32) -> Result<MdatLayout> {
let mts = i128::from(movie_timescale.max(1));
let to_movie = |ticks: i64, ts: u32| -> i64 {
(i128::from(ticks) * mts).div_euclid(i128::from(ts.max(1))) as i64
};
let chunk_ticks = i128::from(movie_timescale.max(1)) * i128::from(INTERLEAVE_CHUNK_MS)
/ i128::from(MS_PER_SECOND);
let chunk_ticks = i64::try_from(chunk_ticks).unwrap_or(i64::MAX).max(1);
let mut track_chunks: Vec<Vec<Chunk>> = Vec::with_capacity(media.tracks.len());
let mut pending: Vec<(i64, usize, usize, usize)> = Vec::new();
for (i, track) in media.tracks.iter().enumerate() {
let times = relative_decode_times(track, &TimelineOrigin::of(&media.tracks));
let mut chunks: Vec<Chunk> = Vec::new();
let mut start = 0usize;
while start < track.samples.len() {
let chunk_start_tick = to_movie(times[start], track.timescale());
let mut end = start;
while end < track.samples.len()
&& to_movie(times[end], track.timescale()) - chunk_start_tick < chunk_ticks
{
end += 1;
}
let count = end - start;
pending.push((chunk_start_tick, i, start, count));
chunks.push(Chunk {
first_sample: start,
sample_count: broadcast_common::len::fit_u32(count, "samples_per_chunk")?,
rel_offset: 0,
});
start = end;
}
track_chunks.push(chunks);
}
pending = interleave_order(pending);
let mut payload_len = 0u64;
let mut cursor: Vec<usize> = alloc::vec![0; track_chunks.len()];
for (_, track, first, count) in pending {
let index = cursor[track];
let chunk = track_chunks[track]
.get_mut(index)
.ok_or(Error::InvalidInput(
"progressive: chunk plan is inconsistent",
))?;
if chunk.first_sample != first {
return Err(Error::InvalidInput(
"progressive: chunk plan is out of order",
));
}
cursor[track] += 1;
chunk.rel_offset = payload_len;
for s in &media.tracks[track].samples[first..first + count] {
payload_len = payload_len.saturating_add(s.data.len() as u64);
}
}
Ok(MdatLayout {
track_chunks,
payload_len,
})
}
fn set_track_stbl(
moov: &mut MovieBox,
track_index: usize,
new_children: Vec<StblChild>,
) -> Result<()> {
let trak = moov
.tracks
.get_mut(track_index)
.ok_or(Error::UnexpectedBox { expected: "trak" })?;
let stbl = trak
.mdia
.as_mut()
.and_then(|m| m.minf.as_mut())
.and_then(|m| m.stbl.as_mut())
.ok_or(Error::UnexpectedBox { expected: "stbl" })?;
stbl.children = new_children;
Ok(())
}
fn take_stsd(moov: &MovieBox, track_index: usize) -> Result<StblChild> {
let trak = moov
.tracks
.get(track_index)
.ok_or(Error::UnexpectedBox { expected: "trak" })?;
let stbl = trak
.mdia
.as_ref()
.and_then(|m| m.minf.as_ref())
.and_then(|m| m.stbl.as_ref())
.ok_or(Error::UnexpectedBox { expected: "stbl" })?;
stbl.children
.iter()
.find(|c| matches!(c, StblChild::Stsd(_)))
.cloned()
.ok_or(Error::UnexpectedBox { expected: "stsd" })
}
impl Package for ProgressiveMux {
type Media = Media;
type Output = Vec<u8>;
type Error = Error;
fn package(&mut self, media: &Media) -> Result<Vec<u8>> {
if media.tracks.is_empty() {
return Err(Error::InvalidInput("cannot package a Media with no tracks"));
}
let movie_timescale = if media.movie_timescale == 0 {
DEFAULT_MOVIE_TIMESCALE
} else {
media.movie_timescale
};
let specs: Vec<_> = media.tracks.iter().map(|t| t.spec.clone()).collect();
let init = build_init_segment(&specs, movie_timescale)?;
let moov_bytes =
find_top_box(&init, b"moov").ok_or(Error::UnexpectedBox { expected: "moov" })?;
let mut moov = MovieBox::parse(moov_bytes)?;
moov.mvex = None; let timings = track_timings(media, movie_timescale);
apply_track_timings(&mut moov, &timings);
let layout = plan_mdat_layout(media, movie_timescale)?;
let ftyp = FileTypeBox {
major_brand: FTYP_MAJOR_BRAND,
minor_version: FTYP_MINOR_VERSION,
compatible_brands: vec![*b"isom", *b"iso2", *b"mp41", *b"avc1"],
};
let ftyp_len = ftyp.serialized_len();
let mdat_len = layout.payload_len;
let mdat_header = BoxHeader::for_payload(BoxType::from_bytes(MDAT_TYPE), None, mdat_len);
let stsds: Vec<StblChild> = (0..media.tracks.len())
.map(|i| take_stsd(&moov, i))
.collect::<Result<_>>()?;
let mut use_co64 = false;
let (moov_out, mdat_payload_offset) = loop {
let provisional = assemble_moov(
&mut moov.clone(),
&stsds,
&media.tracks,
&timings,
&layout,
0,
use_co64,
)?;
let moov_size = provisional.len();
let mdat_payload_offset =
mdat_payload_offset(ftyp_len as u64, moov_size as u64, mdat_len, self.faststart);
if needs_co64(mdat_payload_offset, &layout) && !use_co64 {
use_co64 = true;
continue;
}
let moov_out = assemble_moov(
&mut moov.clone(),
&stsds,
&media.tracks,
&timings,
&layout,
mdat_payload_offset,
use_co64,
)?;
debug_assert_eq!(moov_out.len(), moov_size, "moov size must be offset-stable");
break (moov_out, mdat_payload_offset);
};
let actual_payload_offset = ftyp_len as u64
+ if self.faststart {
moov_out.len() as u64
} else {
0
}
+ mdat_header.header_size() as u64;
debug_assert_eq!(actual_payload_offset, mdat_payload_offset);
let payload_len = usize::try_from(mdat_len)
.map_err(|_| Error::InvalidInput("progressive: mdat payload exceeds usize"))?;
let mdat_header_len = mdat_header.header_size();
let total = ftyp_len + moov_out.len() + mdat_header_len + payload_len;
let mut out = vec![0u8; ftyp_len];
ftyp.serialize_into(&mut out)?;
out.reserve_exact(total - ftyp_len);
if self.faststart {
out.extend_from_slice(&moov_out);
}
let header_at = out.len();
out.resize(header_at + mdat_header_len, 0);
mdat_header.serialize_into(&mut out[header_at..])?;
let payload_at = out.len();
out.resize(payload_at + payload_len, 0);
for (i, track) in media.tracks.iter().enumerate() {
for chunk in &layout.track_chunks[i] {
let first = chunk.first_sample;
let end = first + chunk.sample_count as usize;
let mut at = payload_at
+ usize::try_from(chunk.rel_offset).map_err(|_| {
Error::InvalidInput("progressive: chunk offset exceeds usize")
})?;
for s in &track.samples[first..end] {
let next = at + s.data.len();
if next > payload_at + payload_len {
return Err(Error::InvalidInput(
"progressive: chunk plan overflows the mdat payload",
));
}
out[at..next].copy_from_slice(&s.data);
at = next;
}
}
}
if !self.faststart {
out.extend_from_slice(&moov_out);
}
Ok(out)
}
}
fn mdat_payload_offset(
ftyp_len: u64,
moov_len: u64,
mdat_payload_len: u64,
faststart: bool,
) -> u64 {
let before = if faststart {
ftyp_len + moov_len
} else {
ftyp_len
};
let header = BoxHeader::for_payload(BoxType::from_bytes(MDAT_TYPE), None, mdat_payload_len);
before + header.header_size() as u64
}
fn needs_co64(mdat_payload_offset: u64, layout: &MdatLayout) -> bool {
layout
.track_chunks
.iter()
.flatten()
.any(|c| mdat_payload_offset.saturating_add(c.rel_offset) > u64::from(u32::MAX))
}
fn assemble_moov(
moov: &mut MovieBox,
stsds: &[StblChild],
tracks: &[Track],
timings: &[TrackTiming],
layout: &MdatLayout,
mdat_payload_offset: u64,
use_co64: bool,
) -> Result<Vec<u8>> {
for (i, track) in tracks.iter().enumerate() {
let offsets: Vec<u64> = layout.track_chunks[i]
.iter()
.map(|c| mdat_payload_offset + c.rel_offset)
.collect();
let counts: Vec<u32> = layout.track_chunks[i]
.iter()
.map(|c| c.sample_count)
.collect();
let children = build_stbl_children(
stsds[i].clone(),
&track.samples,
&timings[i].deltas,
&offsets,
&counts,
use_co64,
)?;
set_track_stbl(moov, i, children)?;
}
let mut buf = vec![0u8; moov.serialized_len()];
let n = moov.serialize_into(&mut buf)?;
buf.truncate(n);
Ok(buf)
}
struct TrackTiming {
deltas: Vec<u32>,
media_duration: u64,
presentation_duration: u64,
edts: Option<EditBox>,
}
fn track_timings(media: &Media, movie_timescale: u32) -> Vec<TrackTiming> {
let origin = TimelineOrigin::of(&media.tracks);
let mts = i128::from(movie_timescale.max(1));
let to_movie = |ticks: i64, ts: u32| (ticks as i128 * mts).div_euclid(i128::from(ts.max(1)));
struct Span {
times: Vec<i64>,
pres: Option<(i64, i64)>,
}
let spans: Vec<Span> = media
.tracks
.iter()
.map(|t| {
let times = relative_decode_times(t, &origin);
let pres = t.samples.iter().zip(×).fold(None, |acc, (s, &d)| {
let p = d.saturating_add(i64::from(s.composition_offset()));
let e = p.saturating_add(i64::from(s.duration.unwrap_or(0)));
Some(match acc {
None => (p, e),
Some((lo, hi)) => (p.min(lo), e.max(hi)),
})
});
Span { times, pres }
})
.collect();
let movie_start = media
.tracks
.iter()
.zip(&spans)
.filter_map(|(t, sp)| sp.pres.map(|(p, _)| to_movie(p, t.timescale())))
.min()
.unwrap_or(0);
media
.tracks
.iter()
.zip(spans)
.map(|(track, sp)| {
let ts = track.timescale();
let deltas = decode_deltas(&track.samples, &sp.times);
let media_duration: u64 = deltas.iter().map(|&d| u64::from(d)).sum();
let plain_duration = (i128::from(media_duration) * mts / i128::from(ts.max(1))) as u64;
let (Some((pres_start, pres_end)), Some(&first_dts)) = (sp.pres, sp.times.first())
else {
return TrackTiming {
deltas,
media_duration,
presentation_duration: plain_duration,
edts: None,
};
};
let empty = (to_movie(pres_start, ts) - movie_start).max(0) as u64;
let media_time = (pres_start - first_dts).max(0);
let segment_duration =
(to_movie(pres_end, ts) - to_movie(pres_start, ts)).max(0) as u64;
if empty == 0 && media_time == 0 {
return TrackTiming {
deltas,
media_duration,
presentation_duration: plain_duration,
edts: None,
};
}
let mut entries = Vec::with_capacity(2);
if empty > 0 {
entries.push(EditListEntry {
segment_duration: empty,
media_time: ELST_EMPTY_EDIT,
media_rate_integer: ELST_NORMAL_RATE,
media_rate_fraction: 0,
});
}
entries.push(EditListEntry {
segment_duration,
media_time,
media_rate_integer: ELST_NORMAL_RATE,
media_rate_fraction: 0,
});
let wide = entries.iter().any(|e| {
u32::try_from(e.segment_duration).is_err() || i32::try_from(e.media_time).is_err()
});
TrackTiming {
deltas,
media_duration,
presentation_duration: empty.saturating_add(segment_duration),
edts: Some(EditBox::new(
Some(EditListBox {
version: u8::from(wide),
flags: 0,
entries,
}),
Vec::new(),
)),
}
})
.collect()
}
fn apply_track_timings(moov: &mut MovieBox, timings: &[TrackTiming]) {
let mut max_movie_duration = 0u64;
for (i, timing) in timings.iter().enumerate() {
max_movie_duration = max_movie_duration.max(timing.presentation_duration);
if let Some(trak) = moov.tracks.get_mut(i) {
trak.tkhd.version = if duration_fits_v0(timing.presentation_duration) {
0
} else {
1
};
trak.tkhd.duration = timing.presentation_duration;
trak.edts = timing.edts.clone();
if let Some(mdhd) = trak.mdia.as_mut().and_then(|m| m.mdhd.as_mut()) {
mdhd.version = if duration_fits_v0(timing.media_duration) {
0
} else {
1
};
mdhd.duration = timing.media_duration;
}
}
}
moov.mvhd.version = if duration_fits_v0(max_movie_duration) {
0
} else {
1
};
moov.mvhd.duration = max_movie_duration;
}
fn duration_fits_v0(duration: u64) -> bool {
duration <= u64::from(u32::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interleave_order_keeps_a_track_in_sample_order() {
let pending = alloc::vec![
(0i64, 0usize, 0usize, 1usize),
(1000, 0, 1, 1),
(2000, 0, 2, 1),
(500, 0, 3, 1),
(3000, 0, 4, 1),
(500, 1, 0, 1),
(1500, 1, 1, 1),
];
let ordered = interleave_order(pending);
let mut cursor = [0usize; 2];
for &(_, track, first, _) in &ordered {
assert_eq!(
first, cursor[track],
"track {track} chunk arrived out of sample order: {ordered:?}"
);
cursor[track] += 1;
}
assert_eq!(cursor, [5, 2], "every chunk was consumed");
let tracks: alloc::vec::Vec<usize> = ordered.iter().map(|&(_, t, ..)| t).collect();
assert_eq!(
tracks,
alloc::vec![0, 1, 0, 1, 0, 0, 0],
"tracks interleave by tick: {ordered:?}"
);
}
#[test]
fn mdat_payload_offset_accounts_for_largesize_header() {
let fits = u64::from(u32::MAX) - 8; let over = fits + 1; assert_eq!(mdat_payload_offset(24, 1000, fits, true), 24 + 1000 + 8);
assert_eq!(mdat_payload_offset(24, 1000, fits, false), 24 + 8);
assert_eq!(mdat_payload_offset(24, 1000, over, true), 24 + 1000 + 16);
assert_eq!(mdat_payload_offset(24, 1000, over, false), 24 + 16);
}
#[test]
fn co64_chosen_once_an_offset_exceeds_u32() {
let base = 1024;
let layout = |offsets: &[u64]| MdatLayout {
track_chunks: alloc::vec![
offsets
.iter()
.map(|&rel_offset| Chunk {
first_sample: 0,
sample_count: 1,
rel_offset,
})
.collect()
],
payload_len: 0,
};
assert!(!needs_co64(base, &layout(&[0, u64::from(u32::MAX) - base])));
assert!(needs_co64(
base,
&layout(&[0, u64::from(u32::MAX) - base + 1])
));
}
}