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::cenc::{parse_pssh, parse_senc, parse_tenc, PsshBox, SencBox, TencBox};
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();
let mut prfts: Vec<PrftRecord> = 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)?;
}
PRFT => {
let body = read_box_body(&mut *input, &hdr)?;
if let Some(r) = parse_prft(&body)? {
prfts.push(r);
}
}
_ => 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;
}
}
let mut senc_records: Vec<SencRecord> = Vec::new();
for moof in &moofs {
parse_moof(
moof,
&parsed.tracks,
&mut samples,
&mut next_dts,
&mut senc_records,
)?;
}
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
};
let mut metadata = parsed.metadata;
for (n, p) in prfts.iter().enumerate() {
metadata.push((
format!("prft_{n}"),
format!(
"{} {} {}",
p.reference_track_id, p.ntp_timestamp, p.media_time
),
));
}
for (n, p) in parsed.psshes.iter().enumerate() {
let sysid_hex: String = p.system_id.iter().map(|b| format!("{b:02x}")).collect();
metadata.push((
format!("pssh_{n}"),
format!("{} {} {}", sysid_hex, p.kids.len(), p.data.len()),
));
}
for (n, r) in senc_records.iter().enumerate() {
metadata.push((
format!("senc_{n}"),
format!(
"track={} seq={} samples={} flags=0x{:08x}",
r.track_idx,
r.moof_sequence,
r.senc.samples.len(),
r.senc.flags,
),
));
}
Ok(Box::new(Mp4Demuxer {
input,
streams,
samples,
cursor: 0,
metadata,
duration_micros,
sidxes,
tfras,
prfts,
psshes: parsed.psshes,
senc_records,
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(Clone, Copy, Debug)]
pub struct PrftRecord {
pub reference_track_id: u32,
pub ntp_timestamp: u64,
pub media_time: u64,
pub version: u8,
}
#[derive(Default)]
struct ParsedMoov {
tracks: Vec<Track>,
movie_timescale: u32,
movie_duration: u64,
metadata: Vec<(String, String)>,
psshes: Vec<PsshBox>,
}
#[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,
protection_scheme: Option<[u8; 4]>,
tenc: Option<TencBox>,
tref: Vec<([u8; 4], Vec<u32>)>,
elng: Option<String>,
kinds: Vec<(String, String)>,
cslg: Option<CslgBox>,
stsh: Vec<(u32, u32)>,
sbgp: Vec<SbgpBox>,
sgpd: Vec<SgpdBox>,
sdtp: Vec<SdtpEntry>,
subs: Vec<SubsBox>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct SdtpEntry {
is_leading: u8,
sample_depends_on: u8,
sample_is_depended_on: u8,
sample_has_redundancy: u8,
}
#[derive(Clone, Debug, Default)]
struct SbgpBox {
grouping_type: [u8; 4],
grouping_type_parameter: Option<u32>,
entries: Vec<(u32, u32)>,
}
#[derive(Clone, Debug, Default)]
struct SgpdBox {
grouping_type: [u8; 4],
default_sample_description_index: Option<u32>,
entries: Vec<Vec<u8>>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct SubSampleEntry {
subsample_size: u32,
subsample_priority: u8,
discardable: u8,
codec_specific_parameters: u32,
}
#[derive(Clone, Debug, Default)]
struct SubsEntry {
sample_delta: u32,
subsamples: Vec<SubSampleEntry>,
}
#[derive(Clone, Debug, Default)]
struct SubsBox {
version: u8,
flags: u32,
entries: Vec<SubsEntry>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct CslgBox {
composition_to_dts_shift: i64,
least_decode_to_display_delta: i64,
greatest_decode_to_display_delta: i64,
composition_start_time: i64,
composition_end_time: i64,
}
#[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)?;
}
PSSH => {
let body = read_bytes_vec(&mut cur, psz)?;
if let Ok(p) = parse_pssh(&body) {
out.psshes.push(p);
}
}
_ => {
skip_cursor_bytes(&mut cur, psz);
}
}
}
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)?;
}
_ => skip_cursor_bytes(&mut cur, psz),
}
}
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(),
protection_scheme: None,
tenc: None,
tref: Vec::new(),
elng: None,
kinds: Vec::new(),
cslg: None,
stsh: Vec::new(),
sbgp: Vec::new(),
sgpd: Vec::new(),
sdtp: Vec::new(),
subs: Vec::new(),
};
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)?;
}
TREF => {
let sub = read_bytes_vec(&mut cur, psz)?;
parse_tref(&sub, &mut t)?;
}
UDTA => {
let sub = read_bytes_vec(&mut cur, psz)?;
parse_track_udta(&sub, &mut t);
}
_ => {
skip_cursor_bytes(&mut cur, psz);
}
}
}
if has_media {
Ok(Some(t))
} else {
Ok(None)
}
}
fn parse_track_udta(body: &[u8], t: &mut Track) {
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);
if hdr.fourcc == KIND {
parse_kind(&body[start..start + psz], t);
}
}
}
fn parse_kind(body: &[u8], t: &mut Track) {
if body.len() < 4 {
return;
}
let s = &body[4..];
let uri_end = s.iter().position(|&b| b == 0).unwrap_or(s.len());
let uri = match std::str::from_utf8(&s[..uri_end]) {
Ok(u) => u.to_string(),
Err(_) => return,
};
let value = if uri_end >= s.len() {
String::new()
} else {
let rest = &s[uri_end + 1..];
let val_end = rest.iter().position(|&b| b == 0).unwrap_or(rest.len());
match std::str::from_utf8(&rest[..val_end]) {
Ok(v) => v.to_string(),
Err(_) => return,
}
};
if uri.is_empty() {
return;
}
t.kinds.push((uri, value));
}
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_tref(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 child = read_bytes_vec(&mut cur, psz)?;
if child.len() % 4 != 0 {
return Err(Error::invalid("MP4: tref child not a multiple of 4 bytes"));
}
let mut ids = Vec::with_capacity(child.len() / 4);
for chunk in child.chunks_exact(4) {
let id = u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
if id != 0 {
ids.push(id);
}
}
if !t.tref.iter().any(|(ty, _)| *ty == hdr.fourcc) {
t.tref.push((hdr.fourcc, ids));
}
}
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)?;
}
_ => skip_cursor_bytes(&mut cur, psz),
}
}
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)?;
}
ELNG => {
let b = read_bytes_vec(&mut cur, psz)?;
parse_elng(&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)?;
}
_ => skip_cursor_bytes(&mut cur, psz),
}
}
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_elng(body: &[u8], t: &mut Track) {
if body.len() < 4 {
return;
}
let s = &body[4..];
let end = s.iter().position(|&b| b == 0).unwrap_or(s.len());
if let Ok(tag) = std::str::from_utf8(&s[..end]) {
if !tag.is_empty() {
t.elng = Some(tag.to_string());
}
}
}
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,
h if *h == HANDLER_SUBT || *h == HANDLER_SBTL || *h == HANDLER_TEXT => MediaType::Subtitle,
h if *h == HANDLER_META => MediaType::Data,
_ => 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)?;
}
_ => skip_cursor_bytes(&mut cur, psz),
}
}
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)?,
STSH => t.stsh = parse_stsh(&b)?,
SDTP => t.sdtp = parse_sdtp(&b)?,
CTTS => t.ctts = parse_ctts(&b)?,
CSLG => t.cslg = Some(parse_cslg(&b)?),
SBGP => t.sbgp.push(parse_sbgp(&b)?),
SGPD => t.sgpd.push(parse_sgpd(&b)?),
SUBS => t.subs.push(parse_subs(&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;
if matches!(hdr.fourcc, ENCV | ENCA | ENCT | ENCS) {
let unwrap = parse_sinf_for_original_format(&entry, t.media_type)?;
if let Some(fourcc) = unwrap.original_format {
t.codec_id_fourcc = fourcc;
}
t.protection_scheme = unwrap.scheme_type;
t.tenc = unwrap.tenc;
}
parse_sample_entry(&entry, t)?;
Ok(())
}
struct SinfUnwrap {
original_format: Option<[u8; 4]>,
scheme_type: Option<[u8; 4]>,
tenc: Option<TencBox>,
}
impl SinfUnwrap {
fn empty() -> Self {
SinfUnwrap {
original_format: None,
scheme_type: None,
tenc: None,
}
}
}
fn parse_sinf_for_original_format(entry: &[u8], media_type: MediaType) -> Result<SinfUnwrap> {
let preamble = match media_type {
MediaType::Audio => 28,
MediaType::Video => 78,
_ => return Ok(SinfUnwrap::empty()),
};
if entry.len() <= preamble {
return Ok(SinfUnwrap::empty());
}
let mut cur = std::io::Cursor::new(&entry[preamble..]);
let end = (entry.len() - preamble) 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)?;
if hdr.fourcc == SINF {
return parse_sinf_body(&body);
}
}
Ok(SinfUnwrap::empty())
}
fn parse_sinf_body(body: &[u8]) -> Result<SinfUnwrap> {
let mut out = SinfUnwrap::empty();
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 inner = read_bytes_vec(&mut cur, psz)?;
match hdr.fourcc {
FRMA if inner.len() >= 4 => {
out.original_format = Some([inner[0], inner[1], inner[2], inner[3]]);
}
SCHM if inner.len() >= 8 => {
out.scheme_type = Some([inner[4], inner[5], inner[6], inner[7]]);
}
SCHI => {
out.tenc = walk_schi_for_tenc(&inner);
}
_ => {}
}
}
Ok(out)
}
fn walk_schi_for_tenc(body: &[u8]) -> Option<TencBox> {
let mut cur = std::io::Cursor::new(body);
let end = body.len() as u64;
while cur.position() < end {
let hdr = read_box_header(&mut cur).ok().flatten()?;
let psz = hdr.payload_size().unwrap_or(0) as usize;
let inner = read_bytes_vec(&mut cur, psz).ok()?;
if hdr.fourcc == TENC {
return parse_tenc(&inner).ok();
}
}
None
}
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),
MediaType::Subtitle => parse_subtitle_sample_entry(entry, t),
_ => Ok(()),
}
}
fn parse_subtitle_sample_entry(entry: &[u8], t: &mut Track) -> Result<()> {
if entry.len() < 8 {
return Ok(());
}
match &t.codec_id_fourcc {
b"stpp" | b"sbtt" | b"stxt" => {
t.extradata = entry[8..].to_vec();
}
b"wvtt" => {
t.extradata = entry[8..].to_vec();
}
b"tx3g" | b"text" | b"c608" | b"c708" => {
t.extradata = entry[8..].to_vec();
}
_ => {}
}
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_stsh(body: &[u8]) -> Result<Vec<(u32, u32)>> {
if body.len() < 8 {
return Err(Error::invalid("MP4: stsh too short"));
}
let count = u32::from_be_bytes([body[4], body[5], body[6], body[7]]) as usize;
let max_entries = (body.len() - 8) / 8;
let mut out = Vec::with_capacity(count.min(max_entries));
let mut off = 8;
for _ in 0..count {
if off + 8 > body.len() {
return Err(Error::invalid("MP4: stsh truncated"));
}
let shadowed = u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
let sync = u32::from_be_bytes([body[off + 4], body[off + 5], body[off + 6], body[off + 7]]);
out.push((shadowed, sync));
off += 8;
}
Ok(out)
}
fn parse_sdtp(body: &[u8]) -> Result<Vec<SdtpEntry>> {
if body.len() < 4 {
return Err(Error::invalid("MP4: sdtp too short"));
}
let payload = &body[4..];
let mut out = Vec::with_capacity(payload.len());
for &byte in payload {
out.push(SdtpEntry {
is_leading: (byte >> 6) & 0x03,
sample_depends_on: (byte >> 4) & 0x03,
sample_is_depended_on: (byte >> 2) & 0x03,
sample_has_redundancy: byte & 0x03,
});
}
Ok(out)
}
fn parse_sbgp(body: &[u8]) -> Result<SbgpBox> {
if body.len() < 4 {
return Err(Error::invalid("MP4: sbgp too short"));
}
let version = body[0];
let mut off = 4;
let read_u32 = |b: &[u8], o: usize| -> Option<u32> {
b.get(o..o + 4)
.map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]]))
};
let grouping_type_raw =
read_u32(body, off).ok_or_else(|| Error::invalid("MP4: sbgp grouping_type truncated"))?;
let grouping_type = grouping_type_raw.to_be_bytes();
off += 4;
let grouping_type_parameter = if version == 1 {
let p = read_u32(body, off)
.ok_or_else(|| Error::invalid("MP4: sbgp grouping_type_parameter truncated"))?;
off += 4;
Some(p)
} else {
None
};
let count = read_u32(body, off)
.ok_or_else(|| Error::invalid("MP4: sbgp entry_count truncated"))? as usize;
off += 4;
let max_entries = body.len().saturating_sub(off) / 8;
let mut entries = Vec::with_capacity(count.min(max_entries));
for _ in 0..count {
if off + 8 > body.len() {
return Err(Error::invalid("MP4: sbgp entries truncated"));
}
let sample_count =
u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
let gdi = u32::from_be_bytes([body[off + 4], body[off + 5], body[off + 6], body[off + 7]]);
entries.push((sample_count, gdi));
off += 8;
}
Ok(SbgpBox {
grouping_type,
grouping_type_parameter,
entries,
})
}
fn parse_sgpd(body: &[u8]) -> Result<SgpdBox> {
if body.len() < 4 {
return Err(Error::invalid("MP4: sgpd too short"));
}
let version = body[0];
let mut off = 4;
let read_u32 = |b: &[u8], o: usize| -> Option<u32> {
b.get(o..o + 4)
.map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]]))
};
let grouping_type_raw =
read_u32(body, off).ok_or_else(|| Error::invalid("MP4: sgpd grouping_type truncated"))?;
let grouping_type = grouping_type_raw.to_be_bytes();
off += 4;
let default_length = if version == 1 {
let dl = read_u32(body, off)
.ok_or_else(|| Error::invalid("MP4: sgpd default_length truncated"))?;
off += 4;
dl
} else {
0
};
let default_sample_description_index = if version >= 2 {
let d = read_u32(body, off).ok_or_else(|| {
Error::invalid("MP4: sgpd default_sample_description_index truncated")
})?;
off += 4;
Some(d)
} else {
None
};
let count = read_u32(body, off)
.ok_or_else(|| Error::invalid("MP4: sgpd entry_count truncated"))? as usize;
off += 4;
let mut entries: Vec<Vec<u8>> = Vec::new();
if version == 1 && default_length == 0 {
for _ in 0..count {
let len = read_u32(body, off)
.ok_or_else(|| Error::invalid("MP4: sgpd description_length truncated"))?
as usize;
off += 4;
if off + len > body.len() {
return Err(Error::invalid("MP4: sgpd variable entry truncated"));
}
entries.push(body[off..off + len].to_vec());
off += len;
}
} else if default_length > 0 {
let len = default_length as usize;
for _ in 0..count {
if off + len > body.len() {
return Err(Error::invalid("MP4: sgpd fixed entry truncated"));
}
entries.push(body[off..off + len].to_vec());
off += len;
}
} else {
if count > 0 && off < body.len() {
entries.push(body[off..].to_vec());
}
}
Ok(SgpdBox {
grouping_type,
default_sample_description_index,
entries,
})
}
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_subs(body: &[u8]) -> Result<SubsBox> {
if body.len() < 8 {
return Err(Error::invalid("MP4: subs too short"));
}
let version = body[0];
let flags = u32::from_be_bytes([0, body[1], body[2], body[3]]);
let entry_count = u32::from_be_bytes([body[4], body[5], body[6], body[7]]) as usize;
let max_entries = body.len().saturating_sub(8) / 6;
let mut entries: Vec<SubsEntry> = Vec::with_capacity(entry_count.min(max_entries));
let size_width = if version == 1 { 4 } else { 2 };
let per_sub_min = size_width + 1 + 1 + 4; let mut off = 8;
for _ in 0..entry_count {
if off + 6 > body.len() {
return Err(Error::invalid("MP4: subs entry header truncated"));
}
let sample_delta =
u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
off += 4;
let subsample_count = u16::from_be_bytes([body[off], body[off + 1]]) as usize;
off += 2;
let max_subs = (body.len().saturating_sub(off)) / per_sub_min;
let mut subs_vec: Vec<SubSampleEntry> = Vec::with_capacity(subsample_count.min(max_subs));
for _ in 0..subsample_count {
if off + per_sub_min > body.len() {
return Err(Error::invalid("MP4: subs subsample truncated"));
}
let subsample_size = if version == 1 {
let v =
u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
off += 4;
v
} else {
let v = u16::from_be_bytes([body[off], body[off + 1]]) as u32;
off += 2;
v
};
let subsample_priority = body[off];
off += 1;
let discardable = body[off];
off += 1;
let codec_specific_parameters =
u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
off += 4;
subs_vec.push(SubSampleEntry {
subsample_size,
subsample_priority,
discardable,
codec_specific_parameters,
});
}
entries.push(SubsEntry {
sample_delta,
subsamples: subs_vec,
});
}
Ok(SubsBox {
version,
flags,
entries,
})
}
fn parse_cslg(body: &[u8]) -> Result<CslgBox> {
if body.len() < 4 {
return Err(Error::invalid("MP4: cslg too short"));
}
let version = body[0];
let mut off = 4;
let mut take = |width: usize, b: &[u8]| -> Result<i64> {
if off + width > b.len() {
return Err(Error::invalid("MP4: cslg truncated"));
}
let v = if width == 4 {
i32::from_be_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]]) as i64
} else {
i64::from_be_bytes([
b[off],
b[off + 1],
b[off + 2],
b[off + 3],
b[off + 4],
b[off + 5],
b[off + 6],
b[off + 7],
])
};
off += width;
Ok(v)
};
let w = if version == 0 { 4 } else { 8 };
Ok(CslgBox {
composition_to_dts_shift: take(w, body)?,
least_decode_to_display_delta: take(w, body)?,
greatest_decode_to_display_delta: take(w, body)?,
composition_start_time: take(w, body)?,
composition_end_time: take(w, body)?,
})
}
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)
}
#[derive(Clone, Debug)]
pub struct SencRecord {
pub track_idx: u32,
pub moof_sequence: u32,
pub senc: SencBox,
}
fn parse_moof(
moof: &MoofRecord,
tracks: &[Track],
samples: &mut Vec<SampleRef>,
next_dts: &mut [i64],
senc_records: &mut Vec<SencRecord>,
) -> Result<()> {
let mut cur = std::io::Cursor::new(&moof.body);
let end = moof.body.len() as u64;
let mut moof_sequence: u32 = 0;
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 => {
let body = read_bytes_vec(&mut cur, psz)?;
if body.len() >= 8 {
moof_sequence = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
}
}
TRAF => {
let body = read_bytes_vec(&mut cur, psz)?;
parse_traf(
&body,
moof.moof_start,
tracks,
samples,
next_dts,
moof_sequence,
senc_records,
)?;
}
_ => skip_cursor_bytes(&mut cur, psz),
}
}
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],
moof_sequence: u32,
senc_records: &mut Vec<SencRecord>,
) -> Result<()> {
let mut state = TrafState::default();
let mut tfhd_seen = false;
let mut senc_body: Option<Vec<u8>> = None;
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)?);
}
SENC => {
senc_body = Some(read_bytes_vec(&mut cur, psz)?);
}
TRUN => skip_cursor_bytes(&mut cur, psz),
_ => skip_cursor_bytes(&mut cur, psz),
}
}
if !tfhd_seen {
return Err(Error::invalid("MP4: traf missing tfhd"));
}
let track = &tracks[state.track_idx];
if let Some(body) = senc_body {
if let Some(tenc) = &track.tenc {
if let Ok(senc) = parse_senc(&body, tenc.default_per_sample_iv_size) {
senc_records.push(SencRecord {
track_idx: state.track_idx as u32,
moof_sequence,
senc,
});
}
}
}
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 {
skip_cursor_bytes(&mut cur, psz);
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_prft(body: &[u8]) -> Result<Option<PrftRecord>> {
if body.len() < 16 {
return Err(Error::invalid("MP4: prft too short"));
}
let version = body[0];
let reference_track_id = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
let ntp_timestamp = u64::from_be_bytes([
body[8], body[9], body[10], body[11], body[12], body[13], body[14], body[15],
]);
let media_time: u64 = if version == 0 {
if body.len() < 20 {
return Err(Error::invalid("MP4: prft v0 truncated"));
}
u32::from_be_bytes([body[16], body[17], body[18], body[19]]) as u64
} else {
if body.len() < 24 {
return Err(Error::invalid("MP4: prft v1 truncated"));
}
u64::from_be_bytes([
body[16], body[17], body[18], body[19], body[20], body[21], body[22], body[23],
])
};
Ok(Some(PrftRecord {
reference_track_id,
ntp_timestamp,
media_time,
version,
}))
}
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 => {
skip_cursor_bytes(&mut cur, psz);
}
_ => skip_cursor_bytes(&mut cur, psz),
}
}
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),
MediaType::Subtitle => CodecParameters::subtitle(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();
if let Some(scheme) = t.protection_scheme {
let scheme_str = std::str::from_utf8(&scheme).unwrap_or("????").to_string();
params.options.insert("protection_scheme", scheme_str);
}
if let Some(tenc) = &t.tenc {
let kid_hex: String = tenc
.default_kid
.iter()
.map(|b| format!("{b:02x}"))
.collect();
params.options.insert("cenc_default_kid", kid_hex);
params.options.insert(
"cenc_default_is_protected",
tenc.default_is_protected.to_string(),
);
params.options.insert(
"cenc_default_iv_size",
tenc.default_per_sample_iv_size.to_string(),
);
params
.options
.insert("cenc_tenc_version", tenc.version.to_string());
if tenc.version >= 1 {
params.options.insert(
"cenc_default_crypt_byte_block",
tenc.default_crypt_byte_block.to_string(),
);
params.options.insert(
"cenc_default_skip_byte_block",
tenc.default_skip_byte_block.to_string(),
);
}
if let Some(civ) = &tenc.default_constant_iv {
let iv_hex: String = civ.iter().map(|b| format!("{b:02x}")).collect();
params.options.insert("cenc_default_constant_iv", iv_hex);
}
}
if let Some(tag) = &t.elng {
params.options.insert("language", tag.clone());
}
for (ref_type, ids) in &t.tref {
if ids.is_empty() {
continue;
}
let type_str = std::str::from_utf8(ref_type)
.map(|s| s.to_string())
.unwrap_or_else(|_| {
format!(
"{:02x}{:02x}{:02x}{:02x}",
ref_type[0], ref_type[1], ref_type[2], ref_type[3]
)
});
let value = ids
.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(" ");
params.options.insert(format!("tref_{}", type_str), value);
}
for (i, (uri, value)) in t.kinds.iter().enumerate() {
let v = if value.is_empty() {
uri.clone()
} else {
format!("{} {}", uri, value)
};
params.options.insert(format!("kind_{}", i), v);
}
if let Some(c) = &t.cslg {
params.options.insert(
"cslg_composition_to_dts_shift",
c.composition_to_dts_shift.to_string(),
);
params.options.insert(
"cslg_least_decode_to_display_delta",
c.least_decode_to_display_delta.to_string(),
);
params.options.insert(
"cslg_greatest_decode_to_display_delta",
c.greatest_decode_to_display_delta.to_string(),
);
params.options.insert(
"cslg_composition_start_time",
c.composition_start_time.to_string(),
);
params.options.insert(
"cslg_composition_end_time",
c.composition_end_time.to_string(),
);
}
for (i, (shadowed, sync)) in t.stsh.iter().enumerate() {
params
.options
.insert(format!("stsh_{}", i), format!("{} {}", shadowed, sync));
}
let render_grouping_type = |gt: &[u8; 4]| -> String {
std::str::from_utf8(gt)
.ok()
.filter(|s| s.chars().all(|c| !c.is_control()))
.map(|s| s.to_string())
.unwrap_or_else(|| format!("{:02x}{:02x}{:02x}{:02x}", gt[0], gt[1], gt[2], gt[3]))
};
for (i, sb) in t.sbgp.iter().enumerate() {
let mut v = render_grouping_type(&sb.grouping_type);
if let Some(p) = sb.grouping_type_parameter {
v.push_str(&format!(" param={}", p));
}
for (count, idx) in &sb.entries {
v.push_str(&format!(" {}:{}", count, idx));
}
params.options.insert(format!("sbgp_{}", i), v);
}
for (i, sg) in t.sgpd.iter().enumerate() {
let mut v = render_grouping_type(&sg.grouping_type);
if let Some(d) = sg.default_sample_description_index {
v.push_str(&format!(" default={}", d));
}
for entry in &sg.entries {
v.push(' ');
for byte in entry {
v.push_str(&format!("{:02x}", byte));
}
}
params.options.insert(format!("sgpd_{}", i), v);
}
if !t.sdtp.is_empty() {
let mut leading = 0u32;
let mut independent = 0u32;
let mut disposable = 0u32;
let mut redundant = 0u32;
for e in &t.sdtp {
if e.is_leading == 1 || e.is_leading == 3 {
leading += 1;
}
if e.sample_depends_on == 2 {
independent += 1;
}
if e.sample_is_depended_on == 2 {
disposable += 1;
}
if e.sample_has_redundancy == 1 {
redundant += 1;
}
}
params
.options
.insert("sdtp_count".to_string(), t.sdtp.len().to_string());
params
.options
.insert("sdtp_leading_count".to_string(), leading.to_string());
params.options.insert(
"sdtp_independent_count".to_string(),
independent.to_string(),
);
params
.options
.insert("sdtp_disposable_count".to_string(), disposable.to_string());
params
.options
.insert("sdtp_redundant_count".to_string(), redundant.to_string());
}
for (i, sb) in t.subs.iter().enumerate() {
let mut v = format!("v{} flags={}", sb.version, sb.flags);
for ent in &sb.entries {
v.push_str(&format!(" delta={}", ent.sample_delta));
if !ent.subsamples.is_empty() {
v.push(':');
let mut first = true;
for s in &ent.subsamples {
if !first {
v.push(';');
}
first = false;
v.push_str(&format!(
"{},{},{},{:08x}",
s.subsample_size,
s.subsample_priority,
s.discardable,
s.codec_specific_parameters
));
}
}
}
params.options.insert(format!("subs_{}", i), v);
}
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,
sidxes: Vec<SidxRecord>,
tfras: Vec<TfraRecord>,
#[allow(dead_code)]
prfts: Vec<PrftRecord>,
#[allow(dead_code)]
psshes: Vec<PsshBox>,
#[allow(dead_code)]
senc_records: Vec<SencRecord>,
#[allow(dead_code)]
movie_timescale: u32,
track_timescales: Vec<u32>,
track_ids: Vec<u32>,
}
impl Mp4Demuxer {
#[allow(dead_code)]
pub fn psshes(&self) -> &[PsshBox] {
&self.psshes
}
#[allow(dead_code)]
pub fn senc_records(&self) -> &[SencRecord] {
&self.senc_records
}
}
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);
}
}
}
if let Some(target_offset) = self.sidx_seek_target(stream_index, pts) {
for (i, s) in self.samples.iter().enumerate() {
if s.track_idx != stream_index {
continue;
}
if s.offset >= target_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])
}
}
}
}
fn sidx_seek_target(&self, stream_index: u32, pts: i64) -> Option<u64> {
if self.sidxes.is_empty() {
return None;
}
let track_id = *self.track_ids.get(stream_index as usize)?;
let track_ts = *self.track_timescales.get(stream_index as usize)? as u64;
if track_ts == 0 {
return None;
}
let pts_u = if pts < 0 { 0u64 } else { pts as u64 };
let mut best: Option<u64> = None;
let mut first_media_offset: Option<u64> = None;
for sidx in self.sidxes.iter().filter(|s| s.reference_id == track_id) {
if sidx.references.is_empty() || sidx.timescale == 0 {
continue;
}
let target_sidx_time = if track_ts == sidx.timescale as u64 {
pts_u
} else {
((pts_u as u128) * (sidx.timescale as u128) / (track_ts as u128)) as u64
};
let mut cur_time = sidx.earliest_presentation_time;
let mut cur_offset = sidx.first_byte_offset;
for r in &sidx.references {
if r.is_sidx {
cur_offset = cur_offset.saturating_add(r.referenced_size as u64);
continue;
}
if first_media_offset.is_none() {
first_media_offset = Some(cur_offset);
}
if cur_time <= target_sidx_time {
best = Some(cur_offset);
}
cur_time = cur_time.saturating_add(r.subsegment_duration as u64);
cur_offset = cur_offset.saturating_add(r.referenced_size as u64);
}
}
best.or(first_media_offset)
}
}
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)
}
pub fn parse_prft_box(body: &[u8]) -> Result<Option<PrftRecord>> {
parse_prft(body)
}
use std::io::Read;
fn read_bytes_vec<R: Read + ?Sized>(r: &mut R, n: usize) -> Result<Vec<u8>> {
let mut buf = Vec::new();
r.take(n as u64).read_to_end(&mut buf)?;
if buf.len() != n {
return Err(Error::invalid("MP4: truncated box payload"));
}
Ok(buf)
}
fn skip_cursor_bytes<T: AsRef<[u8]>>(cur: &mut std::io::Cursor<T>, psz: usize) {
let end = cur.get_ref().as_ref().len() as u64;
let pos = cur.position();
let next = pos.saturating_add(psz as u64).min(end);
cur.set_position(next);
}
#[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(),
protection_scheme: None,
tenc: None,
tref: Vec::new(),
elng: None,
kinds: Vec::new(),
cslg: None,
stsh: Vec::new(),
sbgp: Vec::new(),
sgpd: Vec::new(),
sdtp: Vec::new(),
subs: Vec::new(),
}
}
fn build_enca_with_sinf(original: &[u8; 4], scheme: &[u8; 4]) -> Vec<u8> {
let mut out = Vec::new();
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 mut sinf_body = Vec::new();
sinf_body.extend_from_slice(&12u32.to_be_bytes());
sinf_body.extend_from_slice(b"frma");
sinf_body.extend_from_slice(original);
sinf_body.extend_from_slice(&20u32.to_be_bytes());
sinf_body.extend_from_slice(b"schm");
sinf_body.extend_from_slice(&[0u8; 4]); sinf_body.extend_from_slice(scheme);
sinf_body.extend_from_slice(&[0u8; 4]);
let sinf_total = (8 + sinf_body.len()) as u32;
out.extend_from_slice(&sinf_total.to_be_bytes());
out.extend_from_slice(b"sinf");
out.extend_from_slice(&sinf_body);
out
}
#[test]
fn enca_sample_entry_recovers_original_format() {
let entry_body = build_enca_with_sinf(b"mp4a", b"cenc");
let mut stsd = Vec::new();
stsd.extend_from_slice(&[0u8; 4]); stsd.extend_from_slice(&1u32.to_be_bytes()); let total = (8 + entry_body.len()) as u32;
stsd.extend_from_slice(&total.to_be_bytes());
stsd.extend_from_slice(b"enca");
stsd.extend_from_slice(&entry_body);
let mut t = fresh_track();
super::parse_stsd(&stsd, &mut t).unwrap();
assert_eq!(&t.codec_id_fourcc, b"mp4a");
assert_eq!(t.protection_scheme, Some(*b"cenc"));
assert_eq!(t.channels, Some(2));
}
#[test]
fn encv_sample_entry_recovers_h264_original() {
let mut entry_body = Vec::new();
entry_body.extend_from_slice(&[0u8; 6]);
entry_body.extend_from_slice(&1u16.to_be_bytes());
entry_body.extend_from_slice(&[0u8; 16]); entry_body.extend_from_slice(&1280u16.to_be_bytes()); entry_body.extend_from_slice(&720u16.to_be_bytes()); entry_body.extend_from_slice(&((72u32) << 16).to_be_bytes()); entry_body.extend_from_slice(&((72u32) << 16).to_be_bytes()); entry_body.extend_from_slice(&[0u8; 4]); entry_body.extend_from_slice(&1u16.to_be_bytes()); entry_body.extend_from_slice(&[0u8; 32]); entry_body.extend_from_slice(&0x0018u16.to_be_bytes()); entry_body.extend_from_slice(&(-1i16).to_be_bytes()); assert_eq!(entry_body.len(), 78);
let mut sinf_body = Vec::new();
sinf_body.extend_from_slice(&12u32.to_be_bytes());
sinf_body.extend_from_slice(b"frma");
sinf_body.extend_from_slice(b"avc1");
sinf_body.extend_from_slice(&20u32.to_be_bytes());
sinf_body.extend_from_slice(b"schm");
sinf_body.extend_from_slice(&[0u8; 4]);
sinf_body.extend_from_slice(b"cbcs");
sinf_body.extend_from_slice(&[0u8; 4]);
let sinf_total = (8 + sinf_body.len()) as u32;
entry_body.extend_from_slice(&sinf_total.to_be_bytes());
entry_body.extend_from_slice(b"sinf");
entry_body.extend_from_slice(&sinf_body);
let mut stsd = Vec::new();
stsd.extend_from_slice(&[0u8; 4]);
stsd.extend_from_slice(&1u32.to_be_bytes());
let total = (8 + entry_body.len()) as u32;
stsd.extend_from_slice(&total.to_be_bytes());
stsd.extend_from_slice(b"encv");
stsd.extend_from_slice(&entry_body);
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
super::parse_stsd(&stsd, &mut t).unwrap();
assert_eq!(&t.codec_id_fourcc, b"avc1");
assert_eq!(t.protection_scheme, Some(*b"cbcs"));
assert_eq!(t.width, Some(1280));
assert_eq!(t.height, Some(720));
}
fn build_tenc_box_v0(kid: &[u8; 16], iv_size: u8) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.push(0); body.push(0); body.push(1); body.push(iv_size);
body.extend_from_slice(kid);
let mut out = Vec::with_capacity(8 + body.len());
out.extend_from_slice(&((8 + body.len()) as u32).to_be_bytes());
out.extend_from_slice(b"tenc");
out.extend_from_slice(&body);
out
}
fn build_tenc_box_v1_constant_iv(
kid: &[u8; 16],
crypt: u8,
skip: u8,
const_iv: &[u8],
) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[1u8, 0, 0, 0]); body.push(0); body.push((crypt << 4) | (skip & 0x0F));
body.push(1); body.push(0); body.extend_from_slice(kid);
body.push(const_iv.len() as u8);
body.extend_from_slice(const_iv);
let mut out = Vec::with_capacity(8 + body.len());
out.extend_from_slice(&((8 + body.len()) as u32).to_be_bytes());
out.extend_from_slice(b"tenc");
out.extend_from_slice(&body);
out
}
fn build_schi_box(child: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(8 + child.len());
out.extend_from_slice(&((8 + child.len()) as u32).to_be_bytes());
out.extend_from_slice(b"schi");
out.extend_from_slice(child);
out
}
fn build_sinf_body_with_schi(
original: &[u8; 4],
scheme: &[u8; 4],
schi_child: &[u8],
) -> Vec<u8> {
let mut sinf_body = Vec::new();
sinf_body.extend_from_slice(&12u32.to_be_bytes());
sinf_body.extend_from_slice(b"frma");
sinf_body.extend_from_slice(original);
sinf_body.extend_from_slice(&20u32.to_be_bytes());
sinf_body.extend_from_slice(b"schm");
sinf_body.extend_from_slice(&[0u8; 4]);
sinf_body.extend_from_slice(scheme);
sinf_body.extend_from_slice(&[0u8; 4]);
let schi = build_schi_box(schi_child);
sinf_body.extend_from_slice(&schi);
sinf_body
}
fn build_enca_with_full_sinf(original: &[u8; 4], scheme: &[u8; 4], tenc_box: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
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 sinf_body = build_sinf_body_with_schi(original, scheme, tenc_box);
let sinf_total = (8 + sinf_body.len()) as u32;
out.extend_from_slice(&sinf_total.to_be_bytes());
out.extend_from_slice(b"sinf");
out.extend_from_slice(&sinf_body);
out
}
#[test]
fn enca_sample_entry_with_sinf_schi_tenc_populates_track_tenc_v0() {
let kid: [u8; 16] = [
0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
0x88, 0x99,
];
let tenc_box = build_tenc_box_v0(&kid, 16);
let entry_body = build_enca_with_full_sinf(b"mp4a", b"cenc", &tenc_box);
let mut stsd = Vec::new();
stsd.extend_from_slice(&[0u8; 4]);
stsd.extend_from_slice(&1u32.to_be_bytes());
let total = (8 + entry_body.len()) as u32;
stsd.extend_from_slice(&total.to_be_bytes());
stsd.extend_from_slice(b"enca");
stsd.extend_from_slice(&entry_body);
let mut t = fresh_track();
super::parse_stsd(&stsd, &mut t).unwrap();
assert_eq!(&t.codec_id_fourcc, b"mp4a");
assert_eq!(t.protection_scheme, Some(*b"cenc"));
let tenc = t.tenc.expect("tenc parsed from sinf/schi/tenc");
assert_eq!(tenc.version, 0);
assert_eq!(tenc.default_is_protected, 1);
assert_eq!(tenc.default_per_sample_iv_size, 16);
assert_eq!(tenc.default_kid, kid);
assert!(tenc.default_constant_iv.is_none());
}
#[test]
fn encv_sample_entry_v1_pattern_with_constant_iv_lands_on_track() {
let kid = [0x01u8; 16];
let civ = [0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE];
let tenc_box = build_tenc_box_v1_constant_iv(&kid, 1, 9, &civ);
let mut entry_body = Vec::new();
entry_body.extend_from_slice(&[0u8; 6]);
entry_body.extend_from_slice(&1u16.to_be_bytes());
entry_body.extend_from_slice(&[0u8; 16]);
entry_body.extend_from_slice(&1280u16.to_be_bytes());
entry_body.extend_from_slice(&720u16.to_be_bytes());
entry_body.extend_from_slice(&((72u32) << 16).to_be_bytes());
entry_body.extend_from_slice(&((72u32) << 16).to_be_bytes());
entry_body.extend_from_slice(&[0u8; 4]);
entry_body.extend_from_slice(&1u16.to_be_bytes());
entry_body.extend_from_slice(&[0u8; 32]);
entry_body.extend_from_slice(&0x0018u16.to_be_bytes());
entry_body.extend_from_slice(&(-1i16).to_be_bytes());
assert_eq!(entry_body.len(), 78);
let sinf_body = build_sinf_body_with_schi(b"avc1", b"cbcs", &tenc_box);
let sinf_total = (8 + sinf_body.len()) as u32;
entry_body.extend_from_slice(&sinf_total.to_be_bytes());
entry_body.extend_from_slice(b"sinf");
entry_body.extend_from_slice(&sinf_body);
let mut stsd = Vec::new();
stsd.extend_from_slice(&[0u8; 4]);
stsd.extend_from_slice(&1u32.to_be_bytes());
let total = (8 + entry_body.len()) as u32;
stsd.extend_from_slice(&total.to_be_bytes());
stsd.extend_from_slice(b"encv");
stsd.extend_from_slice(&entry_body);
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
super::parse_stsd(&stsd, &mut t).unwrap();
assert_eq!(&t.codec_id_fourcc, b"avc1");
assert_eq!(t.protection_scheme, Some(*b"cbcs"));
let tenc = t.tenc.expect("tenc parsed from sinf/schi/tenc");
assert_eq!(tenc.version, 1);
assert_eq!(tenc.default_crypt_byte_block, 1);
assert_eq!(tenc.default_skip_byte_block, 9);
assert_eq!(tenc.default_per_sample_iv_size, 0);
assert_eq!(tenc.default_constant_iv.as_deref(), Some(&civ[..]));
}
#[test]
fn enca_sample_entry_without_tenc_leaves_track_tenc_none() {
let entry_body = build_enca_with_sinf(b"mp4a", b"cenc");
let mut stsd = Vec::new();
stsd.extend_from_slice(&[0u8; 4]);
stsd.extend_from_slice(&1u32.to_be_bytes());
let total = (8 + entry_body.len()) as u32;
stsd.extend_from_slice(&total.to_be_bytes());
stsd.extend_from_slice(b"enca");
stsd.extend_from_slice(&entry_body);
let mut t = fresh_track();
super::parse_stsd(&stsd, &mut t).unwrap();
assert_eq!(&t.codec_id_fourcc, b"mp4a");
assert_eq!(t.protection_scheme, Some(*b"cenc"));
assert!(t.tenc.is_none());
}
#[test]
fn subtitle_handler_dispatches_subtitle_media_type() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(b"subt"); body.extend_from_slice(&[0u8; 12]); body.extend_from_slice(b"\0");
let mut t = fresh_track();
super::parse_hdlr(&body, &mut t).unwrap();
assert_eq!(t.media_type, oxideav_core::MediaType::Subtitle);
let mut body2 = Vec::new();
body2.extend_from_slice(&[0u8; 4]);
body2.extend_from_slice(&[0u8; 4]);
body2.extend_from_slice(b"text");
body2.extend_from_slice(&[0u8; 12]);
body2.extend_from_slice(b"\0");
let mut t2 = fresh_track();
super::parse_hdlr(&body2, &mut t2).unwrap();
assert_eq!(t2.media_type, oxideav_core::MediaType::Subtitle);
let mut body3 = Vec::new();
body3.extend_from_slice(&[0u8; 4]);
body3.extend_from_slice(&[0u8; 4]);
body3.extend_from_slice(b"sbtl");
body3.extend_from_slice(&[0u8; 12]);
body3.extend_from_slice(b"\0");
let mut t3 = fresh_track();
super::parse_hdlr(&body3, &mut t3).unwrap();
assert_eq!(t3.media_type, oxideav_core::MediaType::Subtitle);
}
#[test]
fn tx3g_sample_entry_preserves_payload_as_extradata() {
let mut entry = Vec::new();
entry.extend_from_slice(&[0u8; 6]);
entry.extend_from_slice(&1u16.to_be_bytes());
let tx3g_header = [
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x10, ];
entry.extend_from_slice(&tx3g_header);
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Subtitle;
t.codec_id_fourcc = *b"tx3g";
super::parse_subtitle_sample_entry(&entry, &mut t).unwrap();
assert_eq!(t.extradata, tx3g_header);
}
#[test]
fn stpp_sample_entry_preserves_xml_namespace_strings() {
let mut entry = Vec::new();
entry.extend_from_slice(&[0u8; 6]);
entry.extend_from_slice(&1u16.to_be_bytes());
entry.extend_from_slice(b"http://www.w3.org/ns/ttml\0\0\0");
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Subtitle;
t.codec_id_fourcc = *b"stpp";
super::parse_subtitle_sample_entry(&entry, &mut t).unwrap();
assert!(t.extradata.starts_with(b"http://www.w3.org/ns/ttml"));
}
#[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);
}
#[test]
fn parse_tref_chap_single_id() {
let inner = wrap_box_full_size(b"chap", &3u32.to_be_bytes());
let mut t = fresh_track();
super::parse_tref(&inner, &mut t).unwrap();
assert_eq!(t.tref.len(), 1);
assert_eq!(&t.tref[0].0, b"chap");
assert_eq!(t.tref[0].1, vec![3]);
}
#[test]
fn parse_tref_multiple_types() {
let mut body = Vec::new();
let mut subt_payload = Vec::new();
subt_payload.extend_from_slice(&4u32.to_be_bytes());
subt_payload.extend_from_slice(&5u32.to_be_bytes());
body.extend(wrap_box_full_size(b"subt", &subt_payload));
body.extend(wrap_box_full_size(b"cdsc", &2u32.to_be_bytes()));
let mut t = fresh_track();
super::parse_tref(&body, &mut t).unwrap();
assert_eq!(t.tref.len(), 2);
assert_eq!(&t.tref[0].0, b"subt");
assert_eq!(t.tref[0].1, vec![4, 5]);
assert_eq!(&t.tref[1].0, b"cdsc");
assert_eq!(t.tref[1].1, vec![2]);
}
#[test]
fn parse_tref_drops_zero_track_ids() {
let mut payload = Vec::new();
payload.extend_from_slice(&0u32.to_be_bytes());
payload.extend_from_slice(&7u32.to_be_bytes());
let body = wrap_box_full_size(b"font", &payload);
let mut t = fresh_track();
super::parse_tref(&body, &mut t).unwrap();
assert_eq!(t.tref.len(), 1);
assert_eq!(t.tref[0].1, vec![7]);
}
#[test]
fn parse_tref_misaligned_child_rejected() {
let body = wrap_box_full_size(b"hint", &[0u8, 0, 0, 1, 0xFF]); let mut t = fresh_track();
let err = super::parse_tref(&body, &mut t).unwrap_err();
assert!(matches!(err, oxideav_core::Error::InvalidData(_)));
}
#[test]
fn parse_tref_duplicate_type_keeps_first() {
let mut body = Vec::new();
body.extend(wrap_box_full_size(b"subt", &1u32.to_be_bytes()));
body.extend(wrap_box_full_size(b"subt", &2u32.to_be_bytes()));
let mut t = fresh_track();
super::parse_tref(&body, &mut t).unwrap();
assert_eq!(t.tref.len(), 1);
assert_eq!(t.tref[0].1, vec![1]);
}
#[test]
fn parse_tref_empty_body_is_ok() {
let mut t = fresh_track();
super::parse_tref(&[], &mut t).unwrap();
assert!(t.tref.is_empty());
}
#[test]
fn build_stream_info_surfaces_tref_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Subtitle;
t.codec_id_fourcc = *b"tx3g";
t.timescale = 1000;
t.tref.push((*b"subt", vec![10, 11]));
t.tref.push((*b"font", vec![20]));
t.tref.push((*b"hint", vec![])); let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("tref_subt"), Some("10 11"));
assert_eq!(info.params.options.get("tref_font"), Some("20"));
assert_eq!(info.params.options.get("tref_hint"), None);
}
#[test]
fn parse_elng_reads_bcp47_tag() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(b"en-US\0"); let mut t = fresh_track();
super::parse_elng(&body, &mut t);
assert_eq!(t.elng.as_deref(), Some("en-US"));
}
#[test]
fn parse_elng_subtags_and_missing_nul() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]);
body.extend_from_slice(b"zh-Hant-HK\0");
let mut t = fresh_track();
super::parse_elng(&body, &mut t);
assert_eq!(t.elng.as_deref(), Some("zh-Hant-HK"));
let mut body2 = Vec::new();
body2.extend_from_slice(&[0u8; 4]);
body2.extend_from_slice(b"fr-FR");
let mut t2 = fresh_track();
super::parse_elng(&body2, &mut t2);
assert_eq!(t2.elng.as_deref(), Some("fr-FR"));
}
#[test]
fn parse_elng_malformed_is_silently_skipped() {
let mut t = fresh_track();
super::parse_elng(&[0, 0, 0], &mut t);
assert_eq!(t.elng, None);
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]);
body.push(0);
let mut t2 = fresh_track();
super::parse_elng(&body, &mut t2);
assert_eq!(t2.elng, None);
}
#[test]
fn build_stream_info_surfaces_elng_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Audio;
t.codec_id_fourcc = *b"mp4a";
t.timescale = 48000;
t.elng = Some("de-DE".to_string());
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("language"), Some("de-DE"));
}
#[test]
fn build_stream_info_no_elng_no_language_option() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Audio;
t.codec_id_fourcc = *b"mp4a";
t.timescale = 48000;
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("language"), None);
}
#[test]
fn parse_mdia_picks_up_nested_elng() {
let mut mdhd = Vec::new();
mdhd.extend_from_slice(&[0u8; 4]); mdhd.extend_from_slice(&[0u8; 8]); mdhd.extend_from_slice(&1000u32.to_be_bytes()); mdhd.extend_from_slice(&0u32.to_be_bytes()); mdhd.extend_from_slice(&[0u8; 4]);
let mut elng = Vec::new();
elng.extend_from_slice(&[0u8; 4]);
elng.extend_from_slice(b"es-419\0");
let mut mdia = Vec::new();
mdia.extend(wrap_box_full_size(b"mdhd", &mdhd));
mdia.extend(wrap_box_full_size(b"elng", &elng));
let mut t = fresh_track();
super::parse_mdia(&mdia, &mut t).unwrap();
assert_eq!(t.timescale, 1000);
assert_eq!(t.elng.as_deref(), Some("es-419"));
}
fn wrap_box_full_size(fourcc: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let total = (8 + payload.len()) as u32;
let mut out = Vec::with_capacity(total as usize);
out.extend_from_slice(&total.to_be_bytes());
out.extend_from_slice(fourcc);
out.extend_from_slice(payload);
out
}
#[test]
fn parse_kind_uri_only() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(b"urn:mpeg:dash:role:2011\0");
body.extend_from_slice(b"\0"); let mut t = fresh_track();
super::parse_kind(&body, &mut t);
assert_eq!(t.kinds.len(), 1);
assert_eq!(t.kinds[0].0, "urn:mpeg:dash:role:2011");
assert_eq!(t.kinds[0].1, "");
}
#[test]
fn parse_kind_uri_and_value() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]);
body.extend_from_slice(b"urn:mpeg:dash:role:2011\0");
body.extend_from_slice(b"main\0");
let mut t = fresh_track();
super::parse_kind(&body, &mut t);
assert_eq!(t.kinds.len(), 1);
assert_eq!(t.kinds[0].0, "urn:mpeg:dash:role:2011");
assert_eq!(t.kinds[0].1, "main");
}
#[test]
fn parse_kind_missing_value_nul_tolerated() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]);
body.extend_from_slice(b"urn:example:role\0");
body.extend_from_slice(b"alt"); let mut t = fresh_track();
super::parse_kind(&body, &mut t);
assert_eq!(t.kinds.len(), 1);
assert_eq!(t.kinds[0].0, "urn:example:role");
assert_eq!(t.kinds[0].1, "alt");
}
#[test]
fn parse_kind_empty_uri_dropped() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]);
body.extend_from_slice(b"\0\0");
let mut t = fresh_track();
super::parse_kind(&body, &mut t);
assert!(t.kinds.is_empty());
}
#[test]
fn parse_kind_too_short_is_silently_skipped() {
let mut t = fresh_track();
super::parse_kind(&[0, 0, 0], &mut t);
assert!(t.kinds.is_empty());
}
#[test]
fn parse_track_udta_collects_multiple_kinds() {
let mut k1 = Vec::new();
k1.extend_from_slice(&[0u8; 4]);
k1.extend_from_slice(b"urn:mpeg:dash:role:2011\0");
k1.extend_from_slice(b"caption\0");
let mut k2 = Vec::new();
k2.extend_from_slice(&[0u8; 4]);
k2.extend_from_slice(b"urn:apple:hap:subtitles\0\0");
let mut udta = Vec::new();
udta.extend(wrap_box_full_size(b"kind", &k1));
udta.extend(wrap_box_full_size(b"kind", &k2));
let mut t = fresh_track();
super::parse_track_udta(&udta, &mut t);
assert_eq!(t.kinds.len(), 2);
assert_eq!(t.kinds[0].0, "urn:mpeg:dash:role:2011");
assert_eq!(t.kinds[0].1, "caption");
assert_eq!(t.kinds[1].0, "urn:apple:hap:subtitles");
assert_eq!(t.kinds[1].1, "");
}
#[test]
fn parse_track_udta_ignores_non_kind_children() {
let mut titl = Vec::new();
titl.extend_from_slice(&[0u8; 6]); titl.extend_from_slice(b"My Track\0");
let mut udta = Vec::new();
udta.extend(wrap_box_full_size(b"titl", &titl));
let mut t = fresh_track();
super::parse_track_udta(&udta, &mut t);
assert!(t.kinds.is_empty());
}
#[test]
fn build_stream_info_surfaces_kinds_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Subtitle;
t.codec_id_fourcc = *b"wvtt";
t.timescale = 1000;
t.kinds
.push(("urn:mpeg:dash:role:2011".to_string(), "caption".to_string()));
t.kinds
.push(("urn:apple:hap:subtitles".to_string(), String::new()));
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(
info.params.options.get("kind_0"),
Some("urn:mpeg:dash:role:2011 caption")
);
assert_eq!(
info.params.options.get("kind_1"),
Some("urn:apple:hap:subtitles")
);
assert_eq!(info.params.options.get("kind_2"), None);
}
#[test]
fn build_stream_info_no_kind_no_kind_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Audio;
t.codec_id_fourcc = *b"mp4a";
t.timescale = 48000;
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("kind_0"), None);
}
#[test]
fn parse_trak_picks_up_nested_kind() {
let mut tkhd = vec![0u8; 84];
tkhd[12..16].copy_from_slice(&7u32.to_be_bytes());
let mut mdhd = Vec::new();
mdhd.extend_from_slice(&[0u8; 4]); mdhd.extend_from_slice(&[0u8; 8]); mdhd.extend_from_slice(&1000u32.to_be_bytes()); mdhd.extend_from_slice(&0u32.to_be_bytes()); mdhd.extend_from_slice(&[0u8; 4]); let mut hdlr = Vec::new();
hdlr.extend_from_slice(&[0u8; 4]); hdlr.extend_from_slice(&[0u8; 4]); hdlr.extend_from_slice(b"vide"); hdlr.extend_from_slice(&[0u8; 12]); hdlr.push(0); let mut mdia = Vec::new();
mdia.extend(wrap_box_full_size(b"mdhd", &mdhd));
mdia.extend(wrap_box_full_size(b"hdlr", &hdlr));
let mut kind = Vec::new();
kind.extend_from_slice(&[0u8; 4]);
kind.extend_from_slice(b"urn:mpeg:dash:role:2011\0");
kind.extend_from_slice(b"main\0");
let mut udta = Vec::new();
udta.extend(wrap_box_full_size(b"kind", &kind));
let mut trak = Vec::new();
trak.extend(wrap_box_full_size(b"tkhd", &tkhd));
trak.extend(wrap_box_full_size(b"mdia", &mdia));
trak.extend(wrap_box_full_size(b"udta", &udta));
let t = super::parse_trak(&trak).unwrap().unwrap();
assert_eq!(t.track_id, 7);
assert_eq!(t.kinds.len(), 1);
assert_eq!(t.kinds[0].0, "urn:mpeg:dash:role:2011");
assert_eq!(t.kinds[0].1, "main");
}
#[test]
fn parse_cslg_v0() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&512i32.to_be_bytes()); body.extend_from_slice(&(-512i32).to_be_bytes()); body.extend_from_slice(&1024i32.to_be_bytes()); body.extend_from_slice(&0i32.to_be_bytes()); body.extend_from_slice(&48000i32.to_be_bytes()); let c = super::parse_cslg(&body).unwrap();
assert_eq!(c.composition_to_dts_shift, 512);
assert_eq!(c.least_decode_to_display_delta, -512);
assert_eq!(c.greatest_decode_to_display_delta, 1024);
assert_eq!(c.composition_start_time, 0);
assert_eq!(c.composition_end_time, 48000);
}
#[test]
fn parse_cslg_v1_64bit() {
let big = (i32::MAX as i64) + 1_000; let mut body = Vec::new();
body.extend_from_slice(&[1u8, 0, 0, 0]); body.extend_from_slice(&0i64.to_be_bytes()); body.extend_from_slice(&(-1_000i64).to_be_bytes()); body.extend_from_slice(&2_000i64.to_be_bytes()); body.extend_from_slice(&0i64.to_be_bytes()); body.extend_from_slice(&big.to_be_bytes()); let c = super::parse_cslg(&body).unwrap();
assert_eq!(c.composition_to_dts_shift, 0);
assert_eq!(c.least_decode_to_display_delta, -1_000);
assert_eq!(c.greatest_decode_to_display_delta, 2_000);
assert_eq!(c.composition_start_time, 0);
assert_eq!(c.composition_end_time, big);
}
#[test]
fn parse_cslg_too_short() {
assert!(super::parse_cslg(&[0u8, 0, 0]).is_err());
}
#[test]
fn parse_cslg_truncated() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&0i32.to_be_bytes()); body.extend_from_slice(&0i32.to_be_bytes()); body.extend_from_slice(&0i32.to_be_bytes()); body.extend_from_slice(&0i32.to_be_bytes()); assert!(super::parse_cslg(&body).is_err());
}
#[test]
fn parse_stbl_picks_up_cslg() {
let mut cslg = Vec::new();
cslg.extend_from_slice(&[0u8; 4]); cslg.extend_from_slice(&100i32.to_be_bytes());
cslg.extend_from_slice(&(-100i32).to_be_bytes());
cslg.extend_from_slice(&200i32.to_be_bytes());
cslg.extend_from_slice(&0i32.to_be_bytes());
cslg.extend_from_slice(&5000i32.to_be_bytes());
let mut stbl = Vec::new();
stbl.extend(wrap_box_full_size(b"cslg", &cslg));
let mut t = fresh_track();
super::parse_stbl(&stbl, &mut t).unwrap();
let c = t.cslg.expect("cslg should be parsed");
assert_eq!(c.composition_to_dts_shift, 100);
assert_eq!(c.least_decode_to_display_delta, -100);
assert_eq!(c.greatest_decode_to_display_delta, 200);
assert_eq!(c.composition_start_time, 0);
assert_eq!(c.composition_end_time, 5000);
}
#[test]
fn build_stream_info_surfaces_cslg_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 1000;
t.cslg = Some(super::CslgBox {
composition_to_dts_shift: 512,
least_decode_to_display_delta: -512,
greatest_decode_to_display_delta: 1024,
composition_start_time: 0,
composition_end_time: 90000,
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(
info.params.options.get("cslg_composition_to_dts_shift"),
Some("512")
);
assert_eq!(
info.params
.options
.get("cslg_least_decode_to_display_delta"),
Some("-512")
);
assert_eq!(
info.params
.options
.get("cslg_greatest_decode_to_display_delta"),
Some("1024")
);
assert_eq!(
info.params.options.get("cslg_composition_start_time"),
Some("0")
);
assert_eq!(
info.params.options.get("cslg_composition_end_time"),
Some("90000")
);
}
#[test]
fn build_stream_info_no_cslg_no_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 1000;
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(
info.params.options.get("cslg_composition_to_dts_shift"),
None
);
assert_eq!(info.params.options.get("cslg_composition_end_time"), None);
}
#[test]
fn parse_stsh_two_entries() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&2u32.to_be_bytes()); body.extend_from_slice(&7u32.to_be_bytes()); body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&13u32.to_be_bytes());
body.extend_from_slice(&10u32.to_be_bytes());
let v = super::parse_stsh(&body).unwrap();
assert_eq!(v, vec![(7, 1), (13, 10)]);
}
#[test]
fn parse_stsh_empty_table() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&0u32.to_be_bytes()); let v = super::parse_stsh(&body).unwrap();
assert!(v.is_empty());
}
#[test]
fn parse_stsh_too_short() {
assert!(super::parse_stsh(&[0u8, 0, 0, 0, 0, 0, 0]).is_err());
}
#[test]
fn parse_stsh_truncated() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&2u32.to_be_bytes()); body.extend_from_slice(&7u32.to_be_bytes()); body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&13u32.to_be_bytes()); assert!(super::parse_stsh(&body).is_err());
}
#[test]
fn parse_stsh_huge_count_rejected() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&u32::MAX.to_be_bytes()); body.extend_from_slice(&1u32.to_be_bytes()); assert!(super::parse_stsh(&body).is_err());
}
#[test]
fn parse_stbl_picks_up_stsh() {
let mut stsh = Vec::new();
stsh.extend_from_slice(&[0u8; 4]); stsh.extend_from_slice(&1u32.to_be_bytes()); stsh.extend_from_slice(&5u32.to_be_bytes()); stsh.extend_from_slice(&2u32.to_be_bytes()); let mut stbl = Vec::new();
stbl.extend(wrap_box_full_size(b"stsh", &stsh));
let mut t = fresh_track();
super::parse_stbl(&stbl, &mut t).unwrap();
assert_eq!(t.stsh, vec![(5, 2)]);
}
#[test]
fn build_stream_info_surfaces_stsh_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 1000;
t.stsh = vec![(7, 1), (13, 10)];
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("stsh_0"), Some("7 1"));
assert_eq!(info.params.options.get("stsh_1"), Some("13 10"));
}
#[test]
fn build_stream_info_no_stsh_no_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 1000;
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("stsh_0"), None);
}
#[test]
fn parse_sbgp_v0_two_runs() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(b"roll"); body.extend_from_slice(&2u32.to_be_bytes()); body.extend_from_slice(&10u32.to_be_bytes()); body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&5u32.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes()); let sb = super::parse_sbgp(&body).unwrap();
assert_eq!(&sb.grouping_type, b"roll");
assert_eq!(sb.grouping_type_parameter, None);
assert_eq!(sb.entries, vec![(10, 1), (5, 0)]);
}
#[test]
fn parse_sbgp_v1_with_parameter() {
let mut body = Vec::new();
body.extend_from_slice(&[1u8, 0, 0, 0]); body.extend_from_slice(b"rap "); body.extend_from_slice(&7u32.to_be_bytes()); body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&3u32.to_be_bytes()); body.extend_from_slice(&2u32.to_be_bytes()); let sb = super::parse_sbgp(&body).unwrap();
assert_eq!(&sb.grouping_type, b"rap ");
assert_eq!(sb.grouping_type_parameter, Some(7));
assert_eq!(sb.entries, vec![(3, 2)]);
}
#[test]
fn parse_sbgp_keeps_fragment_local_index() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]);
body.extend_from_slice(b"sync");
body.extend_from_slice(&1u32.to_be_bytes());
body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&0x1_0001u32.to_be_bytes()); let sb = super::parse_sbgp(&body).unwrap();
assert_eq!(sb.entries, vec![(1, 0x1_0001)]);
}
#[test]
fn parse_sbgp_empty_table() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]);
body.extend_from_slice(b"roll");
body.extend_from_slice(&0u32.to_be_bytes()); let sb = super::parse_sbgp(&body).unwrap();
assert!(sb.entries.is_empty());
}
#[test]
fn parse_sbgp_truncated_entry_rejected() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]);
body.extend_from_slice(b"roll");
body.extend_from_slice(&2u32.to_be_bytes()); body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&1u32.to_be_bytes());
assert!(super::parse_sbgp(&body).is_err());
}
#[test]
fn parse_sbgp_too_short() {
assert!(super::parse_sbgp(&[0u8, 0, 0, 0, b'r']).is_err());
}
#[test]
fn parse_sgpd_v1_fixed_length() {
let mut body = Vec::new();
body.extend_from_slice(&[1u8, 0, 0, 0]); body.extend_from_slice(b"roll"); body.extend_from_slice(&2u32.to_be_bytes()); body.extend_from_slice(&2u32.to_be_bytes()); body.extend_from_slice(&[0xFF, 0xFB]); body.extend_from_slice(&[0x00, 0x05]); let sg = super::parse_sgpd(&body).unwrap();
assert_eq!(&sg.grouping_type, b"roll");
assert_eq!(sg.default_sample_description_index, None);
assert_eq!(sg.entries, vec![vec![0xFF, 0xFB], vec![0x00, 0x05]]);
}
#[test]
fn parse_sgpd_v1_variable_length() {
let mut body = Vec::new();
body.extend_from_slice(&[1u8, 0, 0, 0]); body.extend_from_slice(b"prol"); body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&2u32.to_be_bytes()); body.extend_from_slice(&3u32.to_be_bytes()); body.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&[0xDD]);
let sg = super::parse_sgpd(&body).unwrap();
assert_eq!(&sg.grouping_type, b"prol");
assert_eq!(sg.entries, vec![vec![0xAA, 0xBB, 0xCC], vec![0xDD]]);
}
#[test]
fn parse_sgpd_v2_default_index() {
let mut body = Vec::new();
body.extend_from_slice(&[2u8, 0, 0, 0]); body.extend_from_slice(b"alst"); body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); let sg = super::parse_sgpd(&body).unwrap();
assert_eq!(&sg.grouping_type, b"alst");
assert_eq!(sg.default_sample_description_index, Some(1));
assert!(sg.entries.is_empty());
}
#[test]
fn parse_sgpd_v0_combined_blob_fallback() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(b"roll"); body.extend_from_slice(&2u32.to_be_bytes()); body.extend_from_slice(&[0x11, 0x22, 0x33, 0x44]); let sg = super::parse_sgpd(&body).unwrap();
assert_eq!(sg.entries, vec![vec![0x11, 0x22, 0x33, 0x44]]);
}
#[test]
fn parse_sgpd_variable_truncated_rejected() {
let mut body = Vec::new();
body.extend_from_slice(&[1u8, 0, 0, 0]);
body.extend_from_slice(b"roll");
body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&8u32.to_be_bytes()); body.extend_from_slice(&[0x01, 0x02]); assert!(super::parse_sgpd(&body).is_err());
}
#[test]
fn parse_stbl_accumulates_sample_groups() {
let mut sbgp = Vec::new();
sbgp.extend_from_slice(&[0u8; 4]);
sbgp.extend_from_slice(b"roll");
sbgp.extend_from_slice(&1u32.to_be_bytes());
sbgp.extend_from_slice(&4u32.to_be_bytes());
sbgp.extend_from_slice(&1u32.to_be_bytes());
let mut sgpd = Vec::new();
sgpd.extend_from_slice(&[1u8, 0, 0, 0]);
sgpd.extend_from_slice(b"roll");
sgpd.extend_from_slice(&2u32.to_be_bytes()); sgpd.extend_from_slice(&1u32.to_be_bytes()); sgpd.extend_from_slice(&[0xFF, 0xFB]);
let mut stbl = Vec::new();
stbl.extend(wrap_box_full_size(b"sbgp", &sbgp));
stbl.extend(wrap_box_full_size(b"sgpd", &sgpd));
let mut t = fresh_track();
super::parse_stbl(&stbl, &mut t).unwrap();
assert_eq!(t.sbgp.len(), 1);
assert_eq!(&t.sbgp[0].grouping_type, b"roll");
assert_eq!(t.sbgp[0].entries, vec![(4, 1)]);
assert_eq!(t.sgpd.len(), 1);
assert_eq!(&t.sgpd[0].grouping_type, b"roll");
assert_eq!(t.sgpd[0].entries, vec![vec![0xFF, 0xFB]]);
}
#[test]
fn build_stream_info_surfaces_sample_groups_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Audio;
t.codec_id_fourcc = *b"mp4a";
t.timescale = 48000;
t.sbgp = vec![
super::SbgpBox {
grouping_type: *b"roll",
grouping_type_parameter: None,
entries: vec![(10, 1), (5, 0)],
},
super::SbgpBox {
grouping_type: *b"rap ",
grouping_type_parameter: Some(7),
entries: vec![(3, 2)],
},
];
t.sgpd = vec![super::SgpdBox {
grouping_type: *b"roll",
default_sample_description_index: Some(1),
entries: vec![vec![0xFF, 0xFB], vec![0x00, 0x05]],
}];
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("sbgp_0"), Some("roll 10:1 5:0"));
assert_eq!(info.params.options.get("sbgp_1"), Some("rap param=7 3:2"));
assert_eq!(
info.params.options.get("sgpd_0"),
Some("roll default=1 fffb 0005")
);
}
#[test]
fn build_stream_info_no_sample_groups_no_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 1000;
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("sbgp_0"), None);
assert_eq!(info.params.options.get("sgpd_0"), None);
}
fn pack_sdtp(is_leading: u8, depends_on: u8, depended_on: u8, redundancy: u8) -> u8 {
((is_leading & 0x03) << 6)
| ((depends_on & 0x03) << 4)
| ((depended_on & 0x03) << 2)
| (redundancy & 0x03)
}
#[test]
fn parse_sdtp_two_entries() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.push(pack_sdtp(2, 2, 1, 2)); body.push(pack_sdtp(1, 1, 2, 2)); let v = super::parse_sdtp(&body).unwrap();
assert_eq!(v.len(), 2);
assert_eq!(v[0].is_leading, 2);
assert_eq!(v[0].sample_depends_on, 2);
assert_eq!(v[0].sample_is_depended_on, 1);
assert_eq!(v[0].sample_has_redundancy, 2);
assert_eq!(v[1].is_leading, 1);
assert_eq!(v[1].sample_depends_on, 1);
assert_eq!(v[1].sample_is_depended_on, 2);
assert_eq!(v[1].sample_has_redundancy, 2);
}
#[test]
fn parse_sdtp_empty_table() {
let body = vec![0u8; 4]; let v = super::parse_sdtp(&body).unwrap();
assert!(v.is_empty());
}
#[test]
fn parse_sdtp_too_short() {
assert!(super::parse_sdtp(&[0u8, 0, 0]).is_err());
}
#[test]
fn parse_sdtp_field_order() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.push(0b11_10_01_00); let v = super::parse_sdtp(&body).unwrap();
assert_eq!(v.len(), 1);
assert_eq!(v[0].is_leading, 3);
assert_eq!(v[0].sample_depends_on, 2);
assert_eq!(v[0].sample_is_depended_on, 1);
assert_eq!(v[0].sample_has_redundancy, 0);
}
#[test]
fn parse_sdtp_all_unknowns() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.push(0u8);
body.push(0u8);
body.push(0u8);
let v = super::parse_sdtp(&body).unwrap();
assert_eq!(v.len(), 3);
for e in &v {
assert_eq!(e.is_leading, 0);
assert_eq!(e.sample_depends_on, 0);
assert_eq!(e.sample_is_depended_on, 0);
assert_eq!(e.sample_has_redundancy, 0);
}
}
#[test]
fn parse_stbl_picks_up_sdtp() {
let mut sdtp = Vec::new();
sdtp.extend_from_slice(&[0u8; 4]); sdtp.push(pack_sdtp(2, 2, 1, 2)); sdtp.push(pack_sdtp(0, 1, 2, 0)); let mut stbl = Vec::new();
stbl.extend(wrap_box_full_size(b"sdtp", &sdtp));
let mut t = fresh_track();
super::parse_stbl(&stbl, &mut t).unwrap();
assert_eq!(t.sdtp.len(), 2);
assert_eq!(t.sdtp[0].sample_depends_on, 2);
assert_eq!(t.sdtp[1].sample_is_depended_on, 2);
}
#[test]
fn build_stream_info_surfaces_sdtp_summary_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 1000;
t.sdtp = vec![
super::SdtpEntry {
is_leading: 2,
sample_depends_on: 2,
sample_is_depended_on: 1,
sample_has_redundancy: 0,
},
super::SdtpEntry {
is_leading: 2,
sample_depends_on: 1,
sample_is_depended_on: 2,
sample_has_redundancy: 0,
},
super::SdtpEntry {
is_leading: 3,
sample_depends_on: 1,
sample_is_depended_on: 2,
sample_has_redundancy: 0,
},
super::SdtpEntry {
is_leading: 2,
sample_depends_on: 1,
sample_is_depended_on: 1,
sample_has_redundancy: 1,
},
];
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("sdtp_count"), Some("4"));
assert_eq!(info.params.options.get("sdtp_leading_count"), Some("1"));
assert_eq!(info.params.options.get("sdtp_independent_count"), Some("1"));
assert_eq!(info.params.options.get("sdtp_disposable_count"), Some("2"));
assert_eq!(info.params.options.get("sdtp_redundant_count"), Some("1"));
}
#[test]
fn build_stream_info_no_sdtp_no_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 1000;
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("sdtp_count"), None);
assert_eq!(info.params.options.get("sdtp_leading_count"), None);
assert_eq!(info.params.options.get("sdtp_independent_count"), None);
assert_eq!(info.params.options.get("sdtp_disposable_count"), None);
assert_eq!(info.params.options.get("sdtp_redundant_count"), None);
}
type SubsV0Row = (u16, u8, u8, u32);
type SubsV1Row = (u32, u8, u8, u32);
fn build_subs_v0(flags: u32, entries: &[(u32, &[SubsV0Row])]) -> Vec<u8> {
let mut body = Vec::new();
body.push(0u8); body.extend_from_slice(&flags.to_be_bytes()[1..]); body.extend_from_slice(&(entries.len() as u32).to_be_bytes());
for (sample_delta, subs) in entries {
body.extend_from_slice(&sample_delta.to_be_bytes());
body.extend_from_slice(&(subs.len() as u16).to_be_bytes());
for (size, prio, disc, csp) in subs.iter() {
body.extend_from_slice(&size.to_be_bytes());
body.push(*prio);
body.push(*disc);
body.extend_from_slice(&csp.to_be_bytes());
}
}
body
}
fn build_subs_v1(flags: u32, entries: &[(u32, &[SubsV1Row])]) -> Vec<u8> {
let mut body = Vec::new();
body.push(1u8);
body.extend_from_slice(&flags.to_be_bytes()[1..]);
body.extend_from_slice(&(entries.len() as u32).to_be_bytes());
for (sample_delta, subs) in entries {
body.extend_from_slice(&sample_delta.to_be_bytes());
body.extend_from_slice(&(subs.len() as u16).to_be_bytes());
for (size, prio, disc, csp) in subs.iter() {
body.extend_from_slice(&size.to_be_bytes());
body.push(*prio);
body.push(*disc);
body.extend_from_slice(&csp.to_be_bytes());
}
}
body
}
#[test]
fn parse_subs_v0_round_trip() {
let body = build_subs_v0(
0,
&[
(
1,
&[
(100, 5, 0, 0x0000_0001),
(50, 4, 1, 0x0000_0002),
(25, 3, 0, 0),
],
),
(2, &[(80, 6, 0, 0xdead_beef)]),
],
);
let s = super::parse_subs(&body).unwrap();
assert_eq!(s.version, 0);
assert_eq!(s.flags, 0);
assert_eq!(s.entries.len(), 2);
assert_eq!(s.entries[0].sample_delta, 1);
assert_eq!(s.entries[0].subsamples.len(), 3);
assert_eq!(s.entries[0].subsamples[0].subsample_size, 100);
assert_eq!(s.entries[0].subsamples[0].subsample_priority, 5);
assert_eq!(s.entries[0].subsamples[0].discardable, 0);
assert_eq!(s.entries[0].subsamples[0].codec_specific_parameters, 1);
assert_eq!(s.entries[0].subsamples[2].subsample_size, 25);
assert_eq!(s.entries[1].sample_delta, 2);
assert_eq!(s.entries[1].subsamples.len(), 1);
assert_eq!(
s.entries[1].subsamples[0].codec_specific_parameters,
0xdead_beef
);
}
#[test]
fn parse_subs_v1_size_is_32bit() {
let body = build_subs_v1(0, &[(1, &[(0x0001_2345, 0, 0, 0)])]);
let s = super::parse_subs(&body).unwrap();
assert_eq!(s.version, 1);
assert_eq!(s.entries[0].subsamples[0].subsample_size, 0x0001_2345);
}
#[test]
fn parse_subs_empty_subsample_count() {
let body = build_subs_v0(0, &[(5, &[])]);
let s = super::parse_subs(&body).unwrap();
assert_eq!(s.entries.len(), 1);
assert_eq!(s.entries[0].sample_delta, 5);
assert!(s.entries[0].subsamples.is_empty());
}
#[test]
fn parse_subs_preserves_flags() {
let body = build_subs_v0(0x00_aa_55, &[(1, &[])]);
let s = super::parse_subs(&body).unwrap();
assert_eq!(s.flags, 0x00_aa_55);
}
#[test]
fn parse_subs_too_short() {
assert!(super::parse_subs(&[0u8; 7]).is_err());
}
#[test]
fn parse_subs_truncated_subsample() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&1u16.to_be_bytes()); body.extend_from_slice(&[0u8; 3]); assert!(super::parse_subs(&body).is_err());
}
#[test]
fn parse_stbl_picks_up_subs() {
let subs_body = build_subs_v0(0, &[(1, &[(42, 0, 0, 0)])]);
let mut stbl = Vec::new();
stbl.extend(wrap_box_full_size(b"subs", &subs_body));
let mut t = fresh_track();
super::parse_stbl(&stbl, &mut t).unwrap();
assert_eq!(t.subs.len(), 1);
assert_eq!(t.subs[0].entries[0].subsamples[0].subsample_size, 42);
}
#[test]
fn parse_stbl_accumulates_multiple_subs() {
let s1 = build_subs_v0(0, &[(1, &[(10, 0, 0, 0)])]);
let s2 = build_subs_v0(1, &[(2, &[(20, 0, 0, 0)])]);
let mut stbl = Vec::new();
stbl.extend(wrap_box_full_size(b"subs", &s1));
stbl.extend(wrap_box_full_size(b"subs", &s2));
let mut t = fresh_track();
super::parse_stbl(&stbl, &mut t).unwrap();
assert_eq!(t.subs.len(), 2);
assert_eq!(t.subs[0].flags, 0);
assert_eq!(t.subs[1].flags, 1);
}
#[test]
fn build_stream_info_surfaces_subs_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 1000;
t.subs.push(super::SubsBox {
version: 0,
flags: 0x42,
entries: vec![
super::SubsEntry {
sample_delta: 1,
subsamples: vec![
super::SubSampleEntry {
subsample_size: 100,
subsample_priority: 5,
discardable: 0,
codec_specific_parameters: 0x0000_0001,
},
super::SubSampleEntry {
subsample_size: 50,
subsample_priority: 0,
discardable: 1,
codec_specific_parameters: 0x0000_0002,
},
],
},
super::SubsEntry {
sample_delta: 3,
subsamples: Vec::new(),
},
],
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
let got = info.params.options.get("subs_0").unwrap();
assert_eq!(
got,
"v0 flags=66 delta=1:100,5,0,00000001;50,0,1,00000002 delta=3"
);
}
#[test]
fn build_stream_info_no_subs_no_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 1000;
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("subs_0"), None);
}
}