use mp4_atom::Stsc;
use crate::error::{Error, Result};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ChunkRun {
pub first_chunk: u32,
pub samples_per_chunk: u32,
pub description_index: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct ChunkMap {
runs: Vec<ChunkRun>,
offsets: Vec<u64>,
}
impl ChunkMap {
pub fn new(stsc: Stsc, offsets: Vec<u64>, descriptions: usize, path: &str) -> Result<Self> {
let runs: Vec<ChunkRun> = stsc
.entries
.into_iter()
.map(|entry| ChunkRun {
first_chunk: entry.first_chunk,
samples_per_chunk: entry.samples_per_chunk,
description_index: entry.sample_description_index,
})
.collect();
let chunk_count = offsets.len() as u64;
let mut previous = 0u32;
for (i, run) in runs.iter().enumerate() {
if i == 0 && run.first_chunk != 1 {
return Err(Error::mp4_inconsistent(
path,
format!(
"the first entry starts at chunk {}, not chunk 1",
run.first_chunk
),
));
}
if run.first_chunk <= previous {
return Err(Error::mp4_inconsistent(
path,
format!(
"entry {i} starts at chunk {} after an entry that starts at chunk {previous}",
run.first_chunk
),
));
}
if u64::from(run.first_chunk) > chunk_count {
return Err(Error::mp4_inconsistent(
path,
format!(
"entry {i} starts at chunk {}, but the chunk offset table holds {chunk_count} chunks",
run.first_chunk
),
));
}
if run.description_index == 0 || run.description_index as usize > descriptions {
return Err(Error::mp4_inconsistent(
path,
format!(
"entry {i} uses sample description {}, but stsd holds {descriptions}",
run.description_index
),
));
}
previous = run.first_chunk;
}
if runs.is_empty() && !offsets.is_empty() {
return Err(Error::mp4_inconsistent(
path,
format!(
"the table is empty, but the chunk offset table holds {chunk_count} chunks"
),
));
}
Ok(Self { runs, offsets })
}
pub fn sample_count(&self, path: &str) -> Result<u64> {
let chunk_count = self.offsets.len() as u64;
let mut total = 0u64;
for (i, run) in self.runs.iter().enumerate() {
let end = self
.runs
.get(i + 1)
.map_or(chunk_count, |next| u64::from(next.first_chunk) - 1);
let chunks = end - (u64::from(run.first_chunk) - 1);
total = chunks
.checked_mul(u64::from(run.samples_per_chunk))
.and_then(|samples| total.checked_add(samples))
.ok_or_else(|| {
Error::mp4_inconsistent(path, "the sample count overflows 64 bits")
})?;
}
Ok(total)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct ChunkCursor {
chunk: usize,
run: usize,
left: u32,
next_offset: u64,
description_index: u32,
}
impl ChunkCursor {
pub fn next_sample(
&mut self,
map: &ChunkMap,
size: u32,
path: &str,
) -> Result<Option<(u64, u32)>> {
while self.left == 0 {
let Some(&offset) = map.offsets.get(self.chunk) else {
return Ok(None);
};
while let Some(next) = map.runs.get(self.run + 1) {
if (next.first_chunk as usize) - 1 <= self.chunk {
self.run += 1;
} else {
break;
}
}
let Some(run) = map.runs.get(self.run) else {
return Ok(None);
};
self.left = run.samples_per_chunk;
self.description_index = run.description_index;
self.next_offset = offset;
self.chunk += 1;
}
let offset = self.next_offset;
self.next_offset = offset.checked_add(u64::from(size)).ok_or_else(|| {
Error::mp4_inconsistent(
path,
format!("a sample of {size} bytes at offset {offset} ends past 2^64"),
)
})?;
self.left -= 1;
Ok(Some((offset, self.description_index)))
}
}
#[cfg(test)]
mod tests {
use mp4_atom::StscEntry;
use super::*;
fn stsc(entries: &[(u32, u32, u32)]) -> Stsc {
Stsc {
entries: entries
.iter()
.map(
|&(first_chunk, samples_per_chunk, sample_description_index)| StscEntry {
first_chunk,
samples_per_chunk,
sample_description_index,
},
)
.collect(),
}
}
fn expand(map: &ChunkMap, sizes: &[u32]) -> Vec<(u64, u32)> {
let mut cursor = ChunkCursor::default();
sizes
.iter()
.map_while(|&size| cursor.next_sample(map, size, "stsc").ok().flatten())
.collect()
}
#[test]
fn stsc_places_samples_in_chunks() {
let map = ChunkMap::new(
stsc(&[(1, 2, 1), (3, 1, 2)]),
vec![100, 200, 300, 400],
2,
"stsc",
)
.expect("the table is consistent");
assert_eq!(map.sample_count("stsc").ok(), Some(6));
let placed = expand(&map, &[10, 20, 30, 40, 50, 60]);
assert_eq!(
placed,
vec![(100, 1), (110, 1), (200, 1), (230, 1), (300, 2), (400, 2)]
);
}
#[test]
fn stsc_skips_chunks_with_no_samples() {
let map = ChunkMap::new(
stsc(&[(1, 1, 1), (2, 0, 1), (3, 1, 1)]),
vec![10, 20, 30],
1,
"stsc",
)
.expect("the table is consistent");
assert_eq!(map.sample_count("stsc").ok(), Some(2));
assert_eq!(expand(&map, &[5, 5]), vec![(10, 1), (30, 1)]);
}
#[test]
fn stsc_rejects_a_first_entry_after_chunk_1() {
let err = ChunkMap::new(stsc(&[(2, 1, 1)]), vec![10, 20], 1, "stsc");
assert!(matches!(err, Err(Error::Inconsistent { .. })));
}
#[test]
fn stsc_rejects_entries_out_of_order() {
let err = ChunkMap::new(
stsc(&[(1, 1, 1), (3, 1, 1), (2, 1, 1)]),
vec![1, 2, 3],
1,
"stsc",
);
assert!(matches!(err, Err(Error::Inconsistent { .. })));
}
#[test]
fn stsc_rejects_a_chunk_past_the_offset_table() {
let err = ChunkMap::new(stsc(&[(1, 1, 1), (5, 1, 1)]), vec![1, 2], 1, "stsc");
assert!(matches!(err, Err(Error::Inconsistent { .. })));
}
#[test]
fn stsc_rejects_an_unknown_sample_description() {
let err = ChunkMap::new(stsc(&[(1, 1, 2)]), vec![1], 1, "stsc");
assert!(matches!(err, Err(Error::Inconsistent { .. })));
}
#[test]
fn chunk_cursor_reports_offset_overflow() {
let map = ChunkMap::new(stsc(&[(1, 2, 1)]), vec![u64::MAX - 1], 1, "stsc")
.expect("the table is consistent");
let mut cursor = ChunkCursor::default();
assert_eq!(
cursor.next_sample(&map, 1, "stsc").ok().flatten(),
Some((u64::MAX - 1, 1))
);
assert!(cursor.next_sample(&map, 4, "stsc").is_err());
}
}