use mp4_atom::{Cslg, Ctts, CttsEntry, Stsc, StscEntry, Stss, Stts, SttsEntry};
use crate::error::Result;
use crate::mp4::mux::bytes::BoxBuf;
#[derive(Debug, Default)]
pub(crate) struct SampleTable {
pub count: u32,
durations: Vec<(u32, u32)>,
offsets: Vec<(u32, i64)>,
pub offset_range: Option<(i64, i64)>,
sizes: Vec<u32>,
sync: Vec<u32>,
chunks: Vec<(u64, u32)>,
pub media_duration: u64,
pub presentation: Option<(i64, i64)>,
}
impl SampleTable {
pub fn push_sample(&mut self, size: u32, offset: i64, sync: bool) {
self.count += 1;
self.sizes.push(size);
if sync {
self.sync.push(self.count);
}
match self.offsets.last_mut() {
Some((count, value)) if *value == offset => *count += 1,
_ => self.offsets.push((1, offset)),
}
self.offset_range = Some(match self.offset_range {
Some((low, high)) => (low.min(offset), high.max(offset)),
None => (offset, offset),
});
}
pub fn push_duration(&mut self, delta: u32, pts: i64) {
match self.durations.last_mut() {
Some((count, value)) if *value == delta => *count += 1,
_ => self.durations.push((1, delta)),
}
self.media_duration += u64::from(delta);
let end = pts.saturating_add(i64::from(delta));
self.presentation = Some(match self.presentation {
Some((start, last)) => (start.min(pts), last.max(end)),
None => (pts, end),
});
}
pub fn push_chunk(&mut self, offset: u64, samples: u32) {
self.chunks.push((offset, samples));
}
pub fn has_offsets(&self) -> bool {
self.offset_range
.is_some_and(|(low, high)| low != 0 || high != 0)
}
fn needs_co64(&self, shift: u64) -> bool {
self.chunks
.last()
.is_some_and(|&(offset, _)| offset.saturating_add(shift) > u64::from(u32::MAX))
}
pub fn encode(&self, buf: &mut BoxBuf, entry: &[u8], shift: u64, path: &str) -> Result<()> {
let stbl = buf.open(b"stbl");
let stsd = buf.open_full(b"stsd", 0, 0);
buf.u32(1);
buf.bytes(entry);
buf.close(stsd, &format!("{path}/stsd"))?;
let stts = Stts {
entries: self
.durations
.iter()
.map(|&(sample_count, sample_delta)| SttsEntry {
sample_count,
sample_delta,
})
.collect(),
};
buf.encode(&stts, &format!("{path}/stts"))?;
if self.has_offsets() {
let ctts = Ctts {
entries: self
.offsets
.iter()
.map(|&(sample_count, sample_offset)| CttsEntry {
sample_count,
sample_offset,
})
.collect(),
};
buf.encode(&ctts, &format!("{path}/ctts"))?;
if let (Some((low, high)), Some((start, end))) = (self.offset_range, self.presentation)
&& low < 0
{
let cslg = Cslg {
composition_to_dts_shift: low.saturating_neg(),
least_decode_to_display_shift: low,
greatest_decode_to_display_delta: high,
composition_start_time: start,
composition_end_time: end,
};
buf.encode(&cslg, &format!("{path}/cslg"))?;
}
}
let mut stsc = Stsc::default();
for (n, &(_, samples)) in self.chunks.iter().enumerate() {
if stsc
.entries
.last()
.is_some_and(|entry| entry.samples_per_chunk == samples)
{
continue;
}
stsc.entries.push(StscEntry {
first_chunk: n as u32 + 1,
samples_per_chunk: samples,
sample_description_index: 1,
});
}
buf.encode(&stsc, &format!("{path}/stsc"))?;
let stsz = buf.open_full(b"stsz", 0, 0);
let uniform = self
.sizes
.first()
.filter(|&&first| first != 0 && self.sizes.iter().all(|&size| size == first));
match uniform {
Some(&size) => {
buf.u32(size);
buf.u32(self.count);
}
None => {
buf.u32(0);
buf.u32(self.count);
for &size in &self.sizes {
buf.u32(size);
}
}
}
buf.close(stsz, &format!("{path}/stsz"))?;
let chunk_count = self.chunks.len() as u32;
if self.needs_co64(shift) {
let co64 = buf.open_full(b"co64", 0, 0);
buf.u32(chunk_count);
for &(offset, _) in &self.chunks {
buf.u64(offset.saturating_add(shift));
}
buf.close(co64, &format!("{path}/co64"))?;
} else {
let stco = buf.open_full(b"stco", 0, 0);
buf.u32(chunk_count);
for &(offset, _) in &self.chunks {
buf.u32(offset.saturating_add(shift) as u32);
}
buf.close(stco, &format!("{path}/stco"))?;
}
if self.sync.len() != self.count as usize {
let stss = Stss {
entries: self.sync.clone(),
};
buf.encode(&stss, &format!("{path}/stss"))?;
}
buf.close(stbl, path)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn table(offsets: &[u64]) -> SampleTable {
let mut table = SampleTable::default();
for (n, &offset) in offsets.iter().enumerate() {
table.push_sample(10, if n == 1 { -5 } else { 0 }, n == 0);
table.push_duration(20, 20 * n as i64);
table.push_chunk(offset, 1);
}
table
}
fn kinds(bytes: &[u8]) -> Vec<[u8; 4]> {
let mut out = Vec::new();
let mut pos = 8;
while pos + 8 <= bytes.len() {
let size =
u32::from_be_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]);
out.push([
bytes[pos + 4],
bytes[pos + 5],
bytes[pos + 6],
bytes[pos + 7],
]);
pos += size as usize;
}
out
}
#[test]
fn offsets_past_4_gib_switch_to_co64() {
let small = table(&[100, 4_000_000_000]);
assert!(!small.needs_co64(0));
assert!(small.needs_co64(u64::from(u32::MAX)));
let mut buf = BoxBuf::new();
let big = table(&[100, 5 << 30, 6 << 30]);
big.encode(&mut buf, &[], 16, "stbl")
.expect("the tables encode");
let bytes = buf.into_bytes();
let found = kinds(&bytes);
assert!(found.contains(b"co64"), "{found:?}");
assert!(!found.contains(b"stco"), "{found:?}");
let at = bytes
.windows(4)
.position(|w| w == b"co64")
.expect("co64 is present");
let entry = |n: usize| {
let start = at + 12 + 8 * n;
u64::from_be_bytes(bytes[start..start + 8].try_into().expect("8 bytes"))
};
assert_eq!(
[entry(0), entry(1), entry(2)],
[116, (5 << 30) + 16, (6 << 30) + 16]
);
}
#[test]
fn negative_offsets_write_ctts_version_1_and_cslg() {
let mut buf = BoxBuf::new();
table(&[0, 10, 20])
.encode(&mut buf, &[], 0, "stbl")
.expect("the tables encode");
let bytes = buf.into_bytes();
let found = kinds(&bytes);
assert!(found.contains(b"cslg"), "{found:?}");
assert!(found.contains(b"stss"), "{found:?}");
let at = bytes
.windows(4)
.position(|w| w == b"ctts")
.expect("ctts is present");
assert_eq!(bytes[at + 4], 1, "ctts version");
}
#[test]
fn stsc_merges_chunks_of_equal_size() {
let mut table = SampleTable::default();
for (offset, samples) in [(0, 2), (10, 2), (20, 1), (30, 2)] {
table.push_chunk(offset, samples);
}
let mut buf = BoxBuf::new();
table
.encode(&mut buf, &[], 0, "stbl")
.expect("the tables encode");
let bytes = buf.into_bytes();
let at = bytes
.windows(4)
.position(|w| w == b"stsc")
.expect("stsc is present");
let count = u32::from_be_bytes(bytes[at + 8..at + 12].try_into().expect("4 bytes"));
assert_eq!(count, 3);
}
}