use crate::budgets::{BudgetKind, MemoryBudgets, check_planned};
use crate::error::{AacError, Result};
fn fence_entries(mem: &MemoryBudgets, count: u64, bytes_per: u64) -> Result<()> {
mem.check_index(count, bytes_per)
.map(|_| ())
.map_err(AacError::from)
}
fn fence_depth(mem: &MemoryBudgets, depth: u32) -> Result<()> {
mem.check_box_depth(depth).map_err(AacError::from)
}
#[derive(Debug)]
pub(crate) struct AacTrack {
pub asc: Vec<u8>,
pub total_samples: u64,
pub frames: Vec<(u64, u32)>,
pub edit_start: u64,
pub edit_duration: u64,
pub movie_timescale: u32,
pub media_timescale: u32,
#[cfg_attr(not(test), allow(dead_code))]
pub media_duration: u64,
pub has_elst: bool,
pub edit_open_end: bool,
}
impl AacTrack {
#[must_use]
#[cfg(test)]
pub(crate) fn skip_samples(&self, sample_rate: u32) -> usize {
if self.edit_start == 0 || self.media_timescale == 0 || sample_rate == 0 {
return 0;
}
let n = self.edit_start.saturating_mul(u64::from(sample_rate))
/ u64::from(self.media_timescale);
usize::try_from(n).unwrap_or(usize::MAX)
}
pub(crate) fn edit_window(&self, sample_rate: u32) -> Result<(usize, Option<usize>)> {
if !self.has_elst {
return Ok((0, None));
}
let skip = convert_exact(
self.edit_start,
sample_rate,
self.media_timescale,
"elst media_time",
)?;
if self.edit_open_end && self.edit_duration == 0 {
return Ok((skip, None));
}
let play = convert_exact(
self.edit_duration,
sample_rate,
self.movie_timescale,
"elst duration",
)?;
Ok((skip, Some(play)))
}
#[must_use]
pub(crate) fn presentation_samples(&self) -> Option<u64> {
if self.movie_timescale == 0 {
return None;
}
self.edit_duration
.checked_mul(u64::from(self.media_timescale))?
.checked_div(u64::from(self.movie_timescale))
}
#[must_use]
pub(crate) fn remainder_samples(&self) -> Option<u64> {
let valid = self.presentation_samples()?;
self.media_duration
.checked_sub(self.edit_start)?
.checked_sub(valid)
}
pub(crate) fn unplayed_tail(&self, skip: u64, samples_out: u64) -> Option<u64> {
if !self.has_elst {
return None;
}
if self.edit_open_end {
Some(
self.total_samples
.saturating_sub(skip)
.saturating_sub(samples_out),
)
} else {
self.remainder_samples()
}
}
}
#[derive(Debug)]
pub(crate) struct AacMeta {
pub asc: Vec<u8>,
pub edit_start: u64,
pub edit_duration: u64,
pub movie_timescale: u32,
pub media_timescale: u32,
pub media_duration: u64,
pub has_elst: bool,
}
type FourCc = [u8; 4];
const BOX_MOOV: FourCc = *b"moov";
const BOX_TRAK: FourCc = *b"trak";
const BOX_MOOF: FourCc = *b"moof";
const BOX_MVEX: FourCc = *b"mvex";
#[derive(Debug, Clone, Copy)]
struct BoxHdr {
content_start: usize,
content_end: usize,
}
fn read_box(data: &[u8], pos: usize) -> Result<Option<(BoxHdr, FourCc)>> {
let Some(hdr) = data.get(pos..pos.saturating_add(8)) else {
return Ok(None);
};
let size32 = u32::from_be_bytes([hdr[0], hdr[1], hdr[2], hdr[3]]) as u64;
let typ: FourCc = [hdr[4], hdr[5], hdr[6], hdr[7]];
let (size, head_len) = match size32 {
1 => {
let ext = data
.get(pos + 8..pos.saturating_add(16))
.ok_or_else(|| AacError::format("isomp4: truncated 64-bit box size"))?;
let arr: [u8; 8] = ext
.try_into()
.map_err(|_| AacError::format("isomp4: truncated 64-bit box size"))?;
(u64::from_be_bytes(arr), 16u64)
}
0 => {
((data.len() - pos) as u64, 8u64)
}
s => (s, 8u64),
};
if size < head_len {
return Err(AacError::format(format!(
"isomp4: box size {size} smaller than its header"
)));
}
let end_u = (pos as u64)
.checked_add(size)
.ok_or_else(|| AacError::format("isomp4: box size overflow"))?;
if end_u > data.len() as u64 {
return Err(AacError::format("isomp4: box extends past end of input"));
}
let end = usize::try_from(end_u).map_err(|_| AacError::format("isomp4: box exceeds usize"))?;
let content_start = (pos as u64)
.checked_add(head_len)
.ok_or_else(|| AacError::format("isomp4: box size overflow"))?;
let content_start = usize::try_from(content_start)
.map_err(|_| AacError::format("isomp4: box exceeds usize"))?;
Ok(Some((
BoxHdr {
content_start,
content_end: end,
},
typ,
)))
}
fn read_top_box(data: &[u8], pos: usize) -> Result<(BoxHdr, FourCc)> {
let truncated = || AacError::Truncated {
at: Some(pos as u64),
};
let avail = data.len() - pos;
if avail < 8 {
return Err(truncated());
}
let size32 = u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
let size = match size32 {
1 => {
let Some(ext) = data.get(pos.saturating_add(8)..pos.saturating_add(16)) else {
return Err(truncated());
};
u64::from_be_bytes(ext.try_into().map_err(|_| truncated())?)
}
0 => avail as u64, s => u64::from(s),
};
if size > avail as u64 {
return Err(truncated());
}
read_box(data, pos)?.ok_or_else(truncated)
}
struct BoxIter<'a> {
data: &'a [u8],
pos: usize,
end: usize,
}
impl<'a> BoxIter<'a> {
fn new(data: &'a [u8], start: usize, end: usize) -> Self {
BoxIter {
data,
pos: start,
end,
}
}
}
impl Iterator for BoxIter<'_> {
type Item = Result<(BoxHdr, FourCc)>;
fn next(&mut self) -> Option<Self::Item> {
if self.pos >= self.end {
return None;
}
match read_box(&self.data[..self.end], self.pos) {
Ok(Some((hdr, typ))) => {
self.pos = hdr.content_end;
Some(Ok((hdr, typ)))
}
Ok(None) => {
self.pos = self.end;
None
}
Err(e) => {
self.pos = self.end;
Some(Err(e))
}
}
}
}
fn read_desc_len(data: &[u8], pos: usize) -> Result<(usize, usize)> {
let mut len = 0usize;
for i in 0..4 {
let &b = data
.get(pos + i)
.ok_or_else(|| AacError::format("isomp4: truncated descriptor length"))?;
len = (len << 7) | usize::from(b & 0x7F);
if b & 0x80 == 0 {
return Ok((len, pos + i + 1));
}
}
Err(AacError::format(
"isomp4: descriptor length exceeds 4 bytes",
))
}
fn parse_esds(data: &[u8]) -> Result<Vec<u8>> {
let pos = 4usize; if data.get(pos) != Some(&0x03) {
return Err(AacError::format(
"isomp4: esds does not start with ES_Descriptor",
));
}
let (es_len, mut p) = read_desc_len(data, pos + 1)?;
let es_end = p
.checked_add(es_len)
.filter(|&e| e <= data.len())
.ok_or_else(|| AacError::format("isomp4: ES_Descriptor overruns esds box"))?;
p = p
.checked_add(3)
.filter(|&e| e <= es_end)
.ok_or_else(|| AacError::format("isomp4: truncated ES_Descriptor"))?;
let flags = data[p - 1];
if flags & 0x80 != 0 {
p = p
.checked_add(2)
.filter(|&e| e <= es_end)
.ok_or_else(|| AacError::format("isomp4: truncated streamDependence"))?;
}
if flags & 0x40 != 0 {
let url_len = *data
.get(p)
.ok_or_else(|| AacError::format("isomp4: truncated URL flag"))?
as usize;
p = p
.checked_add(1 + url_len)
.filter(|&e| e <= es_end)
.ok_or_else(|| AacError::format("isomp4: truncated URL string"))?;
}
if flags & 0x20 != 0 {
p = p
.checked_add(2)
.filter(|&e| e <= es_end)
.ok_or_else(|| AacError::format("isomp4: truncated OCR flag"))?;
}
if data.get(p) != Some(&0x04) {
return Err(AacError::format(
"isomp4: no DecoderConfigDescriptor in esds",
));
}
let (dc_len, mut p) = read_desc_len(data, p + 1)?;
let dc_end = p
.checked_add(dc_len)
.filter(|&e| e <= es_end)
.ok_or_else(|| AacError::format("isomp4: DecoderConfigDescriptor overruns"))?;
let object_type = *data
.get(p)
.ok_or_else(|| AacError::format("isomp4: truncated DecoderConfigDescriptor"))?;
if object_type != 0x40 {
return Err(AacError::format(format!(
"isomp4: unsupported MPEG-4 object type 0x{object_type:02x} (only 0x40 = AAC is handled)"
)));
}
p = p
.checked_add(13)
.filter(|&e| e <= dc_end)
.ok_or_else(|| AacError::format("isomp4: truncated DecoderConfigDescriptor body"))?;
while p < dc_end {
let tag = data[p];
let (len, body) = read_desc_len(data, p + 1)?;
let end = body
.checked_add(len)
.filter(|&e| e <= dc_end)
.ok_or_else(|| AacError::format("isomp4: sub-descriptor overruns"))?;
if tag == 0x05 {
if len == 0 {
return Err(AacError::format("isomp4: empty AudioSpecificConfig"));
}
return Ok(data[body..end].to_vec());
}
p = end;
}
Err(AacError::format(
"isomp4: no DecoderSpecificInfo (AudioSpecificConfig) in esds",
))
}
fn extract_asc(data: &[u8], children: BoxHdr) -> Result<Vec<u8>> {
for child in BoxIter::new(data, children.content_start, children.content_end) {
let (hdr, typ) = child?;
match &typ {
b"esds" => return parse_esds(&data[hdr.content_start..hdr.content_end]),
b"wave" => {
for sub in BoxIter::new(data, hdr.content_start, hdr.content_end) {
let (shdr, styp) = sub?;
if &styp == b"esds" {
return parse_esds(&data[shdr.content_start..shdr.content_end]);
}
}
}
_ => {}
}
}
Err(AacError::format("isomp4: mp4a entry carries no esds box"))
}
struct SampleTable {
sizes: Vec<u32>,
stsc: Vec<(u32, u32)>,
chunk_offsets: Vec<u64>,
total_samples: u64,
}
struct SoundMdia {
asc: Vec<u8>,
table: SampleTable,
timescale: u32,
duration: u64,
}
fn table_pos(base: usize, index: usize, stride: usize, what: &'static str) -> Result<usize> {
index
.checked_mul(stride)
.and_then(|span| base.checked_add(span))
.ok_or_else(|| AacError::format(format!("isomp4: {what} overflow")))
}
fn parse_stts(data: &[u8], mem: &MemoryBudgets) -> Result<u64> {
let count = read_u32(data, 4)? as usize;
fence_entries(mem, count as u64, 8)?;
let mut total = 0u64;
for i in 0..count {
let sample_count = read_u32(data, table_pos(8, i, 8, "stts")?)?;
total = total
.checked_add(u64::from(sample_count))
.ok_or_else(|| AacError::format("isomp4: stts total overflow"))?;
}
Ok(total)
}
fn read_u32(data: &[u8], pos: usize) -> Result<u32> {
let end = pos
.checked_add(4)
.ok_or_else(|| AacError::format("isomp4: truncated table"))?;
let b = data
.get(pos..end)
.ok_or_else(|| AacError::format("isomp4: truncated table"))?;
let arr: [u8; 4] = b
.try_into()
.map_err(|_| AacError::format("isomp4: truncated table"))?;
Ok(u32::from_be_bytes(arr))
}
fn read_u64(data: &[u8], pos: usize) -> Result<u64> {
let hi = u64::from(read_u32(data, pos)?);
let lo_at = pos
.checked_add(4)
.ok_or_else(|| AacError::format("isomp4: truncated table"))?;
let lo = u64::from(read_u32(data, lo_at)?);
Ok((hi << 32) | lo)
}
pub(crate) fn convert_units(value: u64, dst_ts: u32, src_ts: u32, what: &str) -> Result<u64> {
if src_ts == 0 || dst_ts == 0 {
return Err(AacError::format(format!("isomp4: {what} timescale is 0")));
}
let prod = value
.checked_mul(u64::from(dst_ts))
.ok_or_else(|| AacError::format(format!("isomp4: {what} overflow")))?;
if prod % u64::from(src_ts) != 0 {
return Err(AacError::format(format!(
"isomp4: {what} is not an integer number of samples"
)));
}
Ok(prod / u64::from(src_ts))
}
fn convert_exact(value: u64, dst_ts: u32, src_ts: u32, what: &str) -> Result<usize> {
usize::try_from(convert_units(value, dst_ts, src_ts, what)?)
.map_err(|_| AacError::format(format!("isomp4: {what} exceeds usize")))
}
fn parse_stsz(data: &[u8], file_len: u64, mem: &MemoryBudgets) -> Result<Vec<u32>> {
let default_size = read_u32(data, 4)?;
let count = read_u32(data, 8)? as usize;
fence_entries(mem, count as u64, 4)?;
if default_size != 0 {
let total = u64::from(default_size)
.checked_mul(count as u64)
.ok_or_else(|| AacError::format("isomp4: stsz overflow"))?;
if total > file_len {
return Err(AacError::format(format!(
"isomp4: stsz declares {count} samples of {default_size} bytes, more than the input holds"
)));
}
let mut sizes = Vec::new();
sizes.try_reserve(count).map_err(|_| {
AacError::from(crate::budgets::BudgetExceeded {
kind: crate::budgets::BudgetKind::Metadata,
observed: (count as u64).saturating_mul(4),
max: mem.max_metadata_bytes,
})
})?;
sizes.resize(count, default_size);
return Ok(sizes);
}
let mut sizes = Vec::new();
sizes.try_reserve(count).map_err(|_| {
AacError::from(crate::budgets::BudgetExceeded {
kind: crate::budgets::BudgetKind::Metadata,
observed: (count as u64).saturating_mul(4),
max: mem.max_metadata_bytes,
})
})?;
for i in 0..count {
sizes.push(read_u32(data, table_pos(12, i, 4, "stsz")?)?);
}
Ok(sizes)
}
fn parse_stsc(data: &[u8], mem: &MemoryBudgets) -> Result<Vec<(u32, u32)>> {
let count = read_u32(data, 4)? as usize;
fence_entries(mem, count as u64, 12)?;
let mut out = Vec::new();
out.try_reserve(count).map_err(|_| {
AacError::from(crate::budgets::BudgetExceeded {
kind: crate::budgets::BudgetKind::Metadata,
observed: (count as u64).saturating_mul(12),
max: mem.max_metadata_bytes,
})
})?;
for i in 0..count {
let base = table_pos(8, i, 12, "stsc")?;
let first_chunk = read_u32(data, base)?;
let samples_per_chunk = read_u32(data, table_pos(base, 1, 4, "stsc")?)?;
out.push((first_chunk, samples_per_chunk));
}
if out.is_empty() {
return Err(AacError::format("isomp4: empty stsc"));
}
Ok(out)
}
fn parse_stco(data: &[u8], wide: bool, mem: &MemoryBudgets) -> Result<Vec<u64>> {
let count = read_u32(data, 4)? as usize;
let bpe = if wide { 8 } else { 4 };
fence_entries(mem, count as u64, bpe)?;
let mut out = Vec::new();
out.try_reserve(count).map_err(|_| {
AacError::from(crate::budgets::BudgetExceeded {
kind: crate::budgets::BudgetKind::Metadata,
observed: (count as u64).saturating_mul(bpe),
max: mem.max_metadata_bytes,
})
})?;
for i in 0..count {
let off = if wide {
let base = table_pos(8, i, 8, "co64")?;
let hi = read_u32(data, base)? as u64;
let lo = read_u32(data, table_pos(base, 1, 4, "co64")?)? as u64;
(hi << 32) | lo
} else {
read_u32(data, table_pos(8, i, 4, "stco")?)? as u64
};
out.push(off);
}
Ok(out)
}
fn parse_stbl(
data: &[u8],
stbl: BoxHdr,
mem: &MemoryBudgets,
file_len: u64,
) -> Result<(Vec<u8>, SampleTable)> {
let mut asc = None;
let mut stts_total = None;
let mut sizes = None;
let mut stsc = None;
let mut chunk_offsets = None;
for child in BoxIter::new(data, stbl.content_start, stbl.content_end) {
let (hdr, typ) = child?;
let body = &data[hdr.content_start..hdr.content_end];
match &typ {
b"stsd" => asc = Some(parse_stsd_asc(body)?),
b"stts" => stts_total = Some(parse_stts(body, mem)?),
b"stsz" => sizes = Some(parse_stsz(body, file_len, mem)?),
b"stsc" => stsc = Some(parse_stsc(body, mem)?),
b"stco" => chunk_offsets = Some(parse_stco(body, false, mem)?),
b"co64" => chunk_offsets = Some(parse_stco(body, true, mem)?),
_ => {}
}
}
let asc =
asc.ok_or_else(|| AacError::format("isomp4: no AAC sample description (stsd/mp4a)"))?;
let table = SampleTable {
sizes: sizes.ok_or_else(|| AacError::format("isomp4: missing stsz"))?,
stsc: stsc.ok_or_else(|| AacError::format("isomp4: missing stsc"))?,
chunk_offsets: chunk_offsets
.ok_or_else(|| AacError::format("isomp4: missing stco/co64"))?,
total_samples: stts_total.ok_or_else(|| AacError::format("isomp4: missing stts"))?,
};
Ok((asc, table))
}
fn parse_stsd_asc(body: &[u8]) -> Result<Vec<u8>> {
let entries = read_u32(body, 4)? as usize;
if entries == 0 {
return Err(AacError::format("isomp4: stsd with no sample entries"));
}
let (ehdr, etyp) =
read_box(body, 8)?.ok_or_else(|| AacError::format("isomp4: truncated stsd entry"))?;
match &etyp {
b"mp4a" => {}
b"enca" => {
return Err(AacError::format(
"isomp4: encrypted (DRM) audio is not supported",
));
}
other => {
return Err(AacError::format(format!(
"isomp4: unsupported audio sample entry '{}'",
String::from_utf8_lossy(other)
)));
}
}
let body_start = ehdr.content_start;
let version = u16::from_be_bytes([
*body
.get(body_start + 8)
.ok_or_else(|| AacError::format("isomp4: truncated mp4a entry"))?,
*body
.get(body_start + 9)
.ok_or_else(|| AacError::format("isomp4: truncated mp4a entry"))?,
]);
let fixed = match version {
0 => 28usize,
1 => 28 + 16,
2 => 28 + 36,
v => {
return Err(AacError::format(format!(
"isomp4: unsupported sound sample entry version {v}"
)));
}
};
let children = BoxHdr {
content_start: body_start
.checked_add(fixed)
.filter(|&e| e <= ehdr.content_end)
.ok_or_else(|| AacError::format("isomp4: truncated mp4a entry"))?,
content_end: ehdr.content_end,
};
extract_asc(body, children)
}
fn parse_stbl_asc(data: &[u8], stbl: BoxHdr) -> Result<Vec<u8>> {
for child in BoxIter::new(data, stbl.content_start, stbl.content_end) {
let (hdr, typ) = child?;
if &typ == b"stsd" {
return parse_stsd_asc(&data[hdr.content_start..hdr.content_end]);
}
}
Err(AacError::format(
"isomp4: no AAC sample description (stsd/mp4a)",
))
}
fn parse_elst(body: &[u8], mem: &MemoryBudgets) -> Result<Option<(u64, u64)>> {
let version = *body
.first()
.ok_or_else(|| AacError::format("isomp4: truncated elst"))?;
let count = read_u32(body, 4)? as usize;
fence_entries(mem, count as u64, if version == 1 { 20 } else { 12 })?;
if count == 0 {
return Ok(None);
}
if count != 1 {
return Err(AacError::format(
"isomp4: multiple elst edits are not supported",
));
}
let (segment_duration, media_time, rate) = if version == 1 {
let seg = read_u64(body, 8)?;
let mt = i64::from_be_bytes(read_u64(body, 16)?.to_be_bytes());
(seg, mt, read_u32(body, 24)?)
} else if version == 0 {
(
u64::from(read_u32(body, 8)?),
i64::from(read_u32(body, 12)? as i32),
read_u32(body, 16)?,
)
} else {
return Err(AacError::format("isomp4: unsupported elst version"));
};
if media_time < 0 {
return Err(AacError::format("isomp4: empty elst edit is not supported"));
}
if rate != 0x0001_0000 {
return Err(AacError::format(
"isomp4: elst media_rate other than 1 is not supported",
));
}
Ok(Some((media_time as u64, segment_duration)))
}
#[cfg(test)]
fn parse_elst_start(body: &[u8], mem: &MemoryBudgets) -> Result<u64> {
Ok(parse_elst(body, mem)?.map_or(0, |e| e.0))
}
fn parse_mdhd_timescale(body: &[u8]) -> Result<u32> {
let version = *body
.first()
.ok_or_else(|| AacError::format("isomp4: truncated mdhd"))?;
let off = match version {
0 => 12usize,
1 => 20usize,
_ => return Err(AacError::format("isomp4: unsupported mdhd version")),
};
read_u32(body, off)
}
fn parse_mdhd_duration(body: &[u8]) -> Result<u64> {
let version = *body
.first()
.ok_or_else(|| AacError::format("isomp4: truncated mdhd"))?;
match version {
0 => Ok(u64::from(read_u32(body, 16)?)),
1 => read_u64(body, 24),
_ => Err(AacError::format("isomp4: unsupported mdhd version")),
}
}
fn parse_mvhd_timescale(body: &[u8]) -> Result<u32> {
let version = *body
.first()
.ok_or_else(|| AacError::format("isomp4: truncated mvhd"))?;
let off = match version {
0 => 12usize,
1 => 20usize,
_ => return Err(AacError::format("isomp4: unsupported mvhd version")),
};
read_u32(body, off)
}
fn parse_mdia(
data: &[u8],
mdia: BoxHdr,
mem: &MemoryBudgets,
file_len: u64,
) -> Result<Option<SoundMdia>> {
let mut is_sound = false;
let mut table = None;
let mut timescale = 0u32;
let mut media_duration = 0u64;
for child in BoxIter::new(data, mdia.content_start, mdia.content_end) {
let (hdr, typ) = child?;
match &typ {
b"hdlr" => {
let handler = data
.get(hdr.content_start + 8..hdr.content_start + 12)
.ok_or_else(|| AacError::format("isomp4: truncated hdlr"))?;
is_sound = handler == b"soun";
}
b"mdhd" => {
let body = &data[hdr.content_start..hdr.content_end];
timescale = parse_mdhd_timescale(body)?;
media_duration = parse_mdhd_duration(body).unwrap_or(0);
}
b"minf" => {
for sub in BoxIter::new(data, hdr.content_start, hdr.content_end) {
let (shdr, styp) = sub?;
if &styp == b"stbl" {
table = Some(parse_stbl(data, shdr, mem, file_len)?);
}
}
}
_ => {}
}
}
if is_sound {
let table = table.ok_or_else(|| AacError::format("isomp4: sound track without stbl"))?;
Ok(Some(SoundMdia {
asc: table.0,
table: table.1,
timescale,
duration: media_duration,
}))
} else {
Ok(None)
}
}
fn parse_mdia_meta(
data: &[u8],
mdia: BoxHdr,
mem: &MemoryBudgets,
) -> Result<Option<(Vec<u8>, u32, u64)>> {
fence_depth(mem, 3)?;
let mut is_sound = false;
let mut asc = None;
let mut timescale = 0u32;
let mut media_duration = 0u64;
for child in BoxIter::new(data, mdia.content_start, mdia.content_end) {
let (hdr, typ) = child?;
match &typ {
b"hdlr" => {
let handler = data
.get(hdr.content_start + 8..hdr.content_start + 12)
.ok_or_else(|| AacError::format("isomp4: truncated hdlr"))?;
is_sound = handler == b"soun";
}
b"mdhd" => {
let body = &data[hdr.content_start..hdr.content_end];
timescale = parse_mdhd_timescale(body)?;
media_duration = parse_mdhd_duration(body).unwrap_or(0);
}
b"minf" => {
for sub in BoxIter::new(data, hdr.content_start, hdr.content_end) {
let (shdr, styp) = sub?;
if &styp == b"stbl" {
fence_depth(mem, 4)?;
asc = Some(parse_stbl_asc(data, shdr)?);
}
}
}
_ => {}
}
}
if is_sound {
let asc = asc.ok_or_else(|| AacError::format("isomp4: sound track without stbl"))?;
Ok(Some((asc, timescale, media_duration)))
} else {
Ok(None)
}
}
fn build_frame_index(
data_len: u64,
table: &SampleTable,
mem: &MemoryBudgets,
) -> Result<Vec<(u64, u32)>> {
let n_samples = table.sizes.len();
fence_entries(mem, n_samples as u64, 16)?;
let mut frames = Vec::new();
frames.try_reserve(n_samples).map_err(|_| {
AacError::from(crate::budgets::BudgetExceeded {
kind: crate::budgets::BudgetKind::Metadata,
observed: (n_samples as u64).saturating_mul(16),
max: mem.max_metadata_bytes,
})
})?;
let mut sample_idx = 0usize;
let mut stsc_idx = 0usize;
for (chunk_i, &chunk_off) in table.chunk_offsets.iter().enumerate() {
let chunk_no = chunk_i as u32 + 1;
while stsc_idx + 1 < table.stsc.len() && table.stsc[stsc_idx + 1].0 <= chunk_no {
stsc_idx += 1;
}
let per_chunk = table.stsc[stsc_idx].1 as usize;
let mut off = chunk_off;
for _ in 0..per_chunk {
let Some(&size) = table.sizes.get(sample_idx) else {
return Err(AacError::format(
"isomp4: stsc/stsz disagree on the sample count",
));
};
let end = off
.checked_add(u64::from(size))
.filter(|&e| e <= data_len)
.ok_or_else(|| AacError::format("isomp4: sample extends past end of input"))?;
frames.push((off, size));
off = end;
sample_idx += 1;
}
}
if sample_idx != n_samples {
return Err(AacError::format(format!(
"isomp4: stsz declares {n_samples} samples but the chunk walk placed {sample_idx}"
)));
}
if frames.is_empty() {
return Err(AacError::format("isomp4: track has no audio samples"));
}
Ok(frames)
}
#[cfg(test)]
pub(crate) fn parse_aac_track(data: &[u8]) -> Result<AacTrack> {
parse_aac_track_with(data, &MemoryBudgets::default())
}
pub(crate) fn parse_aac_track_with(data: &[u8], mem: &MemoryBudgets) -> Result<AacTrack> {
parse_aac_track_with_len(data, data.len() as u64, mem)
}
pub(crate) fn parse_aac_track_with_len(
data: &[u8],
file_len: u64,
mem: &MemoryBudgets,
) -> Result<AacTrack> {
fence_depth(mem, 0)?;
let mut moov = None;
for top in BoxIter::new(data, 0, data.len()) {
let (hdr, typ) = top?;
if typ == BOX_MOOV {
moov = Some(hdr);
} else if typ == BOX_MOOF || typ == BOX_MVEX {
return Err(AacError::format("isomp4: fragmented MP4 is not supported"));
}
}
let moov = moov.ok_or_else(|| AacError::format("isomp4: no moov box"))?;
fence_depth(mem, 1)?;
let mut track = None;
let mut movie_timescale = 0u32;
for child in BoxIter::new(data, moov.content_start, moov.content_end) {
let (hdr, typ) = child?;
if typ == BOX_MVEX {
return Err(AacError::format("isomp4: fragmented MP4 is not supported"));
} else if &typ == b"mvhd" {
movie_timescale = parse_mvhd_timescale(&data[hdr.content_start..hdr.content_end])?;
} else if typ == BOX_TRAK {
if track.is_some() {
continue;
}
fence_depth(mem, 2)?;
let mut edit_start = 0u64;
let mut edit_duration = 0u64;
let mut has_elst = false;
let mut mdia_track = None;
for sub in BoxIter::new(data, hdr.content_start, hdr.content_end) {
let (shdr, styp) = sub?;
match &styp {
b"edts" => {
fence_depth(mem, 3)?;
for ed in BoxIter::new(data, shdr.content_start, shdr.content_end) {
let (ehdr, etyp) = ed?;
if &etyp == b"elst" {
fence_depth(mem, 4)?;
if let Some((start, dur)) =
parse_elst(&data[ehdr.content_start..ehdr.content_end], mem)?
{
edit_start = start;
edit_duration = dur;
has_elst = true;
}
}
}
}
b"mdia" => {
fence_depth(mem, 3)?;
match parse_mdia(data, shdr, mem, file_len) {
Ok(Some(t)) => mdia_track = Some(t),
Ok(None) => {}
Err(e @ AacError::Limit { .. }) => return Err(e),
Err(_) => {}
}
}
_ => {}
}
}
if let Some(mdia) = mdia_track {
track = Some((
mdia.asc,
mdia.table,
mdia.timescale,
edit_start,
edit_duration,
mdia.duration,
has_elst,
));
}
}
}
let (asc, table, media_timescale, edit_start, edit_duration, media_duration, has_elst) =
track.ok_or_else(|| AacError::format("isomp4: no AAC audio track found"))?;
let frames = build_frame_index(file_len, &table, mem)?;
Ok(AacTrack {
asc,
total_samples: table.total_samples,
frames,
edit_start,
edit_duration,
movie_timescale,
media_timescale,
media_duration,
has_elst,
edit_open_end: false,
})
}
pub(crate) fn parse_aac_meta(data: &[u8], mem: &MemoryBudgets) -> Result<AacMeta> {
fence_depth(mem, 0)?;
let mut moov = None;
for top in BoxIter::new(data, 0, data.len()) {
let (hdr, typ) = top?;
if typ == BOX_MOOV {
moov = Some(hdr);
} else if typ == BOX_MOOF || typ == BOX_MVEX {
return Err(AacError::format("isomp4: fragmented MP4 is not supported"));
}
}
let moov = moov.ok_or_else(|| AacError::format("isomp4: no moov box"))?;
fence_depth(mem, 1)?;
let mut track = None;
let mut movie_timescale = 0u32;
for child in BoxIter::new(data, moov.content_start, moov.content_end) {
let (hdr, typ) = child?;
if typ == BOX_MVEX {
return Err(AacError::format("isomp4: fragmented MP4 is not supported"));
} else if &typ == b"mvhd" {
movie_timescale = parse_mvhd_timescale(&data[hdr.content_start..hdr.content_end])?;
} else if typ == BOX_TRAK {
if track.is_some() {
continue;
}
fence_depth(mem, 2)?;
let mut edit_start = 0u64;
let mut edit_duration = 0u64;
let mut has_elst = false;
let mut mdia_track = None;
for sub in BoxIter::new(data, hdr.content_start, hdr.content_end) {
let (shdr, styp) = sub?;
match &styp {
b"edts" => {
fence_depth(mem, 3)?;
for ed in BoxIter::new(data, shdr.content_start, shdr.content_end) {
let (ehdr, etyp) = ed?;
if &etyp == b"elst" {
fence_depth(mem, 4)?;
if let Some((start, dur)) =
parse_elst(&data[ehdr.content_start..ehdr.content_end], mem)?
{
edit_start = start;
edit_duration = dur;
has_elst = true;
}
}
}
}
b"mdia" => match parse_mdia_meta(data, shdr, mem) {
Ok(Some(t)) => mdia_track = Some(t),
Ok(None) => {}
Err(e @ AacError::Limit { .. }) => return Err(e),
Err(_) => {}
},
_ => {}
}
}
if let Some((asc, media_timescale, media_duration)) = mdia_track {
track = Some((
asc,
media_timescale,
edit_start,
edit_duration,
media_duration,
has_elst,
));
}
}
}
let (asc, media_timescale, edit_start, edit_duration, media_duration, has_elst) =
track.ok_or_else(|| AacError::format("isomp4: no AAC audio track found"))?;
Ok(AacMeta {
asc,
edit_start,
edit_duration,
movie_timescale,
media_timescale,
media_duration,
has_elst,
})
}
pub(crate) fn find_moov_slice<'a>(data: &'a [u8], mem: &MemoryBudgets) -> Result<&'a [u8]> {
let mut pos = 0usize;
let n = data.len() as u64;
loop {
if (pos as u64).saturating_add(8) > n {
return Err(AacError::NeedMore {
have: n,
need: (pos as u64).saturating_add(8),
});
}
let mut size_bytes = [0u8; 4];
size_bytes.copy_from_slice(&data[pos..pos + 4]);
let size32 = u32::from_be_bytes(size_bytes) as u64;
let typ: FourCc = [data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]];
let (size, head) = match size32 {
1 => {
if (pos as u64).saturating_add(16) > n {
return Err(AacError::NeedMore {
have: n,
need: (pos as u64).saturating_add(16),
});
}
let mut ext = [0u8; 8];
ext.copy_from_slice(&data[pos + 8..pos + 16]);
(u64::from_be_bytes(ext), 16u64)
}
0 => (n.saturating_sub(pos as u64), 8u64),
s => (s, 8u64),
};
if size < head {
return Err(AacError::format(format!(
"isomp4: box size {size} smaller than its header"
)));
}
let end = (pos as u64)
.checked_add(size)
.ok_or_else(|| AacError::format("isomp4: box size overflow"))?;
if typ == BOX_MOOF || typ == BOX_MVEX {
return Err(AacError::format("isomp4: fragmented MP4 is not supported"));
}
if typ == BOX_MOOV {
check_planned(BudgetKind::Metadata, size, mem.max_metadata_bytes)?;
if end > n {
return Err(AacError::NeedMore { have: n, need: end });
}
let end_us =
usize::try_from(end).map_err(|_| AacError::format("isomp4: moov exceeds usize"))?;
return Ok(&data[pos..end_us]);
}
if size32 == 0 {
return Err(AacError::format("isomp4: no moov box"));
}
if end > n {
return Err(AacError::NeedMore { have: n, need: end });
}
pos = usize::try_from(end).map_err(|_| AacError::format("isomp4: box exceeds usize"))?;
}
}
pub(crate) fn sniff_is_m4a(data: &[u8]) -> bool {
if data.len() < 8 {
return false;
}
if &data[4..8] != b"ftyp" {
return false;
}
let size = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
if size < 8 {
return false;
}
if data.len() >= 12 {
return data[8..12]
.iter()
.all(|b| b.is_ascii_graphic() || *b == b' ');
}
true
}
#[inline]
pub fn sniff_is_isobmff(data: &[u8]) -> bool {
sniff_is_m4a(data)
}
#[path = "isomp4_io.rs"]
mod io;
pub(crate) use io::{TrackBoxes, load_track_boxes};
#[path = "isomp4_frag.rs"]
pub(crate) mod frag;
#[cfg(test)]
#[path = "isomp4_struct_tests.rs"]
mod isomp4_struct_tests;
#[cfg(test)]
#[path = "isomp4_tests.rs"]
mod isomp4_tests;