use std::collections::HashSet;
use std::io::SeekFrom;
use oxideav_core::{
CodecId, CodecParameters, CodecResolver, CodecTag, Error, MediaType, Packet, ProbeContext,
Result, SampleFormat, StreamInfo, TimeBase,
};
use oxideav_core::{Demuxer, ReadSeek};
use crate::boxes::*;
use crate::codec_id::{from_sample_entry, from_sample_entry_with_oti};
pub fn open(mut input: Box<dyn ReadSeek>, codecs: &dyn CodecResolver) -> Result<Box<dyn Demuxer>> {
let mut saw_ftyp = false;
let mut moov: Option<Vec<u8>> = None;
let mut moofs: Vec<MoofRecord> = Vec::new();
let mut sidxes: Vec<SidxRecord> = Vec::new();
let mut tfras: Vec<TfraRecord> = Vec::new();
while let Some(hdr) = read_box_header(&mut *input)? {
match hdr.fourcc {
FTYP => {
saw_ftyp = true;
skip_box_body(&mut *input, &hdr)?;
}
MOOV => {
moov = Some(read_box_body(&mut *input, &hdr)?);
}
STYP => skip_box_body(&mut *input, &hdr)?,
SIDX => {
let body_start = input.stream_position()?;
let sidx_end_offset = body_start
+ hdr
.payload_size()
.ok_or_else(|| Error::invalid("MP4: open-ended sidx"))?;
let body = read_box_body(&mut *input, &hdr)?;
if let Some(r) = parse_sidx(&body, sidx_end_offset)? {
sidxes.push(r);
}
}
MOOF => {
let payload_size = hdr
.payload_size()
.ok_or_else(|| Error::invalid("MP4: open-ended moof"))?;
let body_start = input.stream_position()?;
let moof_start = body_start - hdr.header_len;
let body = read_bytes_vec(&mut *input, payload_size as usize)?;
moofs.push(MoofRecord { moof_start, body });
}
MFRA => {
let body = read_box_body(&mut *input, &hdr)?;
parse_mfra(&body, &mut tfras)?;
}
_ => skip_box_body(&mut *input, &hdr)?,
}
}
if !saw_ftyp {
return Err(Error::invalid("MP4: missing ftyp box"));
}
let moov = moov.ok_or_else(|| Error::invalid("MP4: missing moov box"))?;
let parsed = parse_moov(&moov)?;
if parsed.tracks.is_empty() {
return Err(Error::invalid("MP4: no tracks"));
}
let mut streams: Vec<StreamInfo> = Vec::with_capacity(parsed.tracks.len());
let mut samples: Vec<SampleRef> = Vec::new();
for (i, t) in parsed.tracks.iter().enumerate() {
streams.push(build_stream_info(i as u32, t, codecs));
expand_samples(t, i as u32, &mut samples)?;
}
let mut next_dts: Vec<i64> = vec![0; parsed.tracks.len()];
for s in &samples {
let idx = s.track_idx as usize;
let end = s.dts.saturating_add(s.duration);
if end > next_dts[idx] {
next_dts[idx] = end;
}
}
for moof in &moofs {
parse_moof(moof, &parsed.tracks, &mut samples, &mut next_dts)?;
}
samples.sort_by_key(|s| s.offset);
let duration_micros: i64 = if parsed.movie_timescale > 0 && parsed.movie_duration > 0 {
(parsed.movie_duration as i128 * 1_000_000 / parsed.movie_timescale as i128) as i64
} else {
0
};
Ok(Box::new(Mp4Demuxer {
input,
streams,
samples,
cursor: 0,
metadata: parsed.metadata,
duration_micros,
sidxes,
tfras,
movie_timescale: parsed.movie_timescale,
track_timescales: parsed.tracks.iter().map(|t| t.timescale).collect(),
track_ids: parsed.tracks.iter().map(|t| t.track_id).collect(),
}))
}
struct MoofRecord {
moof_start: u64,
body: Vec<u8>,
}
#[derive(Clone, Debug)]
pub struct SidxRecord {
pub reference_id: u32,
pub timescale: u32,
pub earliest_presentation_time: u64,
pub first_byte_offset: u64,
pub references: Vec<SidxReference>,
}
#[derive(Clone, Copy, Debug)]
pub struct SidxReference {
pub is_sidx: bool,
pub referenced_size: u32,
pub subsegment_duration: u32,
pub starts_with_sap: bool,
pub sap_type: u8,
}
#[derive(Clone, Debug)]
pub struct TfraRecord {
pub track_id: u32,
pub entries: Vec<TfraEntry>,
}
#[derive(Clone, Copy, Debug)]
pub struct TfraEntry {
pub time: u64,
pub moof_offset: u64,
pub traf_number: u32,
pub trun_number: u32,
pub sample_number: u32,
}
#[derive(Default)]
struct ParsedMoov {
tracks: Vec<Track>,
movie_timescale: u32,
movie_duration: u64,
metadata: Vec<(String, String)>,
}
#[derive(Clone, Debug)]
struct Track {
track_id: u32,
media_type: MediaType,
codec_id_fourcc: [u8; 4],
timescale: u32,
duration: Option<u64>,
channels: Option<u16>,
sample_rate: Option<u32>,
sample_size_bits: Option<u16>,
width: Option<u32>,
height: Option<u32>,
extradata: Vec<u8>,
esds_oti: Option<u8>,
stts: Vec<(u32, u32)>, stsc: Vec<(u32, u32, u32)>, stsz: Vec<u32>, chunk_offsets: Vec<u64>, stss: Vec<u32>,
ctts: Vec<(u32, i32)>,
elst: Vec<ElstEntry>,
trex: TrexDefaults,
}
#[derive(Clone, Copy, Debug, Default)]
#[allow(dead_code)] struct ElstEntry {
segment_duration: u64,
media_time: i64,
media_rate: u32,
}
#[derive(Clone, Copy, Debug, Default)]
#[allow(dead_code)] struct TrexDefaults {
default_sample_description_index: u32,
default_sample_duration: u32,
default_sample_size: u32,
default_sample_flags: u32,
}
fn parse_moov(moov: &[u8]) -> Result<ParsedMoov> {
let mut out = ParsedMoov::default();
let mut cur = std::io::Cursor::new(moov);
let end = moov.len() as u64;
while cur.position() < end {
let hdr = match read_box_header(&mut cur)? {
Some(h) => h,
None => break,
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
match hdr.fourcc {
TRAK => {
let body = read_bytes_vec(&mut cur, psz)?;
if let Some(t) = parse_trak(&body)? {
out.tracks.push(t);
}
}
MVHD => {
let body = read_bytes_vec(&mut cur, psz)?;
parse_mvhd(&body, &mut out)?;
}
UDTA => {
let body = read_bytes_vec(&mut cur, psz)?;
parse_udta(&body, &mut out.metadata);
}
META => {
let body = read_bytes_vec(&mut cur, psz)?;
parse_meta(&body, &mut out.metadata);
}
MVEX => {
let body = read_bytes_vec(&mut cur, psz)?;
parse_mvex(&body, &mut out.tracks)?;
}
_ => {
cur.set_position(cur.position() + psz as u64);
}
}
}
Ok(out)
}
fn parse_mvex(body: &[u8], tracks: &mut [Track]) -> Result<()> {
let mut cur = std::io::Cursor::new(body);
let end = body.len() as u64;
while cur.position() < end {
let hdr = match read_box_header(&mut cur)? {
Some(h) => h,
None => break,
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
match hdr.fourcc {
TREX => {
let b = read_bytes_vec(&mut cur, psz)?;
parse_trex(&b, tracks)?;
}
_ => cur.set_position(cur.position() + psz as u64),
}
}
Ok(())
}
fn parse_trex(body: &[u8], tracks: &mut [Track]) -> Result<()> {
if body.len() < 24 {
return Err(Error::invalid("MP4: trex too short"));
}
let track_id = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
let dsdi = u32::from_be_bytes([body[8], body[9], body[10], body[11]]);
let ddur = u32::from_be_bytes([body[12], body[13], body[14], body[15]]);
let dsiz = u32::from_be_bytes([body[16], body[17], body[18], body[19]]);
let dflg = u32::from_be_bytes([body[20], body[21], body[22], body[23]]);
if let Some(t) = tracks.iter_mut().find(|t| t.track_id == track_id) {
t.trex = TrexDefaults {
default_sample_description_index: dsdi,
default_sample_duration: ddur,
default_sample_size: dsiz,
default_sample_flags: dflg,
};
}
Ok(())
}
fn parse_mvhd(body: &[u8], out: &mut ParsedMoov) -> Result<()> {
if body.is_empty() {
return Err(Error::invalid("MP4: mvhd empty"));
}
let version = body[0];
let (timescale, duration) = if version == 0 {
if body.len() < 20 {
return Err(Error::invalid("MP4: mvhd v0 too short"));
}
let ts = u32::from_be_bytes([body[12], body[13], body[14], body[15]]);
let du = u32::from_be_bytes([body[16], body[17], body[18], body[19]]) as u64;
(ts, du)
} else {
if body.len() < 32 {
return Err(Error::invalid("MP4: mvhd v1 too short"));
}
let ts = u32::from_be_bytes([body[20], body[21], body[22], body[23]]);
let du = u64::from_be_bytes([
body[24], body[25], body[26], body[27], body[28], body[29], body[30], body[31],
]);
(ts, du)
};
out.movie_timescale = timescale;
out.movie_duration = duration;
Ok(())
}
fn parse_udta(body: &[u8], metadata: &mut Vec<(String, String)>) {
let mut cur = std::io::Cursor::new(body);
let end = body.len() as u64;
while cur.position() < end {
let hdr = match read_box_header(&mut cur).ok().flatten() {
Some(h) => h,
None => break,
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
if cur.position() as usize + psz > body.len() {
break;
}
let start = cur.position() as usize;
cur.set_position((start + psz) as u64);
let payload = &body[start..start + psz];
match &hdr.fourcc {
b"meta" => parse_meta(payload, metadata),
b"titl" | b"auth" | b"cprt" | b"dscp" | b"gnre" | b"albm" | b"yrrc"
if payload.len() >= 6 =>
{
let key = match &hdr.fourcc {
b"titl" => "title",
b"auth" => "artist",
b"cprt" => "copyright",
b"dscp" => "description",
b"gnre" => "genre",
b"albm" => "album",
b"yrrc" => "date",
_ => unreachable!(),
};
let s = decode_utf8_or_utf16(&payload[6..]);
if !s.is_empty() {
metadata.push((key.into(), s));
}
}
_ => {}
}
}
}
fn parse_meta(body: &[u8], metadata: &mut Vec<(String, String)>) {
if body.len() < 4 {
return;
}
let mut cur = std::io::Cursor::new(&body[4..]);
let end = body.len() as u64 - 4;
while cur.position() < end {
let hdr = match read_box_header(&mut cur).ok().flatten() {
Some(h) => h,
None => break,
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
let start = cur.position() as usize;
if start + psz > (body.len() - 4) {
break;
}
cur.set_position((start + psz) as u64);
if hdr.fourcc == ILST {
parse_ilst(&body[4 + start..4 + start + psz], metadata);
}
}
}
fn parse_ilst(body: &[u8], metadata: &mut Vec<(String, String)>) {
let mut cur = std::io::Cursor::new(body);
let end = body.len() as u64;
while cur.position() < end {
let hdr = match read_box_header(&mut cur).ok().flatten() {
Some(h) => h,
None => break,
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
let start = cur.position() as usize;
if start + psz > body.len() {
break;
}
cur.set_position((start + psz) as u64);
let item = &body[start..start + psz];
let key = ilst_key_for(&hdr.fourcc);
if key.is_none() {
continue;
}
let key = key.unwrap();
let mut sub = std::io::Cursor::new(item);
let sub_end = item.len() as u64;
while sub.position() < sub_end {
let sh = match read_box_header(&mut sub).ok().flatten() {
Some(h) => h,
None => break,
};
let sub_psz = sh.payload_size().unwrap_or(0) as usize;
let sub_start = sub.position() as usize;
if sub_start + sub_psz > item.len() {
break;
}
sub.set_position((sub_start + sub_psz) as u64);
if sh.fourcc == DATA {
let data_body = &item[sub_start..sub_start + sub_psz];
if data_body.len() > 8 {
let value = String::from_utf8_lossy(&data_body[8..]).trim().to_string();
if !value.is_empty() {
metadata.push((key.into(), value));
}
}
}
}
}
}
fn ilst_key_for(fourcc: &[u8; 4]) -> Option<&'static str> {
match fourcc {
b"\xa9nam" => Some("title"),
b"\xa9ART" => Some("artist"),
b"\xa9alb" => Some("album"),
b"\xa9cmt" => Some("comment"),
b"\xa9gen" => Some("genre"),
b"\xa9day" => Some("date"),
b"\xa9wrt" => Some("composer"),
b"\xa9too" => Some("encoder"),
b"\xa9cpy" | b"cprt" => Some("copyright"),
b"\xa9lyr" => Some("lyrics"),
b"aART" => Some("album_artist"),
b"trkn" => Some("track"),
b"disk" => Some("disc"),
b"desc" => Some("description"),
_ => None,
}
}
fn decode_utf8_or_utf16(buf: &[u8]) -> String {
if buf.len() >= 2 && buf[0] == 0xFE && buf[1] == 0xFF {
let pairs = buf[2..].chunks_exact(2);
let units: Vec<u16> = pairs.map(|p| u16::from_be_bytes([p[0], p[1]])).collect();
return String::from_utf16_lossy(&units)
.trim_end_matches('\0')
.trim()
.to_string();
}
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
String::from_utf8_lossy(&buf[..end]).trim().to_string()
}
fn parse_trak(body: &[u8]) -> Result<Option<Track>> {
let mut t = Track {
track_id: 0,
media_type: MediaType::Unknown,
codec_id_fourcc: [0; 4],
timescale: 0,
duration: None,
channels: None,
sample_rate: None,
sample_size_bits: None,
width: None,
height: None,
extradata: Vec::new(),
esds_oti: None,
stts: Vec::new(),
stsc: Vec::new(),
stsz: Vec::new(),
chunk_offsets: Vec::new(),
stss: Vec::new(),
ctts: Vec::new(),
elst: Vec::new(),
trex: TrexDefaults::default(),
};
let mut has_media = false;
let mut cur = std::io::Cursor::new(body);
let end = body.len() as u64;
while cur.position() < end {
let hdr = match read_box_header(&mut cur)? {
Some(h) => h,
None => break,
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
match hdr.fourcc {
TKHD => {
let sub = read_bytes_vec(&mut cur, psz)?;
parse_tkhd(&sub, &mut t)?;
}
MDIA => {
let sub = read_bytes_vec(&mut cur, psz)?;
parse_mdia(&sub, &mut t)?;
has_media = true;
}
EDTS => {
let sub = read_bytes_vec(&mut cur, psz)?;
parse_edts(&sub, &mut t)?;
}
_ => {
cur.set_position(cur.position() + psz as u64);
}
}
}
if has_media {
Ok(Some(t))
} else {
Ok(None)
}
}
fn parse_tkhd(body: &[u8], t: &mut Track) -> Result<()> {
if body.is_empty() {
return Err(Error::invalid("MP4: tkhd empty"));
}
let version = body[0];
let off = if version == 0 { 4 + 8 } else { 4 + 16 };
if body.len() < off + 4 {
return Err(Error::invalid("MP4: tkhd too short"));
}
t.track_id = u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
Ok(())
}
fn parse_edts(body: &[u8], t: &mut Track) -> Result<()> {
let mut cur = std::io::Cursor::new(body);
let end = body.len() as u64;
while cur.position() < end {
let hdr = match read_box_header(&mut cur)? {
Some(h) => h,
None => break,
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
match hdr.fourcc {
ELST => {
let b = read_bytes_vec(&mut cur, psz)?;
parse_elst(&b, t)?;
}
_ => cur.set_position(cur.position() + psz as u64),
}
}
Ok(())
}
fn parse_elst(body: &[u8], t: &mut Track) -> Result<()> {
if body.len() < 8 {
return Err(Error::invalid("MP4: elst too short"));
}
let version = body[0];
let count = u32::from_be_bytes([body[4], body[5], body[6], body[7]]) as usize;
let entry_size = if version == 1 { 20 } else { 12 };
let mut off = 8;
let mut entries = Vec::with_capacity(count);
for _ in 0..count {
if off + entry_size > body.len() {
return Err(Error::invalid("MP4: elst truncated"));
}
let (segment_duration, media_time) = if version == 1 {
let dur = u64::from_be_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
body[off + 4],
body[off + 5],
body[off + 6],
body[off + 7],
]);
let mt = i64::from_be_bytes([
body[off + 8],
body[off + 9],
body[off + 10],
body[off + 11],
body[off + 12],
body[off + 13],
body[off + 14],
body[off + 15],
]);
(dur, mt)
} else {
let dur =
u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]) as u64;
let mt =
i32::from_be_bytes([body[off + 4], body[off + 5], body[off + 6], body[off + 7]])
as i64;
(dur, mt)
};
let rate_off = off + if version == 1 { 16 } else { 8 };
let media_rate = u32::from_be_bytes([
body[rate_off],
body[rate_off + 1],
body[rate_off + 2],
body[rate_off + 3],
]);
entries.push(ElstEntry {
segment_duration,
media_time,
media_rate,
});
off += entry_size;
}
t.elst = entries;
Ok(())
}
fn elst_leading_media_time(t: &Track) -> i64 {
for e in &t.elst {
if e.media_time != -1 {
return e.media_time;
}
}
0
}
fn parse_mdia(body: &[u8], t: &mut Track) -> Result<()> {
let mut cur = std::io::Cursor::new(body);
let end = body.len() as u64;
while cur.position() < end {
let hdr = match read_box_header(&mut cur)? {
Some(h) => h,
None => break,
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
match hdr.fourcc {
MDHD => {
let b = read_bytes_vec(&mut cur, psz)?;
parse_mdhd(&b, t)?;
}
HDLR => {
let b = read_bytes_vec(&mut cur, psz)?;
parse_hdlr(&b, t)?;
}
MINF => {
let b = read_bytes_vec(&mut cur, psz)?;
parse_minf(&b, t)?;
}
_ => cur.set_position(cur.position() + psz as u64),
}
}
Ok(())
}
fn parse_mdhd(body: &[u8], t: &mut Track) -> Result<()> {
if body.len() < 24 {
return Err(Error::invalid("MP4: mdhd too short"));
}
let version = body[0];
let (timescale, duration) = if version == 0 {
let ts = u32::from_be_bytes([body[12], body[13], body[14], body[15]]);
let du = u32::from_be_bytes([body[16], body[17], body[18], body[19]]) as u64;
(ts, du)
} else {
if body.len() < 32 {
return Err(Error::invalid("MP4: mdhd v1 too short"));
}
let ts = u32::from_be_bytes([body[20], body[21], body[22], body[23]]);
let du = u64::from_be_bytes([
body[24], body[25], body[26], body[27], body[28], body[29], body[30], body[31],
]);
(ts, du)
};
t.timescale = timescale;
t.duration = Some(duration);
Ok(())
}
fn parse_hdlr(body: &[u8], t: &mut Track) -> Result<()> {
if body.len() < 12 {
return Err(Error::invalid("MP4: hdlr too short"));
}
let mut handler = [0u8; 4];
handler.copy_from_slice(&body[8..12]);
t.media_type = match &handler {
h if *h == HANDLER_SOUN => MediaType::Audio,
h if *h == HANDLER_VIDE => MediaType::Video,
_ => MediaType::Data,
};
Ok(())
}
fn parse_minf(body: &[u8], t: &mut Track) -> Result<()> {
let mut cur = std::io::Cursor::new(body);
let end = body.len() as u64;
while cur.position() < end {
let hdr = match read_box_header(&mut cur)? {
Some(h) => h,
None => break,
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
match hdr.fourcc {
STBL => {
let sub = read_bytes_vec(&mut cur, psz)?;
parse_stbl(&sub, t)?;
}
_ => cur.set_position(cur.position() + psz as u64),
}
}
Ok(())
}
fn parse_stbl(body: &[u8], t: &mut Track) -> Result<()> {
let mut cur = std::io::Cursor::new(body);
let end = body.len() as u64;
while cur.position() < end {
let hdr = match read_box_header(&mut cur)? {
Some(h) => h,
None => break,
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
let b = read_bytes_vec(&mut cur, psz)?;
match hdr.fourcc {
STSD => parse_stsd(&b, t)?,
STTS => t.stts = parse_stts(&b)?,
STSC => t.stsc = parse_stsc(&b)?,
STSZ => t.stsz = parse_stsz(&b)?,
STZ2 => t.stsz = parse_stz2(&b)?,
STCO => t.chunk_offsets = parse_stco(&b)?,
CO64 => t.chunk_offsets = parse_co64(&b)?,
STSS => t.stss = parse_stss(&b)?,
CTTS => t.ctts = parse_ctts(&b)?,
_ => {}
}
}
Ok(())
}
fn parse_stsd(body: &[u8], t: &mut Track) -> Result<()> {
if body.len() < 8 {
return Err(Error::invalid("MP4: stsd too short"));
}
let entry_count = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
if entry_count == 0 {
return Ok(());
}
let mut cur = std::io::Cursor::new(&body[8..]);
let hdr = match read_box_header(&mut cur)? {
Some(h) => h,
None => return Err(Error::invalid("MP4: stsd first entry missing")),
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
let entry = read_bytes_vec(&mut cur, psz)?;
t.codec_id_fourcc = hdr.fourcc;
parse_sample_entry(&entry, t)?;
Ok(())
}
fn parse_sample_entry(entry: &[u8], t: &mut Track) -> Result<()> {
if entry.len() < 8 {
return Ok(());
}
match t.media_type {
MediaType::Audio => parse_audio_sample_entry(entry, t),
MediaType::Video => parse_video_sample_entry(entry, t),
_ => Ok(()),
}
}
fn parse_audio_sample_entry(entry: &[u8], t: &mut Track) -> Result<()> {
if entry.len() < 28 {
return Ok(());
}
let channels = u16::from_be_bytes([entry[16], entry[17]]);
let sample_size = u16::from_be_bytes([entry[18], entry[19]]);
let sample_rate = u32::from_be_bytes([entry[24], entry[25], entry[26], entry[27]]) >> 16;
t.channels = Some(channels);
t.sample_size_bits = Some(sample_size);
t.sample_rate = Some(sample_rate);
let mut cur = std::io::Cursor::new(&entry[28..]);
let end = (entry.len() - 28) as u64;
while cur.position() < end {
let hdr = match read_box_header(&mut cur)? {
Some(h) => h,
None => break,
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
let body = read_bytes_vec(&mut cur, psz)?;
match &hdr.fourcc {
b"dfLa" if body.len() > 4 => {
t.extradata = body[4..].to_vec();
}
b"dOps" if body.len() >= 11 => {
let mut oh = Vec::with_capacity(body.len() + 8);
oh.extend_from_slice(b"OpusHead");
oh.extend_from_slice(&body);
t.extradata = oh;
}
b"esds" if body.len() >= 4 => {
if let Some(parsed) = parse_esds(&body[4..]) {
if !parsed.dsi.is_empty() {
t.extradata = parsed.dsi;
}
t.esds_oti = parsed.oti;
}
}
b"dac3" | b"dec3" => t.extradata = body,
_ => {}
}
}
Ok(())
}
#[derive(Default)]
struct EsdsInfo {
dsi: Vec<u8>,
oti: Option<u8>,
}
fn parse_esds(buf: &[u8]) -> Option<EsdsInfo> {
let mut info = EsdsInfo::default();
let mut cur = 0usize;
let (tag, len, hdr_bytes) = read_descr(buf, cur)?;
if tag != 0x03 {
return None;
}
cur += hdr_bytes;
let es_end = cur.checked_add(len)?;
if es_end > buf.len() {
return None;
}
if cur + 3 > es_end {
return None;
}
let flags = buf[cur + 2];
cur += 3;
if flags & 0x80 != 0 {
cur = cur.checked_add(2)?; }
if flags & 0x40 != 0 {
if cur >= es_end {
return None;
}
let url_len = buf[cur] as usize;
cur = cur.checked_add(1 + url_len)?;
}
if flags & 0x20 != 0 {
cur = cur.checked_add(2)?; }
while cur < es_end {
let (sub_tag, sub_len, sub_hdr) = read_descr(buf, cur)?;
cur += sub_hdr;
let sub_end = cur.checked_add(sub_len)?;
if sub_end > es_end {
return None;
}
if sub_tag == 0x04 {
if sub_len < 13 {
return None;
}
info.oti = Some(buf[cur]);
if sub_len > 13 {
let mut inner = cur + 13;
while inner < sub_end {
let (dsi_tag, dsi_len, dsi_hdr) = read_descr(buf, inner)?;
inner += dsi_hdr;
let dsi_end = inner.checked_add(dsi_len)?;
if dsi_end > sub_end {
return None;
}
if dsi_tag == 0x05 {
info.dsi = buf[inner..dsi_end].to_vec();
break;
}
inner = dsi_end;
}
}
}
cur = sub_end;
}
Some(info)
}
#[cfg(test)]
fn parse_esds_dsi(buf: &[u8]) -> Option<Vec<u8>> {
let info = parse_esds(buf)?;
if info.dsi.is_empty() {
None
} else {
Some(info.dsi)
}
}
fn read_descr(buf: &[u8], off: usize) -> Option<(u8, usize, usize)> {
if off >= buf.len() {
return None;
}
let tag = buf[off];
let mut len: usize = 0;
let mut consumed = 1usize;
for _ in 0..4 {
let p = off + consumed;
if p >= buf.len() {
return None;
}
let b = buf[p];
consumed += 1;
len = (len << 7) | (b & 0x7F) as usize;
if b & 0x80 == 0 {
return Some((tag, len, consumed));
}
}
None
}
fn parse_video_sample_entry(entry: &[u8], t: &mut Track) -> Result<()> {
if entry.len() < 28 {
return Ok(());
}
let width = u16::from_be_bytes([entry[24], entry[25]]);
let height = u16::from_be_bytes([entry[26], entry[27]]);
t.width = Some(width as u32);
t.height = Some(height as u32);
if entry.len() <= 78 {
return Ok(());
}
let mut cur = std::io::Cursor::new(&entry[78..]);
let end = (entry.len() - 78) as u64;
while cur.position() < end {
let hdr = match read_box_header(&mut cur)? {
Some(h) => h,
None => break,
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
let body = read_bytes_vec(&mut cur, psz)?;
match &hdr.fourcc {
b"avcC" => t.extradata = body,
b"hvcC" => t.extradata = body,
b"av1C" => t.extradata = body,
b"vpcC" => t.extradata = body,
b"esds" if body.len() >= 4 => {
if let Some(parsed) = parse_esds(&body[4..]) {
if !parsed.dsi.is_empty() {
t.extradata = parsed.dsi;
}
t.esds_oti = parsed.oti;
}
}
_ => {}
}
}
Ok(())
}
fn parse_stts(body: &[u8]) -> Result<Vec<(u32, u32)>> {
if body.len() < 8 {
return Err(Error::invalid("MP4: stts too short"));
}
let count = u32::from_be_bytes([body[4], body[5], body[6], body[7]]) as usize;
let mut out = Vec::with_capacity(count);
let mut off = 8;
for _ in 0..count {
if off + 8 > body.len() {
return Err(Error::invalid("MP4: stts truncated"));
}
let cnt = u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
let dlt = u32::from_be_bytes([body[off + 4], body[off + 5], body[off + 6], body[off + 7]]);
out.push((cnt, dlt));
off += 8;
}
Ok(out)
}
fn parse_stsc(body: &[u8]) -> Result<Vec<(u32, u32, u32)>> {
if body.len() < 8 {
return Err(Error::invalid("MP4: stsc too short"));
}
let count = u32::from_be_bytes([body[4], body[5], body[6], body[7]]) as usize;
let mut out = Vec::with_capacity(count);
let mut off = 8;
for _ in 0..count {
if off + 12 > body.len() {
return Err(Error::invalid("MP4: stsc truncated"));
}
let fc = u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
let spc = u32::from_be_bytes([body[off + 4], body[off + 5], body[off + 6], body[off + 7]]);
let sdi =
u32::from_be_bytes([body[off + 8], body[off + 9], body[off + 10], body[off + 11]]);
out.push((fc, spc, sdi));
off += 12;
}
Ok(out)
}
fn parse_stsz(body: &[u8]) -> Result<Vec<u32>> {
if body.len() < 12 {
return Err(Error::invalid("MP4: stsz too short"));
}
let uniform = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
let count = u32::from_be_bytes([body[8], body[9], body[10], body[11]]) as usize;
if uniform != 0 {
return Ok(vec![uniform; count]);
}
let mut out = Vec::with_capacity(count);
let mut off = 12;
for _ in 0..count {
if off + 4 > body.len() {
return Err(Error::invalid("MP4: stsz truncated"));
}
out.push(u32::from_be_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
]));
off += 4;
}
Ok(out)
}
fn parse_stz2(body: &[u8]) -> Result<Vec<u32>> {
if body.len() < 12 {
return Err(Error::invalid("MP4: stz2 too short"));
}
let field_size = body[7];
let count = u32::from_be_bytes([body[8], body[9], body[10], body[11]]) as usize;
let mut out = Vec::with_capacity(count);
let off = 12;
match field_size {
4 => {
for i in 0..count {
if off + i / 2 >= body.len() {
return Err(Error::invalid("MP4: stz2 4-bit truncated"));
}
let b = body[off + i / 2];
let v = if i % 2 == 0 { b >> 4 } else { b & 0x0F };
out.push(v as u32);
}
}
8 => {
if off + count > body.len() {
return Err(Error::invalid("MP4: stz2 8-bit truncated"));
}
for i in 0..count {
out.push(body[off + i] as u32);
}
}
16 => {
if off + count * 2 > body.len() {
return Err(Error::invalid("MP4: stz2 16-bit truncated"));
}
for i in 0..count {
out.push(u16::from_be_bytes([body[off + 2 * i], body[off + 2 * i + 1]]) as u32);
}
}
_ => return Err(Error::invalid("MP4: stz2 invalid field size")),
}
Ok(out)
}
fn parse_stss(body: &[u8]) -> Result<Vec<u32>> {
if body.len() < 8 {
return Err(Error::invalid("MP4: stss too short"));
}
let count = u32::from_be_bytes([body[4], body[5], body[6], body[7]]) as usize;
let mut out = Vec::with_capacity(count);
let mut off = 8;
for _ in 0..count {
if off + 4 > body.len() {
return Err(Error::invalid("MP4: stss truncated"));
}
out.push(u32::from_be_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
]));
off += 4;
}
Ok(out)
}
fn parse_ctts(body: &[u8]) -> Result<Vec<(u32, i32)>> {
if body.len() < 8 {
return Err(Error::invalid("MP4: ctts too short"));
}
let version = body[0];
let count = u32::from_be_bytes([body[4], body[5], body[6], body[7]]) as usize;
let mut out = Vec::with_capacity(count);
let mut off = 8;
for _ in 0..count {
if off + 8 > body.len() {
return Err(Error::invalid("MP4: ctts truncated"));
}
let cnt = u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
let raw = [body[off + 4], body[off + 5], body[off + 6], body[off + 7]];
let dlt: i32 = if version == 0 {
u32::from_be_bytes(raw) as i32
} else {
i32::from_be_bytes(raw)
};
out.push((cnt, dlt));
off += 8;
}
Ok(out)
}
fn parse_stco(body: &[u8]) -> Result<Vec<u64>> {
if body.len() < 8 {
return Err(Error::invalid("MP4: stco too short"));
}
let count = u32::from_be_bytes([body[4], body[5], body[6], body[7]]) as usize;
let mut out = Vec::with_capacity(count);
let mut off = 8;
for _ in 0..count {
if off + 4 > body.len() {
return Err(Error::invalid("MP4: stco truncated"));
}
out.push(
u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]) as u64,
);
off += 4;
}
Ok(out)
}
fn parse_co64(body: &[u8]) -> Result<Vec<u64>> {
if body.len() < 8 {
return Err(Error::invalid("MP4: co64 too short"));
}
let count = u32::from_be_bytes([body[4], body[5], body[6], body[7]]) as usize;
let mut out = Vec::with_capacity(count);
let mut off = 8;
for _ in 0..count {
if off + 8 > body.len() {
return Err(Error::invalid("MP4: co64 truncated"));
}
out.push(u64::from_be_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
body[off + 4],
body[off + 5],
body[off + 6],
body[off + 7],
]));
off += 8;
}
Ok(out)
}
fn parse_moof(
moof: &MoofRecord,
tracks: &[Track],
samples: &mut Vec<SampleRef>,
next_dts: &mut [i64],
) -> Result<()> {
let mut cur = std::io::Cursor::new(&moof.body);
let end = moof.body.len() as u64;
while cur.position() < end {
let hdr = match read_box_header(&mut cur)? {
Some(h) => h,
None => break,
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
match hdr.fourcc {
MFHD => cur.set_position(cur.position() + psz as u64),
TRAF => {
let body = read_bytes_vec(&mut cur, psz)?;
parse_traf(&body, moof.moof_start, tracks, samples, next_dts)?;
}
_ => cur.set_position(cur.position() + psz as u64),
}
}
Ok(())
}
#[derive(Default)]
struct TrafState {
track_idx: usize,
tfhd_flags: u32,
base_data_offset: u64,
default_sample_duration: u32,
default_sample_size: u32,
default_sample_flags: u32,
base_media_decode_time: Option<i64>,
}
const TFHD_BASE_DATA_OFFSET_PRESENT: u32 = 0x000001;
const TFHD_SAMPLE_DESCRIPTION_INDEX_PRESENT: u32 = 0x000002;
const TFHD_DEFAULT_SAMPLE_DURATION_PRESENT: u32 = 0x000008;
const TFHD_DEFAULT_SAMPLE_SIZE_PRESENT: u32 = 0x000010;
const TFHD_DEFAULT_SAMPLE_FLAGS_PRESENT: u32 = 0x000020;
#[allow(dead_code)]
const TFHD_DEFAULT_BASE_IS_MOOF: u32 = 0x020000;
const TRUN_DATA_OFFSET_PRESENT: u32 = 0x000001;
const TRUN_FIRST_SAMPLE_FLAGS_PRESENT: u32 = 0x000004;
const TRUN_SAMPLE_DURATION_PRESENT: u32 = 0x000100;
const TRUN_SAMPLE_SIZE_PRESENT: u32 = 0x000200;
const TRUN_SAMPLE_FLAGS_PRESENT: u32 = 0x000400;
const TRUN_SAMPLE_COMPOSITION_TIME_OFFSETS_PRESENT: u32 = 0x000800;
const SAMPLE_IS_NON_SYNC: u32 = 0x0001_0000;
fn parse_traf(
body: &[u8],
moof_start: u64,
tracks: &[Track],
samples: &mut Vec<SampleRef>,
next_dts: &mut [i64],
) -> Result<()> {
let mut state = TrafState::default();
let mut tfhd_seen = false;
let mut cur = std::io::Cursor::new(body);
let end = body.len() as u64;
while cur.position() < end {
let hdr = match read_box_header(&mut cur)? {
Some(h) => h,
None => break,
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
match hdr.fourcc {
TFHD => {
let b = read_bytes_vec(&mut cur, psz)?;
parse_tfhd(&b, moof_start, tracks, &mut state)?;
tfhd_seen = true;
}
TFDT => {
let b = read_bytes_vec(&mut cur, psz)?;
state.base_media_decode_time = Some(parse_tfdt(&b)?);
}
TRUN => cur.set_position(cur.position() + psz as u64),
_ => cur.set_position(cur.position() + psz as u64),
}
}
if !tfhd_seen {
return Err(Error::invalid("MP4: traf missing tfhd"));
}
let track = &tracks[state.track_idx];
let mut frag_dts: i64 = state
.base_media_decode_time
.unwrap_or(next_dts[state.track_idx]);
let mut next_data_offset_within_traf: u64 = state.base_data_offset;
let mut cur = std::io::Cursor::new(body);
while cur.position() < end {
let hdr = match read_box_header(&mut cur)? {
Some(h) => h,
None => break,
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
if hdr.fourcc != TRUN {
cur.set_position(cur.position() + psz as u64);
continue;
}
let b = read_bytes_vec(&mut cur, psz)?;
let parsed = parse_trun(&b)?;
let mut sample_off = if let Some(d) = parsed.data_offset {
(state.base_data_offset as i64).wrapping_add(d as i64) as u64
} else {
next_data_offset_within_traf
};
for (i, s) in parsed.samples.iter().enumerate() {
let dur = s.duration.unwrap_or(state.default_sample_duration) as i64;
let size = s.size.unwrap_or(state.default_sample_size);
let flags = s.flags.unwrap_or_else(|| {
if i == 0 {
parsed
.first_sample_flags
.unwrap_or(state.default_sample_flags)
} else {
state.default_sample_flags
}
});
let keyframe = (flags & SAMPLE_IS_NON_SYNC) == 0;
let cts_off = s.composition_time_offset.unwrap_or(0) as i64;
let elst_shift = elst_leading_media_time(track);
let dts_v = frag_dts.saturating_sub(elst_shift);
let cts_v = frag_dts.saturating_add(cts_off).saturating_sub(elst_shift);
samples.push(SampleRef {
track_idx: state.track_idx as u32,
offset: sample_off,
size,
pts: cts_v,
dts: dts_v,
duration: dur,
keyframe,
});
sample_off = sample_off.saturating_add(size as u64);
frag_dts = frag_dts.saturating_add(dur);
}
next_data_offset_within_traf = sample_off;
}
next_dts[state.track_idx] = frag_dts;
Ok(())
}
fn parse_tfhd(body: &[u8], moof_start: u64, tracks: &[Track], state: &mut TrafState) -> Result<()> {
if body.len() < 8 {
return Err(Error::invalid("MP4: tfhd too short"));
}
let flags = u32::from_be_bytes([0, body[1], body[2], body[3]]);
let track_id = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
let track_idx = tracks
.iter()
.position(|t| t.track_id == track_id)
.ok_or_else(|| {
Error::invalid(format!("MP4: tfhd refers to unknown track_ID {track_id}"))
})?;
let mut off = 8;
let trex = tracks[track_idx].trex;
state.track_idx = track_idx;
state.tfhd_flags = flags;
state.default_sample_duration = trex.default_sample_duration;
state.default_sample_size = trex.default_sample_size;
state.default_sample_flags = trex.default_sample_flags;
state.base_media_decode_time = None;
let mut explicit_base: Option<u64> = None;
if flags & TFHD_BASE_DATA_OFFSET_PRESENT != 0 {
if off + 8 > body.len() {
return Err(Error::invalid("MP4: tfhd base_data_offset truncated"));
}
let v = u64::from_be_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
body[off + 4],
body[off + 5],
body[off + 6],
body[off + 7],
]);
explicit_base = Some(v);
off += 8;
}
if flags & TFHD_SAMPLE_DESCRIPTION_INDEX_PRESENT != 0 {
if off + 4 > body.len() {
return Err(Error::invalid(
"MP4: tfhd sample_description_index truncated",
));
}
off += 4;
}
if flags & TFHD_DEFAULT_SAMPLE_DURATION_PRESENT != 0 {
if off + 4 > body.len() {
return Err(Error::invalid(
"MP4: tfhd default_sample_duration truncated",
));
}
state.default_sample_duration =
u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
off += 4;
}
if flags & TFHD_DEFAULT_SAMPLE_SIZE_PRESENT != 0 {
if off + 4 > body.len() {
return Err(Error::invalid("MP4: tfhd default_sample_size truncated"));
}
state.default_sample_size =
u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
off += 4;
}
if flags & TFHD_DEFAULT_SAMPLE_FLAGS_PRESENT != 0 {
if off + 4 > body.len() {
return Err(Error::invalid("MP4: tfhd default_sample_flags truncated"));
}
state.default_sample_flags =
u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
}
state.base_data_offset = explicit_base.unwrap_or(moof_start);
Ok(())
}
fn parse_tfdt(body: &[u8]) -> Result<i64> {
if body.len() < 4 {
return Err(Error::invalid("MP4: tfdt too short"));
}
let version = body[0];
if version == 1 {
if body.len() < 12 {
return Err(Error::invalid("MP4: tfdt v1 too short"));
}
Ok(u64::from_be_bytes([
body[4], body[5], body[6], body[7], body[8], body[9], body[10], body[11],
]) as i64)
} else {
if body.len() < 8 {
return Err(Error::invalid("MP4: tfdt v0 too short"));
}
Ok(u32::from_be_bytes([body[4], body[5], body[6], body[7]]) as i64)
}
}
#[derive(Clone, Copy, Debug, Default)]
struct TrunSample {
duration: Option<u32>,
size: Option<u32>,
flags: Option<u32>,
composition_time_offset: Option<i32>,
}
#[derive(Default)]
struct ParsedTrun {
data_offset: Option<i32>,
first_sample_flags: Option<u32>,
samples: Vec<TrunSample>,
}
fn parse_trun(body: &[u8]) -> Result<ParsedTrun> {
if body.len() < 8 {
return Err(Error::invalid("MP4: trun too short"));
}
let version = body[0];
let flags = u32::from_be_bytes([0, body[1], body[2], body[3]]);
let sample_count = u32::from_be_bytes([body[4], body[5], body[6], body[7]]) as usize;
let mut off = 8usize;
let mut out = ParsedTrun::default();
out.samples.reserve(sample_count);
if flags & TRUN_DATA_OFFSET_PRESENT != 0 {
if off + 4 > body.len() {
return Err(Error::invalid("MP4: trun data_offset truncated"));
}
out.data_offset = Some(i32::from_be_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
]));
off += 4;
}
if flags & TRUN_FIRST_SAMPLE_FLAGS_PRESENT != 0 {
if off + 4 > body.len() {
return Err(Error::invalid("MP4: trun first_sample_flags truncated"));
}
out.first_sample_flags = Some(u32::from_be_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
]));
off += 4;
}
let per_sample_fields = ((flags & TRUN_SAMPLE_DURATION_PRESENT) != 0) as usize
+ ((flags & TRUN_SAMPLE_SIZE_PRESENT) != 0) as usize
+ ((flags & TRUN_SAMPLE_FLAGS_PRESENT) != 0) as usize
+ ((flags & TRUN_SAMPLE_COMPOSITION_TIME_OFFSETS_PRESENT) != 0) as usize;
let needed = sample_count.saturating_mul(4 * per_sample_fields);
if off + needed > body.len() {
return Err(Error::invalid("MP4: trun samples truncated"));
}
for _ in 0..sample_count {
let mut s = TrunSample::default();
if flags & TRUN_SAMPLE_DURATION_PRESENT != 0 {
s.duration = Some(u32::from_be_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
]));
off += 4;
}
if flags & TRUN_SAMPLE_SIZE_PRESENT != 0 {
s.size = Some(u32::from_be_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
]));
off += 4;
}
if flags & TRUN_SAMPLE_FLAGS_PRESENT != 0 {
s.flags = Some(u32::from_be_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
]));
off += 4;
}
if flags & TRUN_SAMPLE_COMPOSITION_TIME_OFFSETS_PRESENT != 0 {
let raw = [body[off], body[off + 1], body[off + 2], body[off + 3]];
let v = if version == 0 {
u32::from_be_bytes(raw) as i32
} else {
i32::from_be_bytes(raw)
};
s.composition_time_offset = Some(v);
off += 4;
}
out.samples.push(s);
}
Ok(out)
}
fn parse_sidx(body: &[u8], sidx_end_offset: u64) -> Result<Option<SidxRecord>> {
if body.len() < 12 {
return Err(Error::invalid("MP4: sidx too short"));
}
let version = body[0];
let mut off = 4usize; let reference_id = u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
off += 4;
let timescale = u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
off += 4;
let (ept, first_offset) = if version == 0 {
if off + 8 > body.len() {
return Err(Error::invalid("MP4: sidx v0 truncated"));
}
let e = u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]) as u64;
off += 4;
let f = u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]) as u64;
off += 4;
(e, f)
} else {
if off + 16 > body.len() {
return Err(Error::invalid("MP4: sidx v1 truncated"));
}
let e = u64::from_be_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
body[off + 4],
body[off + 5],
body[off + 6],
body[off + 7],
]);
off += 8;
let f = u64::from_be_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
body[off + 4],
body[off + 5],
body[off + 6],
body[off + 7],
]);
off += 8;
(e, f)
};
if off + 4 > body.len() {
return Err(Error::invalid("MP4: sidx header truncated"));
}
let reference_count = u16::from_be_bytes([body[off + 2], body[off + 3]]) as usize;
off += 4;
let needed = reference_count.saturating_mul(12);
if off + needed > body.len() {
return Err(Error::invalid("MP4: sidx references truncated"));
}
let mut references = Vec::with_capacity(reference_count);
for _ in 0..reference_count {
let r0 = u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
off += 4;
let r1 = u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
off += 4;
let r2 = u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
off += 4;
let is_sidx = (r0 & 0x8000_0000) != 0;
let referenced_size = r0 & 0x7FFF_FFFF;
let starts_with_sap = (r2 & 0x8000_0000) != 0;
let sap_type = ((r2 >> 28) & 0x7) as u8;
references.push(SidxReference {
is_sidx,
referenced_size,
subsegment_duration: r1,
starts_with_sap,
sap_type,
});
}
Ok(Some(SidxRecord {
reference_id,
timescale,
earliest_presentation_time: ept,
first_byte_offset: sidx_end_offset.saturating_add(first_offset),
references,
}))
}
fn parse_mfra(body: &[u8], out: &mut Vec<TfraRecord>) -> Result<()> {
let mut cur = std::io::Cursor::new(body);
let end = body.len() as u64;
while cur.position() < end {
let hdr = match read_box_header(&mut cur)? {
Some(h) => h,
None => break,
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
match hdr.fourcc {
TFRA => {
let b = read_bytes_vec(&mut cur, psz)?;
if let Some(r) = parse_tfra(&b)? {
out.push(r);
}
}
MFRO => {
cur.set_position(cur.position() + psz as u64);
}
_ => cur.set_position(cur.position() + psz as u64),
}
}
Ok(())
}
fn parse_tfra(body: &[u8]) -> Result<Option<TfraRecord>> {
if body.len() < 12 {
return Err(Error::invalid("MP4: tfra too short"));
}
let version = body[0];
let mut off = 4usize;
let track_id = u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
off += 4;
let lengths = u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
off += 4;
let len_traf = (((lengths >> 4) & 0x3) as usize) + 1;
let len_trun = (((lengths >> 2) & 0x3) as usize) + 1;
let len_sample = ((lengths & 0x3) as usize) + 1;
if off + 4 > body.len() {
return Err(Error::invalid("MP4: tfra entry_count truncated"));
}
let n = u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]) as usize;
off += 4;
let mut entries = Vec::with_capacity(n);
let entry_size = if version == 1 { 16 } else { 8 } + len_traf + len_trun + len_sample;
if off + n.saturating_mul(entry_size) > body.len() {
return Err(Error::invalid("MP4: tfra entries truncated"));
}
for _ in 0..n {
let (time, moof_offset) = if version == 1 {
let t = u64::from_be_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
body[off + 4],
body[off + 5],
body[off + 6],
body[off + 7],
]);
off += 8;
let m = u64::from_be_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
body[off + 4],
body[off + 5],
body[off + 6],
body[off + 7],
]);
off += 8;
(t, m)
} else {
let t =
u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]) as u64;
off += 4;
let m =
u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]) as u64;
off += 4;
(t, m)
};
let traf_number = read_var_u32(&body[off..off + len_traf]);
off += len_traf;
let trun_number = read_var_u32(&body[off..off + len_trun]);
off += len_trun;
let sample_number = read_var_u32(&body[off..off + len_sample]);
off += len_sample;
entries.push(TfraEntry {
time,
moof_offset,
traf_number,
trun_number,
sample_number,
});
}
Ok(Some(TfraRecord { track_id, entries }))
}
fn read_var_u32(buf: &[u8]) -> u32 {
let mut v: u32 = 0;
for &b in buf {
v = (v << 8) | b as u32;
}
v
}
#[derive(Clone, Copy, Debug)]
struct SampleRef {
track_idx: u32,
offset: u64,
size: u32,
pts: i64,
dts: i64,
duration: i64,
keyframe: bool,
}
fn expand_samples(t: &Track, track_idx: u32, out: &mut Vec<SampleRef>) -> Result<()> {
if t.stsz.is_empty() {
return Ok(());
}
let n_samples = t.stsz.len();
let mut pts = Vec::with_capacity(n_samples);
{
let mut i = 0;
let mut t_accum: i64 = 0;
for &(count, delta) in &t.stts {
for _ in 0..count {
if i >= n_samples {
break;
}
pts.push((t_accum, delta as i64));
t_accum += delta as i64;
i += 1;
}
}
while pts.len() < n_samples {
pts.push((t_accum, 0));
}
}
let mut cts_offsets: Vec<i64> = vec![0; n_samples];
if !t.ctts.is_empty() {
let mut i = 0usize;
for &(count, off) in &t.ctts {
for _ in 0..count {
if i >= n_samples {
break;
}
cts_offsets[i] = off as i64;
i += 1;
}
}
}
let mut chunk_of_sample = Vec::with_capacity(n_samples);
let mut sample_within_chunk = Vec::with_capacity(n_samples);
{
let mut sample_i = 0;
let mut chunk_i = 1u32;
let n_samples_u32 = u32::try_from(n_samples).unwrap_or(u32::MAX);
for entry_i in 0..t.stsc.len() {
let (fc, spc, _sdi) = t.stsc[entry_i];
let next_fc = t
.stsc
.get(entry_i + 1)
.map(|e| e.0)
.unwrap_or(t.chunk_offsets.len() as u32 + 1);
let spc_clamped = spc.min(n_samples_u32);
let mut ch = chunk_i.max(fc);
while ch < next_fc && sample_i < n_samples {
for s_in_ch in 0..spc_clamped {
if sample_i >= n_samples {
break;
}
chunk_of_sample.push(ch);
sample_within_chunk.push(s_in_ch);
sample_i += 1;
}
ch += 1;
}
chunk_i = ch;
}
while sample_within_chunk.len() < n_samples {
chunk_of_sample.push(*chunk_of_sample.last().unwrap_or(&1));
sample_within_chunk.push(0);
}
}
let stss_all_keyframes = t.stss.is_empty();
let stss_set: std::collections::HashSet<u32> = t.stss.iter().copied().collect();
for i in 0..n_samples {
let chunk = chunk_of_sample[i] as usize;
if chunk == 0 || chunk > t.chunk_offsets.len() {
return Err(Error::invalid(format!(
"MP4: chunk index {chunk} out of range (track {track_idx})"
)));
}
let chunk_off = t.chunk_offsets[chunk - 1];
let chunk_start_sample = i - sample_within_chunk[i] as usize;
let mut preceding: u64 = 0;
for j in chunk_start_sample..i {
preceding += t.stsz[j] as u64;
}
let size = t.stsz[i];
let (dts_v, dur) = pts[i];
let elst_shift = elst_leading_media_time(t);
let cts_v = dts_v
.saturating_add(cts_offsets[i])
.saturating_sub(elst_shift);
let dts_v_shifted = dts_v.saturating_sub(elst_shift);
let one_based = (i as u32) + 1;
let keyframe = stss_all_keyframes || stss_set.contains(&one_based);
out.push(SampleRef {
track_idx,
offset: chunk_off + preceding,
size,
pts: cts_v,
dts: dts_v_shifted,
duration: dur,
keyframe,
});
}
Ok(())
}
fn build_ctx<'a>(tag: &'a CodecTag, t: &'a Track) -> ProbeContext<'a> {
let mut ctx = ProbeContext::new(tag);
if !t.extradata.is_empty() {
ctx = ctx.header(&t.extradata);
}
if let Some(b) = t.sample_size_bits {
ctx = ctx.bits(b);
}
if let Some(c) = t.channels {
ctx = ctx.channels(c);
}
if let Some(sr) = t.sample_rate {
ctx = ctx.sample_rate(sr);
}
if let Some(w) = t.width {
ctx = ctx.width(w);
}
if let Some(h) = t.height {
ctx = ctx.height(h);
}
ctx
}
fn build_stream_info(index: u32, t: &Track, codecs: &dyn CodecResolver) -> StreamInfo {
let codec_id = {
let mut resolved: Option<CodecId> = None;
if let Some(oti) = t.esds_oti {
let tag = CodecTag::mp4_object_type(oti);
let ctx = build_ctx(&tag, t);
resolved = codecs.resolve_tag(&ctx);
}
if resolved.is_none() {
let tag = CodecTag::fourcc(&t.codec_id_fourcc);
let ctx = build_ctx(&tag, t);
resolved = codecs.resolve_tag(&ctx);
}
resolved.unwrap_or_else(|| match t.esds_oti {
Some(oti) => from_sample_entry_with_oti(&t.codec_id_fourcc, oti),
None => from_sample_entry(&t.codec_id_fourcc),
})
};
let mut params = match t.media_type {
MediaType::Audio => CodecParameters::audio(codec_id),
MediaType::Video => CodecParameters::video(codec_id),
_ => {
let mut p = CodecParameters::audio(codec_id);
p.media_type = MediaType::Data;
p
}
};
params.channels = t.channels;
params.sample_rate = t.sample_rate;
params.sample_format = match (params.codec_id.as_str(), t.sample_size_bits) {
("flac", Some(8)) => Some(SampleFormat::U8),
("flac", Some(16)) => Some(SampleFormat::S16),
("flac", Some(24)) => Some(SampleFormat::S24),
("flac", Some(32)) => Some(SampleFormat::S32),
("pcm_s16le", _) => Some(SampleFormat::S16),
_ => None,
};
params.width = t.width;
params.height = t.height;
params.extradata = t.extradata.clone();
let timescale = if t.timescale == 0 { 1 } else { t.timescale };
StreamInfo {
index,
time_base: TimeBase::new(1, timescale as i64),
duration: t.duration.map(|d| d as i64),
start_time: Some(0),
params,
}
}
struct Mp4Demuxer {
input: Box<dyn ReadSeek>,
streams: Vec<StreamInfo>,
samples: Vec<SampleRef>,
cursor: usize,
metadata: Vec<(String, String)>,
duration_micros: i64,
#[allow(dead_code)]
sidxes: Vec<SidxRecord>,
tfras: Vec<TfraRecord>,
#[allow(dead_code)]
movie_timescale: u32,
track_timescales: Vec<u32>,
track_ids: Vec<u32>,
}
impl Demuxer for Mp4Demuxer {
fn format_name(&self) -> &str {
"mp4"
}
fn streams(&self) -> &[StreamInfo] {
&self.streams
}
fn next_packet(&mut self) -> Result<Packet> {
if self.cursor >= self.samples.len() {
return Err(Error::Eof);
}
let s = self.samples[self.cursor];
self.cursor += 1;
self.input.seek(SeekFrom::Start(s.offset))?;
let mut data = vec![0u8; s.size as usize];
self.input.read_exact(&mut data)?;
let stream = &self.streams[s.track_idx as usize];
let mut pkt = Packet::new(s.track_idx, stream.time_base, data);
pkt.pts = Some(s.pts);
pkt.dts = Some(s.dts);
pkt.duration = Some(s.duration);
pkt.flags.keyframe = s.keyframe;
Ok(pkt)
}
fn seek_to(&mut self, stream_index: u32, pts: i64) -> Result<i64> {
if stream_index as usize >= self.streams.len() {
return Err(Error::invalid(format!(
"MP4: stream index {stream_index} out of range"
)));
}
if let Some(target) = self.tfra_seek_target(stream_index, pts) {
for (i, s) in self.samples.iter().enumerate() {
if s.track_idx != stream_index {
continue;
}
if s.offset >= target.moof_offset && s.keyframe {
self.cursor = i;
return Ok(s.pts);
}
}
}
let mut best_cursor: Option<usize> = None;
let mut best_pts: i64 = 0;
for (i, s) in self.samples.iter().enumerate() {
if s.track_idx != stream_index || !s.keyframe {
continue;
}
if s.pts <= pts {
if best_cursor.is_none() || s.pts >= best_pts {
best_cursor = Some(i);
best_pts = s.pts;
}
} else {
break;
}
}
if best_cursor.is_none() {
for (i, s) in self.samples.iter().enumerate() {
if s.track_idx == stream_index && s.keyframe {
best_cursor = Some(i);
best_pts = s.pts;
break;
}
}
}
let cursor = best_cursor.ok_or_else(|| {
Error::unsupported(format!(
"MP4: no keyframes in stream {stream_index} to seek to"
))
})?;
self.cursor = cursor;
Ok(best_pts)
}
fn metadata(&self) -> &[(String, String)] {
&self.metadata
}
fn duration_micros(&self) -> Option<i64> {
if self.duration_micros > 0 {
Some(self.duration_micros)
} else {
None
}
}
}
impl Mp4Demuxer {
fn tfra_seek_target(&self, stream_index: u32, pts: i64) -> Option<TfraEntry> {
if self.tfras.is_empty() {
return None;
}
let track_id = self.track_ids.get(stream_index as usize)?;
let tfra = self.tfras.iter().find(|t| t.track_id == *track_id)?;
if tfra.entries.is_empty() {
return None;
}
let _ts = self.track_timescales.get(stream_index as usize)?;
if pts < 0 {
return Some(tfra.entries[0]);
}
let target = pts as u64;
match tfra.entries.binary_search_by_key(&target, |e| e.time) {
Ok(i) => Some(tfra.entries[i]),
Err(i) => {
if i == 0 {
Some(tfra.entries[0])
} else {
Some(tfra.entries[i - 1])
}
}
}
}
}
pub fn parse_sidx_box(body: &[u8], sidx_end_offset: u64) -> Result<Option<SidxRecord>> {
parse_sidx(body, sidx_end_offset)
}
pub fn parse_mfra_box(body: &[u8]) -> Result<Vec<TfraRecord>> {
let mut out = Vec::new();
parse_mfra(body, &mut out)?;
Ok(out)
}
use std::io::Read;
fn read_bytes_vec<R: Read + ?Sized>(r: &mut R, n: usize) -> Result<Vec<u8>> {
let mut buf = vec![0u8; n];
r.read_exact(&mut buf)?;
Ok(buf)
}
#[allow(dead_code)]
fn _unused() -> (HashSet<u32>, SeekFrom) {
(HashSet::new(), SeekFrom::Start(0))
}
#[cfg(test)]
mod tests {
use super::parse_esds_dsi;
fn build_esds_payload(asc: &[u8]) -> Vec<u8> {
let mut dsi = Vec::new();
dsi.push(0x05);
dsi.push(asc.len() as u8);
dsi.extend_from_slice(asc);
let mut dcd = vec![
0x04,
(13 + dsi.len()) as u8,
0x40, (0x05 << 2) | 0x01, ];
dcd.extend_from_slice(&[0, 0, 0]); dcd.extend_from_slice(&[0, 0, 0, 0]); dcd.extend_from_slice(&[0, 0, 0, 0]); dcd.extend_from_slice(&dsi);
let slc = vec![0x06, 0x01, 0x02];
let mut esd = Vec::new();
esd.push(0x03);
esd.push((3 + dcd.len() + slc.len()) as u8);
esd.extend_from_slice(&[0, 0, 0]); esd.extend_from_slice(&dcd);
esd.extend_from_slice(&slc);
esd
}
#[test]
fn extracts_asc_from_esds() {
let asc = [0x12, 0x10];
let payload = build_esds_payload(&asc);
let got = parse_esds_dsi(&payload).expect("dsi");
assert_eq!(got, asc);
}
#[test]
fn handles_ber_multi_byte_length() {
let asc = [0x11, 0x90];
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0]);
let mut dsi = vec![0x05, asc.len() as u8];
dsi.extend_from_slice(&asc);
let mut dcd = vec![0x04, (13 + dsi.len()) as u8, 0x40, (0x05 << 2) | 0x01];
dcd.extend_from_slice(&[0, 0, 0]);
dcd.extend_from_slice(&[0, 0, 0, 0]);
dcd.extend_from_slice(&[0, 0, 0, 0]);
dcd.extend_from_slice(&dsi);
body.extend_from_slice(&dcd);
body.extend_from_slice(&[0x06, 0x01, 0x02]);
let body_len = body.len();
assert!(body_len < 128);
let hi = (body_len >> 7) as u8 | 0x80;
let lo = (body_len & 0x7F) as u8;
let mut payload = vec![0x03, hi, lo];
payload.extend_from_slice(&body);
let got = parse_esds_dsi(&payload).expect("dsi");
assert_eq!(got, asc);
}
#[test]
fn rejects_non_es_descriptor() {
let payload = vec![0x04, 0x01, 0x00];
assert!(parse_esds_dsi(&payload).is_none());
}
fn build_audio_sample_entry(child_fourcc: &[u8; 4], child_body: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(28 + 8 + child_body.len());
out.extend_from_slice(&[0u8; 6]);
out.extend_from_slice(&1u16.to_be_bytes()); out.extend_from_slice(&[0u8; 8]); out.extend_from_slice(&2u16.to_be_bytes()); out.extend_from_slice(&16u16.to_be_bytes()); out.extend_from_slice(&[0u8; 4]); out.extend_from_slice(&((48_000u32) << 16).to_be_bytes()); let total = (8 + child_body.len()) as u32;
out.extend_from_slice(&total.to_be_bytes());
out.extend_from_slice(child_fourcc);
out.extend_from_slice(child_body);
out
}
fn fresh_track() -> super::Track {
super::Track {
track_id: 0,
media_type: oxideav_core::MediaType::Audio,
codec_id_fourcc: [0; 4],
timescale: 0,
duration: None,
channels: None,
sample_rate: None,
sample_size_bits: None,
width: None,
height: None,
extradata: Vec::new(),
esds_oti: None,
stts: Vec::new(),
stsc: Vec::new(),
stsz: Vec::new(),
chunk_offsets: Vec::new(),
stss: Vec::new(),
ctts: Vec::new(),
elst: Vec::new(),
trex: super::TrexDefaults::default(),
}
}
#[test]
fn surfaces_dac3_box_as_extradata() {
let dac3 = [0x10, 0x4C, 0x40];
let entry = build_audio_sample_entry(b"dac3", &dac3);
let mut t = fresh_track();
super::parse_audio_sample_entry(&entry, &mut t).unwrap();
assert_eq!(t.extradata, dac3, "dac3 body should be surfaced verbatim");
assert_eq!(t.channels, Some(2));
assert_eq!(t.sample_rate, Some(48_000));
}
#[test]
fn surfaces_dec3_box_as_extradata() {
let dec3 = [0x07, 0xC0, 0x20, 0x00, 0x00];
let entry = build_audio_sample_entry(b"dec3", &dec3);
let mut t = fresh_track();
super::parse_audio_sample_entry(&entry, &mut t).unwrap();
assert_eq!(t.extradata, dec3, "dec3 body should be surfaced verbatim");
}
#[test]
fn expand_samples_clamps_giant_samples_per_chunk() {
let mut t = fresh_track();
t.stsz = vec![1, 1, 1, 1]; t.stts = vec![(4, 100)]; t.stsc = vec![(1, u32::MAX, 1)]; t.chunk_offsets = vec![0, 100, 200, 300]; let mut out = Vec::new();
let start = std::time::Instant::now();
super::expand_samples(&t, 0, &mut out).unwrap();
let elapsed = start.elapsed();
assert_eq!(out.len(), 4, "should yield exactly 4 samples");
assert!(
elapsed.as_millis() < 100,
"expand_samples spun on adversarial spc: took {elapsed:?}",
);
}
#[test]
fn parse_tfdt_v0_carries_32bit_bmdt() {
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&12_345u32.to_be_bytes());
let bmdt = super::parse_tfdt(&body).unwrap();
assert_eq!(bmdt, 12_345);
}
#[test]
fn parse_tfdt_v1_carries_64bit_bmdt() {
let mut body = Vec::new();
body.extend_from_slice(&[1, 0, 0, 0]); body.extend_from_slice(&0x0000_0001_2345_6789u64.to_be_bytes());
let bmdt = super::parse_tfdt(&body).unwrap();
assert_eq!(bmdt, 0x0000_0001_2345_6789);
}
#[test]
fn parse_trun_extracts_sample_count_size_duration() {
let flags: u32 =
TRUN_DATA_OFFSET_PRESENT | TRUN_SAMPLE_DURATION_PRESENT | TRUN_SAMPLE_SIZE_PRESENT;
let mut body = Vec::new();
body.push(0); body.extend_from_slice(&flags.to_be_bytes()[1..4]); body.extend_from_slice(&3u32.to_be_bytes()); body.extend_from_slice(&0x12345678i32.to_be_bytes()); for (dur, sz) in [(100u32, 50u32), (200, 60), (300, 70)] {
body.extend_from_slice(&dur.to_be_bytes());
body.extend_from_slice(&sz.to_be_bytes());
}
let parsed = super::parse_trun(&body).unwrap();
assert_eq!(parsed.data_offset, Some(0x12345678));
assert_eq!(parsed.samples.len(), 3);
assert_eq!(parsed.samples[0].duration, Some(100));
assert_eq!(parsed.samples[0].size, Some(50));
assert_eq!(parsed.samples[2].duration, Some(300));
assert_eq!(parsed.samples[2].size, Some(70));
}
use super::{TRUN_DATA_OFFSET_PRESENT, TRUN_SAMPLE_DURATION_PRESENT, TRUN_SAMPLE_SIZE_PRESENT};
#[test]
fn parse_trun_v1_signed_composition_offset() {
let flags: u32 = TRUN_SAMPLE_COMPOSITION_TIME_OFFSETS_PRESENT;
let mut body = Vec::new();
body.push(1); body.extend_from_slice(&flags.to_be_bytes()[1..4]);
body.extend_from_slice(&2u32.to_be_bytes()); body.extend_from_slice(&(-50i32).to_be_bytes());
body.extend_from_slice(&(75i32).to_be_bytes());
let parsed = super::parse_trun(&body).unwrap();
assert_eq!(parsed.samples[0].composition_time_offset, Some(-50));
assert_eq!(parsed.samples[1].composition_time_offset, Some(75));
}
use super::TRUN_SAMPLE_COMPOSITION_TIME_OFFSETS_PRESENT;
#[test]
fn parse_trex_populates_track_defaults() {
let mut t = fresh_track();
t.track_id = 7;
let mut tracks = vec![t];
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&7u32.to_be_bytes()); body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&1024u32.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); super::parse_trex(&body, &mut tracks).unwrap();
assert_eq!(tracks[0].trex.default_sample_duration, 1024);
assert_eq!(tracks[0].trex.default_sample_description_index, 1);
}
#[test]
fn parse_elst_v0_multi_segment() {
let mut body = Vec::new();
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&2u32.to_be_bytes());
body.extend_from_slice(&1000u32.to_be_bytes());
body.extend_from_slice(&(-1i32).to_be_bytes());
body.extend_from_slice(&0x0001_0000u32.to_be_bytes());
body.extend_from_slice(&2000u32.to_be_bytes());
body.extend_from_slice(&500i32.to_be_bytes());
body.extend_from_slice(&0x0001_0000u32.to_be_bytes());
let mut t = fresh_track();
super::parse_elst(&body, &mut t).unwrap();
assert_eq!(t.elst.len(), 2);
assert_eq!(t.elst[0].media_time, -1);
assert_eq!(t.elst[1].media_time, 500);
assert_eq!(t.elst[1].segment_duration, 2000);
assert_eq!(super::elst_leading_media_time(&t), 500);
}
}