use alloc::string::{String, ToString};
use alloc::vec::Vec;
use broadcast_common::error::{BufferTooShort, InvalidValue};
use broadcast_common::len::need_in;
use core::marker::PhantomData;
use broadcast_common::{Demand, Parse, Stage, Timestamp, Unpackage};
use crate::error::{Error, Result};
use crate::init_segment::{
ChunkLargeOffsetBox, ChunkOffsetBox, MovieBox, SampleSizeBox, SampleToChunkBox, StblChild,
SyncSampleBox, TrackBox,
};
use crate::media::{
Media, SkippedTrack, Track, find_top_box, normalise_track_nal_lengths, refine_legacy_config,
skipped_track, track_spec_from_trak,
};
use crate::pipeline::Sample;
use crate::timing::{CompositionOffsetBox, TimeToSampleBox};
#[derive(Debug, Clone)]
pub struct ProgressiveDemux<'a> {
_marker: PhantomData<&'a [u8]>,
buf: Vec<u8>,
media: Option<Media>,
finished: bool,
poisoned: bool,
max_bytes: usize,
}
impl ProgressiveDemux<'_> {
pub fn new(max_bytes: usize) -> Result<Self> {
if max_bytes == 0 {
return Err(Error::InvalidInput(
"ProgressiveDemux::new: max_bytes must be non-zero — a zero cap can never accept \
any bytes and would permanently wedge the Stage",
));
}
Ok(Self {
_marker: PhantomData,
buf: Vec::new(),
media: None,
finished: false,
poisoned: false,
max_bytes,
})
}
fn poison_error(&self) -> Error {
Error::BufferCapExceeded {
what: "ProgressiveDemux Stage buffer",
cap: self.max_bytes,
}
}
}
impl<'a> Unpackage for ProgressiveDemux<'a> {
type Input = &'a [u8];
type Media = Media;
type Error = Error;
fn unpackage(&mut self, input: &'a [u8]) -> Result<Media> {
demux_progressive(input)
}
}
fn demux_progressive(input: &[u8]) -> Result<Media> {
let moov_bytes =
find_top_box(input, b"moov").ok_or(Error::UnexpectedBox { expected: "moov" })?;
let moov = MovieBox::parse(moov_bytes)?;
let movie_timescale = moov.mvhd.timescale;
let mut tracks = Vec::with_capacity(moov.tracks.len());
let mut skipped: Vec<SkippedTrack> = Vec::new();
for trak in &moov.tracks {
let mut spec = match track_spec_from_trak(trak) {
Ok(spec) => spec,
Err(err) => {
skipped.push(skipped_track(err, trak.tkhd.track_id));
continue;
}
};
let mut samples = match samples_from_stbl(input, trak) {
Ok(samples) => samples,
Err(err) => {
skipped.push(SkippedTrack {
fourcc: String::from("unknown"),
reason: err.to_string(),
track_id: Some(trak.tkhd.track_id),
});
continue;
}
};
refine_legacy_config(&mut spec.config, &samples);
if let Err(err) = normalise_track_nal_lengths(&mut spec, &mut samples) {
skipped.push(SkippedTrack {
fourcc: String::from("unknown"),
reason: err.to_string(),
track_id: Some(trak.tkhd.track_id),
});
continue;
}
tracks.push(Track::new(spec, samples));
}
let mut media = Media::new(tracks, movie_timescale);
media.skipped = skipped;
Ok(media)
}
impl Stage for ProgressiveDemux<'_> {
type In<'a> = &'a [u8];
type Out = Media;
type Error = Error;
fn feed(&mut self, input: &[u8], _now: Timestamp) -> Result<()> {
if self.poisoned {
return Err(self.poison_error());
}
if self.finished {
return Err(Error::InvalidInput(
"ProgressiveDemux::feed after finish: the whole-file parse has already run, so \
these bytes would never be parsed",
));
}
let new_len = self.buf.len().saturating_add(input.len());
if new_len > self.max_bytes {
self.poisoned = true;
return Err(self.poison_error());
}
self.buf.extend_from_slice(input);
Ok(())
}
fn poll(&mut self) -> Option<Media> {
self.media.take()
}
fn finish(&mut self) -> Result<()> {
if self.poisoned {
return Err(self.poison_error());
}
if self.finished {
return Ok(());
}
self.finished = true;
let media = demux_progressive(&self.buf);
self.buf = Vec::new();
self.media = Some(media?);
Ok(())
}
fn demand(&self) -> Demand {
if self.poisoned || self.finished {
return Demand::saturated();
}
let remaining = self.max_bytes.saturating_sub(self.buf.len());
if remaining == 0 {
Demand::saturated()
} else {
Demand::new(remaining)
}
}
}
fn find_stbl_child<'a, T>(
children: &'a [StblChild],
fourcc: &[u8; 4],
variant: impl Fn(&'a StblChild) -> Option<&'a T>,
reparse_err: impl Fn(&'a [u8]) -> Error,
) -> Result<Option<&'a T>> {
for child in children {
if let Some(t) = variant(child) {
return Ok(Some(t));
}
}
for child in children {
if let StblChild::Opaque(bytes) = child
&& bytes.len() >= 8
&& &bytes[4..8] == fourcc
{
return Err(reparse_err(bytes));
}
}
Ok(None)
}
fn samples_from_stbl(file: &[u8], trak: &TrackBox) -> Result<Vec<Sample>> {
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" })?;
let stts = find_stbl_child(
&stbl.children,
b"stts",
|c| match c {
StblChild::Stts(b) => Some(b),
_ => None,
},
|bytes| TimeToSampleBox::parse(bytes).unwrap_err(),
)?
.ok_or(Error::UnexpectedBox { expected: "stts" })?;
let ctts = find_stbl_child(
&stbl.children,
b"ctts",
|c| match c {
StblChild::Ctts(b) => Some(b),
_ => None,
},
|bytes| CompositionOffsetBox::parse(bytes).unwrap_err(),
)?;
let stss = find_stbl_child(
&stbl.children,
b"stss",
|c| match c {
StblChild::Stss(b) => Some(b),
_ => None,
},
|bytes| SyncSampleBox::parse(bytes).unwrap_err(),
)?;
let stsz = find_stbl_child(
&stbl.children,
b"stsz",
|c| match c {
StblChild::Stsz(b) => Some(b),
_ => None,
},
|bytes| SampleSizeBox::parse(bytes).unwrap_err(),
)?
.ok_or(Error::UnexpectedBox { expected: "stsz" })?;
let stsc = find_stbl_child(
&stbl.children,
b"stsc",
|c| match c {
StblChild::Stsc(b) => Some(b),
_ => None,
},
|bytes| SampleToChunkBox::parse(bytes).unwrap_err(),
)?
.ok_or(Error::UnexpectedBox { expected: "stsc" })?;
let co64 = find_stbl_child(
&stbl.children,
b"co64",
|c| match c {
StblChild::Co64(b) => Some(b),
_ => None,
},
|bytes| ChunkLargeOffsetBox::parse(bytes).unwrap_err(),
)?;
let stco = find_stbl_child(
&stbl.children,
b"stco",
|c| match c {
StblChild::Stco(b) => Some(b),
_ => None,
},
|bytes| ChunkOffsetBox::parse(bytes).unwrap_err(),
)?;
let layout = sample_layout(stsz, stsc, co64, stco, file.len())?;
let total_samples = layout.len();
let durations = expand_stts(stts, total_samples)?;
let composition_offsets = expand_ctts(ctts, total_samples)?;
let sync_flags = expand_stss(stss, total_samples);
let mut samples = Vec::with_capacity(total_samples);
let mut next_dts: i64 = 0;
for i in 0..total_samples {
let (start, size) = layout[i];
let end = start
.checked_add(size)
.ok_or(Error::InvalidInput("sample byte range overflow"))?;
need_in(file, end, "progressive sample data")?;
let dts = next_dts;
samples.push(Sample::new(
file[start..end].to_vec(),
Some(dts),
Some(dts + composition_offsets[i] as i64),
Some(durations[i]),
sync_flags[i],
));
next_dts += durations[i] as i64;
}
Ok(samples)
}
#[cfg(test)]
thread_local! {
static LAYOUT_STEPS: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
}
pub(crate) fn sample_layout(
stsz: &SampleSizeBox,
stsc: &SampleToChunkBox,
co64: Option<&ChunkLargeOffsetBox>,
stco: Option<&ChunkOffsetBox>,
file_len: usize,
) -> Result<Vec<(usize, usize)>> {
let chunk_offsets = chunk_offsets(co64, stco)?;
let declared = stsz.sample_count as usize;
if declared > file_len {
return Err(Error::BufferTooShort(BufferTooShort {
need: declared,
have: file_len,
what: "stsz sample_count vs file length",
}));
}
let entries = &stsc.entries;
let mut prev_first = 0u32;
for e in entries {
if e.first_chunk <= prev_first {
return Err(Error::InvalidValue(InvalidValue {
field: "stsc.first_chunk",
value: u64::from(e.first_chunk),
reason: "first_chunk must be >= 1 and strictly ascending (ISO/IEC 14496-12 §8.7.4)",
}));
}
prev_first = e.first_chunk;
}
let mut layout = Vec::with_capacity(declared);
let mut run = 0usize;
for (c, &chunk_base) in chunk_offsets.iter().enumerate() {
if layout.len() == declared {
break;
}
#[cfg(test)]
LAYOUT_STEPS.with(|n| n.set(n.get() + 1));
let chunk_no = c as u64 + 1;
while run + 1 < entries.len() && u64::from(entries[run + 1].first_chunk) <= chunk_no {
#[cfg(test)]
LAYOUT_STEPS.with(|n| n.set(n.get() + 1));
run += 1;
}
let per_chunk = match entries.get(run) {
Some(e) if u64::from(e.first_chunk) <= chunk_no => e.samples_per_chunk,
_ => 0,
};
let mut cursor = chunk_base;
for _ in 0..per_chunk {
if layout.len() == declared {
break;
}
let size = sample_size(stsz, layout.len())?;
let start = usize::try_from(cursor)
.map_err(|_| Error::InvalidInput("chunk offset exceeds addressable range"))?;
layout.push((start, size));
cursor = cursor.saturating_add(size as u64);
}
}
if layout.len() < declared {
return Err(Error::InvalidInput(
"stsc/stco sample-to-chunk mapping did not cover all samples",
));
}
Ok(layout)
}
fn chunk_offsets(
co64: Option<&ChunkLargeOffsetBox>,
stco: Option<&ChunkOffsetBox>,
) -> Result<Vec<u64>> {
if let Some(co64) = co64 {
Ok(co64.entries.clone())
} else if let Some(stco) = stco {
Ok(stco.entries.iter().map(|&o| o as u64).collect())
} else {
Err(Error::UnexpectedBox {
expected: "stco or co64",
})
}
}
fn sample_size(stsz: &SampleSizeBox, index: usize) -> Result<usize> {
if stsz.sample_size != 0 {
Ok(stsz.sample_size as usize)
} else {
stsz.entries
.get(index)
.map(|&s| s as usize)
.ok_or(Error::InvalidInput("stsz has fewer entries than samples"))
}
}
fn expand_stts(stts: &TimeToSampleBox, total_samples: usize) -> Result<Vec<u32>> {
let mut out = Vec::with_capacity(total_samples);
for entry in &stts.entries {
if entry.sample_count as usize > total_samples - out.len() {
return Err(Error::InvalidInput(
"stts sample count does not match chunk layout",
));
}
for _ in 0..entry.sample_count {
out.push(entry.sample_delta);
}
}
if out.len() != total_samples {
return Err(Error::InvalidInput(
"stts sample count does not match chunk layout",
));
}
Ok(out)
}
fn expand_ctts(ctts: Option<&CompositionOffsetBox>, total_samples: usize) -> Result<Vec<i32>> {
let Some(ctts) = ctts else {
return Ok(alloc::vec![0i32; total_samples]);
};
let mut out = Vec::with_capacity(total_samples);
for entry in &ctts.entries {
if entry.sample_count as usize > total_samples - out.len() {
return Err(Error::InvalidInput(
"ctts sample count does not match chunk layout",
));
}
for _ in 0..entry.sample_count {
out.push(entry.sample_offset);
}
}
if out.len() != total_samples {
return Err(Error::InvalidInput(
"ctts sample count does not match chunk layout",
));
}
Ok(out)
}
fn expand_stss(stss: Option<&SyncSampleBox>, total_samples: usize) -> Vec<bool> {
let Some(stss) = stss else {
return alloc::vec![true; total_samples];
};
let mut flags = alloc::vec![false; total_samples];
for &one_based in &stss.entries {
let idx = one_based as usize;
if idx >= 1 && idx <= total_samples {
flags[idx - 1] = true;
}
}
flags
}
#[cfg(test)]
mod tests {
use broadcast_common::error::InvalidValue;
use super::*;
use crate::init_segment::StscEntry;
#[test]
fn new_zero_cap_is_rejected() {
let err =
ProgressiveDemux::new(0).expect_err("a zero cap must be rejected at construction");
assert!(
matches!(err, Error::InvalidInput(_)),
"expected Error::InvalidInput for a zero cap, got {err:?}"
);
}
#[test]
fn new_nonzero_cap_still_works() {
use broadcast_common::{Stage, Timestamp};
let mut demux = ProgressiveDemux::new(16).expect("non-zero cap must construct");
Stage::feed(&mut demux, &[0u8; 4], Timestamp::ZERO).expect("feed under the cap fits");
assert!(
!Stage::demand(&demux).saturated,
"headroom remains under the cap"
);
}
fn malformed_stsc_opaque() -> StblChild {
let mut raw = alloc::vec![0u8; 8];
raw[0..4].copy_from_slice(&8u32.to_be_bytes());
raw[4..8].copy_from_slice(b"stsc");
StblChild::Opaque(raw)
}
fn stsc_variant(c: &StblChild) -> Option<&SampleToChunkBox> {
match c {
StblChild::Stsc(b) => Some(b),
_ => None,
}
}
fn stsc_reparse_err(bytes: &[u8]) -> Error {
SampleToChunkBox::parse(bytes).unwrap_err()
}
#[test]
fn find_stbl_child_returns_typed_when_present() {
let stsc = SampleToChunkBox {
version: 0,
flags: 0,
entries: Vec::new(),
};
let children = alloc::vec![StblChild::Stsc(stsc.clone())];
let found = find_stbl_child(&children, b"stsc", stsc_variant, stsc_reparse_err)
.expect("typed present must not error");
assert_eq!(found, Some(&stsc));
}
#[test]
fn find_stbl_child_recovers_real_error_from_matching_opaque() {
let children = alloc::vec![malformed_stsc_opaque()];
let err = find_stbl_child(&children, b"stsc", stsc_variant, stsc_reparse_err)
.expect_err("a matching Opaque box must surface its real parse error");
assert!(
matches!(err, Error::BufferTooShort(..)),
"expected the real BufferTooShort from SampleToChunkBox::parse, got {err:?}"
);
}
#[test]
fn find_stbl_child_returns_none_for_non_matching_opaque() {
let mut raw = alloc::vec![0u8; 8];
raw[0..4].copy_from_slice(&8u32.to_be_bytes());
raw[4..8].copy_from_slice(b"stsz");
let children = alloc::vec![StblChild::Opaque(raw)];
let found = find_stbl_child(&children, b"stsc", stsc_variant, stsc_reparse_err)
.expect("a non-matching Opaque box must not be treated as this box's error");
assert!(found.is_none());
}
fn run(first_chunk: u32, samples_per_chunk: u32) -> StscEntry {
StscEntry {
first_chunk,
samples_per_chunk,
sample_description_index: 1,
}
}
fn stsc_of(entries: Vec<StscEntry>) -> SampleToChunkBox {
SampleToChunkBox {
version: 0,
flags: 0,
entries,
}
}
fn stco_of(offsets: &[u32]) -> ChunkOffsetBox {
ChunkOffsetBox {
version: 0,
flags: 0,
entries: offsets.to_vec(),
}
}
fn per_sample_stsz(sizes: &[u32]) -> SampleSizeBox {
SampleSizeBox {
version: 0,
flags: 0,
sample_size: 0,
sample_count: sizes.len() as u32,
entries: sizes.to_vec(),
}
}
fn uniform_stsz(size: u32, count: u32) -> SampleSizeBox {
SampleSizeBox {
version: 0,
flags: 0,
sample_size: size,
sample_count: count,
entries: Vec::new(),
}
}
#[test]
fn layout_tolerates_surplus_capacity_per_sample_stsz() {
let stsz = per_sample_stsz(&[10, 20, 30, 40, 50]);
let layout = sample_layout(
&stsz,
&stsc_of(alloc::vec![run(1, 3)]),
None,
Some(&stco_of(&[100, 1000])),
1 << 20,
)
.unwrap();
assert_eq!(
layout,
[(100, 10), (110, 20), (130, 30), (1000, 40), (1040, 50)]
);
}
#[test]
fn layout_tolerates_surplus_capacity_uniform_stsz() {
let layout = sample_layout(
&uniform_stsz(8, 4),
&stsc_of(alloc::vec![run(1, 3)]),
None,
Some(&stco_of(&[0, 100, 200])),
1 << 20,
)
.unwrap();
assert_eq!(layout, [(0, 8), (8, 8), (16, 8), (100, 8)]);
}
#[test]
fn layout_rejects_shortfall() {
for stsz in [uniform_stsz(8, 5), per_sample_stsz(&[8; 5])] {
let err = sample_layout(
&stsz,
&stsc_of(alloc::vec![run(1, 2)]),
None,
Some(&stco_of(&[0, 100])),
1 << 20,
)
.unwrap_err();
assert!(matches!(err, Error::InvalidInput(_)), "{err:?}");
}
}
#[test]
fn layout_rejects_non_ascending_first_chunk() {
for entries in [
alloc::vec![run(1, 2), run(0xFFFF_FFFF, 3), run(1, 4)],
alloc::vec![run(2, 1), run(2, 1)],
alloc::vec![run(0, 1)],
] {
let err = sample_layout(
&uniform_stsz(1, 1),
&stsc_of(entries),
None,
Some(&stco_of(&[0])),
1 << 20,
)
.unwrap_err();
assert!(
matches!(
err,
Error::InvalidValue(InvalidValue {
field: "stsc.first_chunk",
..
})
),
"{err:?}"
);
}
}
#[test]
fn layout_work_is_linear_in_chunks_and_entries() {
const CHUNKS: u32 = 400;
let entries: Vec<StscEntry> = (1..=CHUNKS).map(|c| run(c, 1)).collect();
let offsets: Vec<u32> = (1..=CHUNKS).map(|c| c * 100).collect();
LAYOUT_STEPS.with(|n| n.set(0));
let layout = sample_layout(
&uniform_stsz(10, CHUNKS),
&stsc_of(entries),
None,
Some(&stco_of(&offsets)),
1 << 20,
)
.unwrap();
let steps = LAYOUT_STEPS.with(core::cell::Cell::get);
assert_eq!(layout[0], (100, 10));
assert_eq!(layout[CHUNKS as usize - 1], (CHUNKS as usize * 100, 10));
assert!(steps <= 2 * CHUNKS as usize, "steps {steps}");
}
}