use alloc::vec;
use alloc::vec::Vec;
use broadcast_common::{Package, Parse, Serialize};
use crate::error::{Error, Result};
use crate::init_segment::{
ChunkLargeOffsetBox, ChunkOffsetBox, MovieBox, SampleSizeBox, SampleToChunkBox, StblChild,
StscEntry, SyncSampleBox,
};
use crate::media::Media;
use crate::pipeline::{build_init_segment, Sample};
use crate::segments::{FileTypeBox, MediaDataBox};
use crate::timing::{CompositionOffsetBox, CttsEntry, SttsEntry, TimeToSampleBox};
const DEFAULT_MOVIE_TIMESCALE: u32 = 1000;
const FTYP_MAJOR_BRAND: [u8; 4] = *b"isom";
const FTYP_MINOR_VERSION: u32 = 512;
const MDAT_HEADER_LEN: usize = 8;
const SAMPLE_DESCRIPTION_INDEX: u32 = 1;
#[derive(Debug, Clone, Default)]
pub struct ProgressiveMux {
pub faststart: bool,
}
impl ProgressiveMux {
pub fn new(faststart: bool) -> Self {
Self { faststart }
}
}
fn build_stts(samples: &[Sample]) -> TimeToSampleBox {
let mut entries: Vec<SttsEntry> = Vec::new();
for s in samples {
match entries.last_mut() {
Some(last) if last.sample_delta == s.duration => last.sample_count += 1,
_ => entries.push(SttsEntry {
sample_count: 1,
sample_delta: s.duration,
}),
}
}
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.is_sync) {
return None;
}
let entries: Vec<u32> = samples
.iter()
.enumerate()
.filter_map(|(i, s)| if s.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],
chunk_offset: u64,
use_co64: bool,
) -> Vec<StblChild> {
let stsz = SampleSizeBox {
version: 0,
flags: 0,
sample_size: 0,
entries: samples.iter().map(|s| s.data.len() as u32).collect(),
};
let stsc = SampleToChunkBox {
version: 0,
flags: 0,
entries: vec![StscEntry {
first_chunk: 1,
samples_per_chunk: samples.len() as u32,
sample_description_index: SAMPLE_DESCRIPTION_INDEX,
}],
};
let mut children = vec![
stsd,
StblChild::Stts(build_stts(samples)),
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: vec![chunk_offset],
}));
} else {
children.push(StblChild::Stco(ChunkOffsetBox {
version: 0,
flags: 0,
entries: vec![chunk_offset as u32],
}));
}
if let Some(stss) = build_stss(samples) {
children.push(StblChild::Stss(stss));
}
children
}
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; set_track_durations(&mut moov, media, movie_timescale);
let mut mdat_payload: Vec<u8> = Vec::new();
let mut rel_chunk_offsets: Vec<u64> = Vec::with_capacity(media.tracks.len());
for track in &media.tracks {
rel_chunk_offsets.push(mdat_payload.len() as u64);
for s in &track.samples {
mdat_payload.extend_from_slice(&s.data);
}
}
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 = MediaDataBox { data: mdat_payload };
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 = self.assemble_moov(
&mut moov.clone(),
&stsds,
media,
&rel_chunk_offsets,
0,
use_co64,
)?;
let moov_size = provisional.len();
let mdat_payload_offset = if self.faststart {
(ftyp_len + moov_size + MDAT_HEADER_LEN) as u64
} else {
(ftyp_len + MDAT_HEADER_LEN) as u64
};
let needs_co64 = rel_chunk_offsets
.iter()
.any(|&rel| mdat_payload_offset + rel > u32::MAX as u64);
if needs_co64 && !use_co64 {
use_co64 = true;
continue;
}
let moov_out = self.assemble_moov(
&mut moov.clone(),
&stsds,
media,
&rel_chunk_offsets,
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 = if self.faststart {
(ftyp_len + moov_out.len() + MDAT_HEADER_LEN) as u64
} else {
(ftyp_len + MDAT_HEADER_LEN) as u64
};
debug_assert_eq!(actual_payload_offset, mdat_payload_offset);
let mut out = Vec::with_capacity(ftyp_len + moov_out.len() + mdat.serialized_len());
let mut ftyp_buf = vec![0u8; ftyp_len];
let n = ftyp.serialize_into(&mut ftyp_buf)?;
out.extend_from_slice(&ftyp_buf[..n]);
let mut mdat_buf = vec![0u8; mdat.serialized_len()];
let m = mdat.serialize_into(&mut mdat_buf)?;
if self.faststart {
out.extend_from_slice(&moov_out);
out.extend_from_slice(&mdat_buf[..m]);
} else {
out.extend_from_slice(&mdat_buf[..m]);
out.extend_from_slice(&moov_out);
}
Ok(out)
}
}
impl ProgressiveMux {
fn assemble_moov(
&self,
moov: &mut MovieBox,
stsds: &[StblChild],
media: &Media,
rel_chunk_offsets: &[u64],
mdat_payload_offset: u64,
use_co64: bool,
) -> Result<Vec<u8>> {
for (i, track) in media.tracks.iter().enumerate() {
let abs_offset = mdat_payload_offset + rel_chunk_offsets[i];
let children =
build_stbl_children(stsds[i].clone(), &track.samples, abs_offset, 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)
}
}
fn set_track_durations(moov: &mut MovieBox, media: &Media, movie_timescale: u32) {
let mut max_movie_duration = 0u64;
for (i, track) in media.tracks.iter().enumerate() {
let media_duration: u64 = track.samples.iter().map(|s| s.duration as u64).sum();
let ts = if track.timescale() == 0 {
1
} else {
track.timescale()
} as u64;
let movie_duration = media_duration * movie_timescale as u64 / ts;
if movie_duration > max_movie_duration {
max_movie_duration = movie_duration;
}
if let Some(trak) = moov.tracks.get_mut(i) {
trak.tkhd.duration = movie_duration;
if let Some(mdhd) = trak.mdia.as_mut().and_then(|m| m.mdhd.as_mut()) {
mdhd.duration = media_duration;
}
}
}
moov.mvhd.duration = max_movie_duration;
}
fn find_top_box<'a>(data: &'a [u8], fourcc: &[u8; 4]) -> Option<&'a [u8]> {
let mut offset = 0usize;
while offset + 8 <= data.len() {
let (bx, consumed) = crate::box_types::parse_box(&data[offset..]).ok()?;
if bx.header.box_type.is(fourcc) {
let end = if bx.header.size == 0 {
data.len()
} else {
offset + bx.header.size as usize
};
return Some(&data[offset..end]);
}
if consumed == 0 {
break;
}
offset += consumed;
}
None
}