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 ssixes: Vec<SsixRecord> = Vec::new();
let mut tfras: Vec<TfraRecord> = Vec::new();
let mut prfts: Vec<PrftRecord> = Vec::new();
let mut pdin: Option<PdinRecord> = None;
while let Some(hdr) = read_box_header(&mut *input)? {
match hdr.fourcc {
FTYP => {
saw_ftyp = true;
skip_box_body(&mut *input, &hdr)?;
}
PDIN => {
let body = read_box_body(&mut *input, &hdr)?;
if pdin.is_none() {
pdin = Some(parse_pdin(&body)?);
}
}
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);
}
}
SSIX => {
let body = read_box_body(&mut *input, &hdr)?;
if let Ok(r) = parse_ssix(&body) {
ssixes.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();
let mut sai_records: Vec<SaiRecord> = Vec::new();
let mut moof_psshes: Vec<MoofPsshRecord> = Vec::new();
for moof in &moofs {
parse_moof(
moof,
&parsed.tracks,
&mut samples,
&mut next_dts,
&mut senc_records,
&mut sai_records,
&mut moof_psshes,
)?;
}
samples.sort_by_key(|s| s.offset);
let effective_duration: u64 = parsed
.mehd_fragment_duration
.filter(|&d| d > 0)
.unwrap_or(parsed.movie_duration);
let duration_micros: i64 = if parsed.movie_timescale > 0 && effective_duration > 0 {
(effective_duration as i128 * 1_000_000 / parsed.movie_timescale as i128) as i64
} else {
0
};
let mut metadata = parsed.metadata;
if let Some(d) = parsed.mehd_fragment_duration {
metadata.push(("mehd_fragment_duration".to_string(), d.to_string()));
}
if let Some(p) = pdin.as_ref() {
metadata.push(("pdin_count".to_string(), p.entries.len().to_string()));
for (n, e) in p.entries.iter().enumerate() {
metadata.push((
format!("pdin_{n}"),
format!("{} {}", e.rate, e.initial_delay),
));
}
}
if let Some(l) = parsed.leva.as_ref() {
metadata.push(("leva_count".to_string(), l.entries.len().to_string()));
for (n, e) in l.entries.iter().enumerate() {
let mut s = format!(
"{} pad={} at={}",
e.track_id, e.padding_flag as u8, e.assignment_type
);
match e.assignment_type {
0 => {
use std::fmt::Write;
let _ = write!(s, " grouping_type={}", e.grouping_type);
}
1 => {
use std::fmt::Write;
let _ = write!(
s,
" grouping_type={} grouping_type_parameter={}",
e.grouping_type, e.grouping_type_parameter,
);
}
4 => {
use std::fmt::Write;
let _ = write!(s, " sub_track_id={}", e.sub_track_id);
}
_ => {}
}
metadata.push((format!("leva_{n}"), s));
}
}
for (n, t) in parsed.treps.iter().enumerate() {
let mut s = format!("{} children={}", t.track_id, t.children.len());
for c in &t.children {
use std::fmt::Write;
let _ = write!(s, " {}", String::from_utf8_lossy(&c.fourcc));
}
metadata.push((format!("trep_{n}"), s));
}
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, s) in ssixes.iter().enumerate() {
let total_ranges: usize = s.subsegments.iter().map(|ss| ss.ranges.len()).sum();
metadata.push((
format!("ssix_{n}"),
format!("{} {}", s.subsegments.len(), total_ranges),
));
}
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,
),
));
}
for (n, r) in sai_records.iter().enumerate() {
metadata.push((
format!("frag_sai_{n}"),
format!(
"track={} seq={} saiz={} saio={}",
r.track_idx,
r.moof_sequence,
r.saiz.len(),
r.saio.len(),
),
));
}
for (n, r) in moof_psshes.iter().enumerate() {
let sysid_hex: String = r
.pssh
.system_id
.iter()
.map(|b| format!("{b:02x}"))
.collect();
metadata.push((
format!("moof_pssh_{n}"),
format!(
"systemid={} seq={} kids={} data={}",
sysid_hex,
r.moof_sequence,
r.pssh.kids.len(),
r.pssh.data.len(),
),
));
}
Ok(Box::new(Mp4Demuxer {
input,
streams,
samples,
cursor: 0,
metadata,
duration_micros,
sidxes,
ssixes,
tfras,
prfts,
pdin,
leva: parsed.leva,
treps: parsed.treps,
psshes: parsed.psshes,
moof_psshes,
senc_records,
sai_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, PartialEq, Eq)]
pub struct SsixRecord {
pub subsegments: Vec<SsixSubsegment>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SsixSubsegment {
pub ranges: Vec<SsixRange>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SsixRange {
pub level: u8,
pub range_size: u32,
}
#[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(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PdinEntry {
pub rate: u32,
pub initial_delay: u32,
}
#[derive(Clone, Debug)]
pub struct PdinRecord {
pub entries: Vec<PdinEntry>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LevaEntry {
pub track_id: u32,
pub padding_flag: bool,
pub assignment_type: u8,
pub grouping_type: u32,
pub grouping_type_parameter: u32,
pub sub_track_id: u32,
}
#[derive(Clone, Debug)]
pub struct LevaRecord {
pub entries: Vec<LevaEntry>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TrepChild {
pub fourcc: [u8; 4],
pub payload_len: usize,
}
#[derive(Clone, Debug)]
pub struct TrepRecord {
pub track_id: u32,
pub children: Vec<TrepChild>,
}
#[derive(Default)]
struct ParsedMoov {
tracks: Vec<Track>,
movie_timescale: u32,
movie_duration: u64,
mehd_fragment_duration: Option<u64>,
metadata: Vec<(String, String)>,
psshes: Vec<PsshBox>,
leva: Option<LevaRecord>,
treps: Vec<TrepRecord>,
}
#[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>)>,
trgr: Vec<([u8; 4], u32)>,
elng: Option<String>,
kinds: Vec<(String, String)>,
copyrights: Vec<CopyrightRecord>,
tsel: Option<TselBox>,
sub_tracks: Vec<SubTrack>,
cslg: Option<CslgBox>,
vmhd: Option<VmhdBox>,
dref: Option<DrefBox>,
stsh: Vec<(u32, u32)>,
sbgp: Vec<SbgpBox>,
sgpd: Vec<SgpdBox>,
csgp: Vec<CsgpBox>,
sdtp: Vec<SdtpEntry>,
stdp: Vec<u16>,
padb: Vec<u8>,
subs: Vec<SubsBox>,
saiz: Vec<SaizBox>,
saio: Vec<SaioBox>,
stsd_entries: Vec<StsdEntry>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct StsdEntry {
format: [u8; 4],
data_reference_index: u16,
}
#[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, Debug, Default)]
struct CsgpPattern {
sample_count: u32,
indices: Vec<u32>,
}
#[derive(Clone, Debug, Default)]
struct CsgpBox {
grouping_type: [u8; 4],
grouping_type_parameter: Option<u32>,
patterns: Vec<CsgpPattern>,
}
#[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, Debug, Default)]
struct SaizBox {
aux_info_type: Option<[u8; 4]>,
aux_info_type_parameter: Option<u32>,
default_sample_info_size: u8,
sample_count: u32,
per_sample: Vec<u8>,
}
#[derive(Clone, Debug, Default)]
struct SaioBox {
version: u8,
aux_info_type: Option<[u8; 4]>,
aux_info_type_parameter: Option<u32>,
offsets: Vec<u64>,
}
#[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, PartialEq, Eq)]
struct VmhdBox {
graphicsmode: u16,
opcolor: [u16; 3],
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct DataEntry {
kind: [u8; 4],
flags: u32,
name: Option<String>,
location: Option<String>,
}
impl DataEntry {
fn is_self_contained(&self) -> bool {
self.flags & 1 != 0
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct DrefBox {
entries: Vec<DataEntry>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct TselBox {
switch_group: i32,
attribute_list: Vec<[u8; 4]>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct SubTrackSampleGroup {
grouping_type: [u8; 4],
group_description_indices: Vec<u32>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct SubTrack {
switch_group: i16,
alternate_group: i16,
sub_track_id: u32,
attribute_list: Vec<[u8; 4]>,
sample_groups: Vec<SubTrackSampleGroup>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct CopyrightRecord {
language: [u8; 3],
notice: String,
}
#[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,
&mut out.mehd_fragment_duration,
&mut out.leva,
&mut out.treps,
)?;
}
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],
mehd_fragment_duration: &mut Option<u64>,
leva: &mut Option<LevaRecord>,
treps: &mut Vec<TrepRecord>,
) -> 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)?;
}
MEHD => {
let b = read_bytes_vec(&mut cur, psz)?;
if let Ok(d) = parse_mehd(&b) {
if mehd_fragment_duration.is_none() {
*mehd_fragment_duration = Some(d);
}
}
}
LEVA => {
let b = read_bytes_vec(&mut cur, psz)?;
if leva.is_none() {
if let Ok(r) = parse_leva(&b) {
*leva = Some(r);
}
}
}
TREP => {
let b = read_bytes_vec(&mut cur, psz)?;
if let Ok(r) = parse_trep(&b) {
treps.push(r);
}
}
_ => skip_cursor_bytes(&mut cur, psz),
}
}
Ok(())
}
fn parse_mehd(body: &[u8]) -> Result<u64> {
if body.is_empty() {
return Err(Error::invalid("MP4: mehd empty"));
}
let version = body[0];
match version {
0 => {
if body.len() < 8 {
return Err(Error::invalid("MP4: mehd v0 too short"));
}
Ok(u32::from_be_bytes([body[4], body[5], body[6], body[7]]) as u64)
}
1 => {
if body.len() < 12 {
return Err(Error::invalid("MP4: mehd v1 too short"));
}
Ok(u64::from_be_bytes([
body[4], body[5], body[6], body[7], body[8], body[9], body[10], body[11],
]))
}
_ => Err(Error::invalid("MP4: mehd unknown version")),
}
}
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(),
trgr: Vec::new(),
elng: None,
kinds: Vec::new(),
copyrights: Vec::new(),
tsel: None,
sub_tracks: Vec::new(),
cslg: None,
vmhd: None,
dref: None,
stsh: Vec::new(),
sbgp: Vec::new(),
sgpd: Vec::new(),
csgp: Vec::new(),
sdtp: Vec::new(),
stdp: Vec::new(),
padb: Vec::new(),
subs: Vec::new(),
saiz: Vec::new(),
saio: Vec::new(),
stsd_entries: 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)?;
}
TRGR => {
let sub = read_bytes_vec(&mut cur, psz)?;
parse_trgr(&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);
match hdr.fourcc {
KIND => parse_kind(&body[start..start + psz], t),
TSEL => parse_tsel(&body[start..start + psz], t),
STRK => parse_strk(&body[start..start + psz], t),
CPRT => parse_cprt(&body[start..start + psz], t),
_ => {
}
}
}
}
fn parse_cprt(body: &[u8], t: &mut Track) {
if body.len() < 6 {
return;
}
let version = body[0];
if version != 0 {
return;
}
let packed = u16::from_be_bytes([body[4], body[5]]);
let c0 = ((packed >> 10) & 0x1F) as u8;
let c1 = ((packed >> 5) & 0x1F) as u8;
let c2 = (packed & 0x1F) as u8;
let language = [c0 + 0x60, c1 + 0x60, c2 + 0x60];
let notice = decode_utf8_or_utf16(&body[6..]);
t.copyrights.push(CopyrightRecord { language, notice });
}
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_tsel(body: &[u8], t: &mut Track) {
if body.len() < 8 {
return;
}
let version = body[0];
if version != 0 {
return;
}
let switch_group = i32::from_be_bytes([body[4], body[5], body[6], body[7]]);
let mut attribute_list = Vec::new();
let mut i = 8;
while i + 4 <= body.len() {
let attr: [u8; 4] = [body[i], body[i + 1], body[i + 2], body[i + 3]];
attribute_list.push(attr);
i += 4;
}
t.tsel = Some(TselBox {
switch_group,
attribute_list,
});
}
fn fourcc_token(a: &[u8; 4]) -> String {
std::str::from_utf8(a)
.ok()
.filter(|s| s.chars().all(|c| !c.is_control()))
.map(|s| s.to_string())
.unwrap_or_else(|| format!("{:02x}{:02x}{:02x}{:02x}", a[0], a[1], a[2], a[3]))
}
fn parse_strk(body: &[u8], t: &mut Track) {
let mut cur = std::io::Cursor::new(body);
let end = body.len() as u64;
let mut info: Option<SubTrack> = None;
let mut sample_groups: Vec<SubTrackSampleGroup> = Vec::new();
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);
match hdr.fourcc {
STRI => info = parse_stri(&body[start..start + psz]),
STRD => sample_groups = parse_strd(&body[start..start + psz]),
_ => {
}
}
}
if let Some(mut st) = info {
st.sample_groups = sample_groups;
t.sub_tracks.push(st);
}
}
fn parse_stri(body: &[u8]) -> Option<SubTrack> {
if body.len() < 12 {
return None;
}
let version = body[0];
if version != 0 {
return None;
}
let switch_group = i16::from_be_bytes([body[4], body[5]]);
let alternate_group = i16::from_be_bytes([body[6], body[7]]);
let sub_track_id = u32::from_be_bytes([body[8], body[9], body[10], body[11]]);
let mut attribute_list = Vec::new();
let mut i = 12;
while i + 4 <= body.len() {
let attr: [u8; 4] = [body[i], body[i + 1], body[i + 2], body[i + 3]];
attribute_list.push(attr);
i += 4;
}
Some(SubTrack {
switch_group,
alternate_group,
sub_track_id,
attribute_list,
sample_groups: Vec::new(),
})
}
fn parse_strd(body: &[u8]) -> Vec<SubTrackSampleGroup> {
let mut cur = std::io::Cursor::new(body);
let end = body.len() as u64;
let mut out = Vec::new();
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 == STSG {
if let Some(sg) = parse_stsg(&body[start..start + psz]) {
out.push(sg);
}
}
}
out
}
fn parse_stsg(body: &[u8]) -> Option<SubTrackSampleGroup> {
if body.len() < 10 {
return None;
}
let version = body[0];
if version != 0 {
return None;
}
let grouping_type: [u8; 4] = [body[4], body[5], body[6], body[7]];
let item_count = u16::from_be_bytes([body[8], body[9]]) as usize;
if body.len() < 10 + item_count * 4 {
return None;
}
let mut group_description_indices = Vec::with_capacity(item_count);
let mut i = 10;
for _ in 0..item_count {
group_description_indices.push(u32::from_be_bytes([
body[i],
body[i + 1],
body[i + 2],
body[i + 3],
]));
i += 4;
}
Some(SubTrackSampleGroup {
grouping_type,
group_description_indices,
})
}
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_trgr(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() < 8 {
return Err(Error::invalid(
"MP4: trgr child too short for FullBox + track_group_id",
));
}
let version = child[0];
if version != 0 {
continue;
}
let track_group_id = u32::from_be_bytes([child[4], child[5], child[6], child[7]]);
t.trgr.push((hdr.fourcc, track_group_id));
}
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)?;
}
VMHD => {
let sub = read_bytes_vec(&mut cur, psz)?;
if t.vmhd.is_none() {
if let Ok(v) = parse_vmhd(&sub) {
t.vmhd = Some(v);
}
}
}
DINF => {
let sub = read_bytes_vec(&mut cur, psz)?;
if t.dref.is_none() {
if let Some(d) = parse_dinf(&sub) {
t.dref = Some(d);
}
}
}
_ => skip_cursor_bytes(&mut cur, psz),
}
}
Ok(())
}
fn parse_dinf(body: &[u8]) -> Option<DrefBox> {
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()??;
let psz = hdr.payload_size().unwrap_or(0) as usize;
let sub = read_bytes_vec(&mut cur, psz).ok()?;
if hdr.fourcc == DREF {
return parse_dref(&sub).ok();
}
}
None
}
fn parse_dref(body: &[u8]) -> Result<DrefBox> {
if body.len() < 8 {
return Err(Error::invalid("MP4: dref too short"));
}
let entry_count = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
let mut cur = std::io::Cursor::new(&body[8..]);
let avail = (body.len() - 8) as u64;
let max_entries = (avail / 8).max(1);
let mut entries = Vec::with_capacity(entry_count.min(max_entries as u32) as usize);
for _ in 0..entry_count {
if cur.position() >= avail {
break;
}
let hdr = match read_box_header(&mut cur)? {
Some(h) => h,
None => break,
};
let psz = match hdr.payload_size() {
Some(p) => p as usize,
None => break,
};
let child = match read_bytes_vec(&mut cur, psz) {
Ok(c) => c,
Err(_) => break,
};
if child.len() < 4 {
continue;
}
let flags = u32::from_be_bytes([0, child[1], child[2], child[3]]);
let payload = &child[4..];
match hdr.fourcc {
URL_ => {
let location = if flags & 1 != 0 {
None
} else {
read_c_string(payload)
};
entries.push(DataEntry {
kind: URL_,
flags,
name: None,
location,
});
}
URN_ => {
let (name, rest) = read_c_string_split(payload);
let location = read_c_string(rest);
entries.push(DataEntry {
kind: URN_,
flags,
name,
location,
});
}
_ => {}
}
}
Ok(DrefBox { entries })
}
fn read_c_string(buf: &[u8]) -> Option<String> {
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
if end == 0 {
return None;
}
Some(String::from_utf8_lossy(&buf[..end]).into_owned())
}
fn read_c_string_split(buf: &[u8]) -> (Option<String>, &[u8]) {
match buf.iter().position(|&b| b == 0) {
Some(pos) => {
let s = if pos == 0 {
None
} else {
Some(String::from_utf8_lossy(&buf[..pos]).into_owned())
};
(s, &buf[pos + 1..])
}
None => {
let s = if buf.is_empty() {
None
} else {
Some(String::from_utf8_lossy(buf).into_owned())
};
(s, &[])
}
}
}
fn parse_vmhd(body: &[u8]) -> Result<VmhdBox> {
if body.len() < 12 {
return Err(Error::invalid("MP4: vmhd too short"));
}
let graphicsmode = u16::from_be_bytes([body[4], body[5]]);
let opcolor = [
u16::from_be_bytes([body[6], body[7]]),
u16::from_be_bytes([body[8], body[9]]),
u16::from_be_bytes([body[10], body[11]]),
];
Ok(VmhdBox {
graphicsmode,
opcolor,
})
}
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)?,
STDP => t.stdp = parse_stdp(&b)?,
PADB => t.padb = parse_padb(&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)?),
CSGP => t.csgp.push(parse_csgp(&b)?),
SUBS => t.subs.push(parse_subs(&b)?),
SAIZ => t.saiz.push(parse_saiz(&b)?),
SAIO => t.saio.push(parse_saio(&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 mut first_entry: Option<(BoxHeader, Vec<u8>)> = None;
let max_entries = (body.len() / 16).max(1);
t.stsd_entries
.reserve(entry_count.min(max_entries as u32) as usize);
for i in 0..entry_count {
let hdr = match read_box_header(&mut cur)? {
Some(h) => h,
None => {
if i == 0 {
return Err(Error::invalid("MP4: stsd first entry missing"));
}
break;
}
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
let entry = read_bytes_vec(&mut cur, psz)?;
let data_reference_index = if entry.len() >= 8 {
u16::from_be_bytes([entry[6], entry[7]])
} else {
0
};
t.stsd_entries.push(StsdEntry {
format: hdr.fourcc,
data_reference_index,
});
if i == 0 {
first_entry = Some((hdr, entry));
}
}
let (hdr, entry) = match first_entry {
Some(pair) => pair,
None => return Err(Error::invalid("MP4: stsd first entry missing")),
};
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_stdp(body: &[u8]) -> Result<Vec<u16>> {
if body.len() < 4 {
return Err(Error::invalid("MP4: stdp too short"));
}
let payload = &body[4..];
let count = payload.len() / 2;
let mut out = Vec::with_capacity(count);
for i in 0..count {
let off = i * 2;
out.push(u16::from_be_bytes([payload[off], payload[off + 1]]));
}
Ok(out)
}
fn parse_padb(body: &[u8]) -> Result<Vec<u8>> {
if body.len() < 8 {
return Err(Error::invalid("MP4: padb too short"));
}
let sample_count = u32::from_be_bytes([body[4], body[5], body[6], body[7]]) as usize;
let need = sample_count.div_ceil(2);
if body.len() < 8 + need {
return Err(Error::invalid(
"MP4: padb body shorter than declared sample_count",
));
}
let payload = &body[8..8 + need];
let mut out = Vec::with_capacity(sample_count);
for (i, &byte) in payload.iter().enumerate() {
let pad1 = (byte >> 4) & 0x07;
out.push(pad1);
if out.len() == sample_count {
break;
}
let pad2 = byte & 0x07;
out.push(pad2);
if out.len() == sample_count {
break;
}
let _ = i;
}
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,
})
}
struct BitCursor<'a> {
data: &'a [u8],
bit_pos: usize,
}
impl<'a> BitCursor<'a> {
fn new(data: &'a [u8], byte_off: usize) -> Self {
BitCursor {
data,
bit_pos: byte_off * 8,
}
}
fn read(&mut self, n: u32) -> Option<u32> {
debug_assert!(n <= 32);
if n == 0 {
return Some(0);
}
let total_bits = self.data.len() * 8;
if self.bit_pos + n as usize > total_bits {
return None;
}
let mut value: u32 = 0;
for _ in 0..n {
let byte = self.data[self.bit_pos >> 3];
let bit = (byte >> (7 - (self.bit_pos & 7))) & 1;
value = (value << 1) | bit as u32;
self.bit_pos += 1;
}
Some(value)
}
}
fn parse_csgp(body: &[u8]) -> Result<CsgpBox> {
if body.len() < 4 {
return Err(Error::invalid("MP4: csgp too short"));
}
let flags = u32::from_be_bytes([0, body[1], body[2], body[3]]);
let index_size_code = (flags & 0x3) as u8;
let count_size_code = ((flags >> 2) & 0x3) as u8;
let pattern_size_code = ((flags >> 4) & 0x3) as u8;
let gtpp = (flags >> 6) & 0x1 == 1;
let width = |code: u8| -> u32 { 4u32 << code };
let index_w = width(index_size_code);
let count_w = width(count_size_code);
let pattern_w = width(pattern_size_code);
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 mut off = 4usize;
let grouping_type_raw =
read_u32(body, off).ok_or_else(|| Error::invalid("MP4: csgp grouping_type truncated"))?;
let grouping_type = grouping_type_raw.to_be_bytes();
off += 4;
let grouping_type_parameter = if gtpp {
let p = read_u32(body, off)
.ok_or_else(|| Error::invalid("MP4: csgp grouping_type_parameter truncated"))?;
off += 4;
Some(p)
} else {
None
};
let pattern_count = read_u32(body, off)
.ok_or_else(|| Error::invalid("MP4: csgp pattern_count truncated"))?
as usize;
off += 4;
let mut bits = BitCursor::new(body, off);
let remaining_bits = body.len().saturating_sub(off).saturating_mul(8);
let min_pattern_bits = (pattern_w + count_w) as usize;
let cap = pattern_count.min(remaining_bits / min_pattern_bits);
let mut lengths: Vec<(u32, u32)> = Vec::with_capacity(cap);
for _ in 0..pattern_count {
let pattern_length = bits
.read(pattern_w)
.ok_or_else(|| Error::invalid("MP4: csgp pattern_length truncated"))?;
let sample_count = bits
.read(count_w)
.ok_or_else(|| Error::invalid("MP4: csgp sample_count truncated"))?;
lengths.push((pattern_length, sample_count));
}
let mut patterns: Vec<CsgpPattern> = Vec::with_capacity(lengths.len());
for (pattern_length, sample_count) in lengths {
let mut indices = Vec::with_capacity(pattern_length.min(remaining_bits as u32) as usize);
for _ in 0..pattern_length {
let idx = bits
.read(index_w)
.ok_or_else(|| Error::invalid("MP4: csgp index truncated"))?;
indices.push(idx);
}
patterns.push(CsgpPattern {
sample_count,
indices,
});
}
Ok(CsgpBox {
grouping_type,
grouping_type_parameter,
patterns,
})
}
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_saiz(body: &[u8]) -> Result<SaizBox> {
if body.len() < 4 {
return Err(Error::invalid("MP4: saiz too short"));
}
let flags = u32::from_be_bytes([0, body[1], body[2], body[3]]);
let mut off = 4usize;
let mut aux_info_type: Option<[u8; 4]> = None;
let mut aux_info_type_parameter: Option<u32> = None;
if flags & 1 != 0 {
if off + 8 > body.len() {
return Err(Error::invalid("MP4: saiz aux_info_type truncated"));
}
aux_info_type = Some([body[off], body[off + 1], body[off + 2], body[off + 3]]);
off += 4;
aux_info_type_parameter = Some(u32::from_be_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
]));
off += 4;
}
if off + 5 > body.len() {
return Err(Error::invalid("MP4: saiz header truncated"));
}
let default_sample_info_size = body[off];
off += 1;
let sample_count = u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
off += 4;
let mut per_sample: Vec<u8> = Vec::new();
if default_sample_info_size == 0 {
let want = sample_count as usize;
let avail = body.len().saturating_sub(off);
per_sample = Vec::with_capacity(want.min(avail));
if off + want > body.len() {
return Err(Error::invalid("MP4: saiz sample_info_size truncated"));
}
per_sample.extend_from_slice(&body[off..off + want]);
}
Ok(SaizBox {
aux_info_type,
aux_info_type_parameter,
default_sample_info_size,
sample_count,
per_sample,
})
}
fn parse_saio(body: &[u8]) -> Result<SaioBox> {
if body.len() < 4 {
return Err(Error::invalid("MP4: saio too short"));
}
let version = body[0];
let flags = u32::from_be_bytes([0, body[1], body[2], body[3]]);
let mut off = 4usize;
let mut aux_info_type: Option<[u8; 4]> = None;
let mut aux_info_type_parameter: Option<u32> = None;
if flags & 1 != 0 {
if off + 8 > body.len() {
return Err(Error::invalid("MP4: saio aux_info_type truncated"));
}
aux_info_type = Some([body[off], body[off + 1], body[off + 2], body[off + 3]]);
off += 4;
aux_info_type_parameter = Some(u32::from_be_bytes([
body[off],
body[off + 1],
body[off + 2],
body[off + 3],
]));
off += 4;
}
if off + 4 > body.len() {
return Err(Error::invalid("MP4: saio entry_count truncated"));
}
let entry_count =
u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]) as usize;
off += 4;
let off_width = if version == 1 { 8 } else { 4 };
let avail = body.len().saturating_sub(off);
let max_entries = avail / off_width;
let mut offsets: Vec<u64> = Vec::with_capacity(entry_count.min(max_entries));
for _ in 0..entry_count {
if off + off_width > body.len() {
return Err(Error::invalid("MP4: saio offset truncated"));
}
let v = if version == 1 {
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],
])
} else {
u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]) as u64
};
offsets.push(v);
off += off_width;
}
Ok(SaioBox {
version,
aux_info_type,
aux_info_type_parameter,
offsets,
})
}
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,
}
#[derive(Clone, Debug)]
pub struct SaiRecord {
pub track_idx: u32,
pub moof_sequence: u32,
pub saiz: Vec<TrafSaiz>,
pub saio: Vec<TrafSaio>,
}
#[derive(Clone, Debug, Default)]
pub struct TrafSaiz {
pub aux_info_type: Option<[u8; 4]>,
pub aux_info_type_parameter: Option<u32>,
pub default_sample_info_size: u8,
pub sample_count: u32,
pub per_sample: Vec<u8>,
}
#[derive(Clone, Debug, Default)]
pub struct TrafSaio {
pub version: u8,
pub aux_info_type: Option<[u8; 4]>,
pub aux_info_type_parameter: Option<u32>,
pub offsets: Vec<u64>,
}
#[derive(Clone, Debug)]
pub struct MoofPsshRecord {
pub moof_sequence: u32,
pub pssh: PsshBox,
}
#[allow(clippy::too_many_arguments)] fn parse_moof(
moof: &MoofRecord,
tracks: &[Track],
samples: &mut Vec<SampleRef>,
next_dts: &mut [i64],
senc_records: &mut Vec<SencRecord>,
sai_records: &mut Vec<SaiRecord>,
moof_psshes: &mut Vec<MoofPsshRecord>,
) -> Result<()> {
let mut cur = std::io::Cursor::new(&moof.body);
let end = moof.body.len() as u64;
let mut moof_sequence: u32 = 0;
let mut pending_pssh_bodies: Vec<Vec<u8>> = Vec::new();
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,
sai_records,
)?;
}
PSSH => {
let body = read_bytes_vec(&mut cur, psz)?;
pending_pssh_bodies.push(body);
}
_ => skip_cursor_bytes(&mut cur, psz),
}
}
for body in pending_pssh_bodies {
if let Ok(pssh) = parse_pssh(&body) {
moof_psshes.push(MoofPsshRecord {
moof_sequence,
pssh,
});
}
}
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;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SampleFlags {
pub is_leading: u8,
pub sample_depends_on: u8,
pub sample_is_depended_on: u8,
pub sample_has_redundancy: u8,
pub sample_padding_value: u8,
pub sample_is_non_sync_sample: bool,
pub sample_degradation_priority: u16,
}
impl SampleFlags {
pub fn from_u32(raw: u32) -> Self {
Self {
is_leading: ((raw >> 26) & 0x03) as u8,
sample_depends_on: ((raw >> 24) & 0x03) as u8,
sample_is_depended_on: ((raw >> 22) & 0x03) as u8,
sample_has_redundancy: ((raw >> 20) & 0x03) as u8,
sample_padding_value: ((raw >> 17) & 0x07) as u8,
sample_is_non_sync_sample: (raw & SAMPLE_IS_NON_SYNC) != 0,
sample_degradation_priority: (raw & 0xFFFF) as u16,
}
}
pub fn to_u32(self) -> u32 {
((self.is_leading as u32 & 0x03) << 26)
| ((self.sample_depends_on as u32 & 0x03) << 24)
| ((self.sample_is_depended_on as u32 & 0x03) << 22)
| ((self.sample_has_redundancy as u32 & 0x03) << 20)
| ((self.sample_padding_value as u32 & 0x07) << 17)
| (if self.sample_is_non_sync_sample {
SAMPLE_IS_NON_SYNC
} else {
0
})
| self.sample_degradation_priority as u32
}
pub fn is_sync_sample(self) -> bool {
!self.sample_is_non_sync_sample
}
}
pub fn parse_sample_flags(raw: u32) -> SampleFlags {
SampleFlags::from_u32(raw)
}
#[allow(clippy::too_many_arguments)] 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>,
sai_records: &mut Vec<SaiRecord>,
) -> Result<()> {
let mut state = TrafState::default();
let mut tfhd_seen = false;
let mut senc_body: Option<Vec<u8>> = None;
let mut frag_saiz: Vec<TrafSaiz> = Vec::new();
let mut frag_saio: Vec<TrafSaio> = Vec::new();
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)?);
}
SAIZ => {
let b = read_bytes_vec(&mut cur, psz)?;
let sb = parse_saiz(&b)?;
frag_saiz.push(TrafSaiz {
aux_info_type: sb.aux_info_type,
aux_info_type_parameter: sb.aux_info_type_parameter,
default_sample_info_size: sb.default_sample_info_size,
sample_count: sb.sample_count,
per_sample: sb.per_sample,
});
}
SAIO => {
let b = read_bytes_vec(&mut cur, psz)?;
let sb = parse_saio(&b)?;
frag_saio.push(TrafSaio {
version: sb.version,
aux_info_type: sb.aux_info_type,
aux_info_type_parameter: sb.aux_info_type_parameter,
offsets: sb.offsets,
});
}
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,
});
}
}
}
if !frag_saiz.is_empty() || !frag_saio.is_empty() {
sai_records.push(SaiRecord {
track_idx: state.track_idx as u32,
moof_sequence,
saiz: frag_saiz,
saio: frag_saio,
});
}
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_ssix(body: &[u8]) -> Result<SsixRecord> {
if body.len() < 8 {
return Err(Error::invalid("MP4: ssix too short"));
}
if body[0] != 0 {
return Err(Error::invalid("MP4: ssix unsupported version"));
}
let subsegment_count = u32::from_be_bytes([body[4], body[5], body[6], body[7]]) as usize;
let mut cursor = 8usize;
let mut subsegments =
Vec::with_capacity(subsegment_count.min(body.len().saturating_sub(cursor) / 4));
for _ in 0..subsegment_count {
if body.len() - cursor < 4 {
return Err(Error::invalid("MP4: ssix subsegment truncated"));
}
let range_count = u32::from_be_bytes([
body[cursor],
body[cursor + 1],
body[cursor + 2],
body[cursor + 3],
]) as usize;
cursor += 4;
if (body.len() - cursor) / 4 < range_count {
return Err(Error::invalid("MP4: ssix range list truncated"));
}
let mut ranges = Vec::with_capacity(range_count);
for _ in 0..range_count {
let level = body[cursor];
let range_size =
u32::from_be_bytes([0, body[cursor + 1], body[cursor + 2], body[cursor + 3]]);
cursor += 4;
ranges.push(SsixRange { level, range_size });
}
subsegments.push(SsixSubsegment { ranges });
}
Ok(SsixRecord { subsegments })
}
fn parse_pdin(body: &[u8]) -> Result<PdinRecord> {
if body.len() < 4 {
return Err(Error::invalid("MP4: pdin too short"));
}
let payload = &body[4..];
if payload.len() % 8 != 0 {
return Err(Error::invalid(
"MP4: pdin payload not a multiple of 8 bytes (rate + initial_delay pairs)",
));
}
let count = payload.len() / 8;
let mut entries = Vec::with_capacity(count);
for i in 0..count {
let off = i * 8;
let rate = u32::from_be_bytes([
payload[off],
payload[off + 1],
payload[off + 2],
payload[off + 3],
]);
let initial_delay = u32::from_be_bytes([
payload[off + 4],
payload[off + 5],
payload[off + 6],
payload[off + 7],
]);
entries.push(PdinEntry {
rate,
initial_delay,
});
}
Ok(PdinRecord { entries })
}
fn parse_trep(body: &[u8]) -> Result<TrepRecord> {
if body.len() < 4 {
return Err(Error::invalid("MP4: trep too short for FullBox preamble"));
}
let payload = &body[4..];
if payload.len() < 4 {
return Err(Error::invalid("MP4: trep truncated track_id"));
}
let track_id = u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
let mut children = Vec::new();
let mut off = 4usize;
while off + 8 <= payload.len() {
let size32 = u32::from_be_bytes([
payload[off],
payload[off + 1],
payload[off + 2],
payload[off + 3],
]) as usize;
let fourcc = [
payload[off + 4],
payload[off + 5],
payload[off + 6],
payload[off + 7],
];
let (header_len, box_size) = match size32 {
0 => (8usize, payload.len() - off),
1 => {
if off + 16 > payload.len() {
break;
}
let large = u64::from_be_bytes([
payload[off + 8],
payload[off + 9],
payload[off + 10],
payload[off + 11],
payload[off + 12],
payload[off + 13],
payload[off + 14],
payload[off + 15],
]) as usize;
(16usize, large)
}
n => (8usize, n),
};
if box_size < header_len {
break;
}
let available = payload.len() - off;
let effective = box_size.min(available);
let payload_len = effective - header_len;
children.push(TrepChild {
fourcc,
payload_len,
});
off += effective;
if effective < box_size {
break;
}
}
Ok(TrepRecord { track_id, children })
}
fn parse_leva(body: &[u8]) -> Result<LevaRecord> {
if body.len() < 4 {
return Err(Error::invalid("MP4: leva too short for FullBox preamble"));
}
let payload = &body[4..];
if payload.is_empty() {
return Err(Error::invalid("MP4: leva missing level_count byte"));
}
let level_count = payload[0] as usize;
let mut cursor = 1usize;
let mut entries = Vec::with_capacity(level_count);
for _ in 0..level_count {
if cursor + 5 > payload.len() {
return Err(Error::invalid(
"MP4: leva truncated entry header (need 5 bytes for track_id+padding+assignment)",
));
}
let track_id = u32::from_be_bytes([
payload[cursor],
payload[cursor + 1],
payload[cursor + 2],
payload[cursor + 3],
]);
let packed = payload[cursor + 4];
let padding_flag = (packed & 0x80) != 0;
let assignment_type = packed & 0x7f;
cursor += 5;
let mut grouping_type = 0u32;
let mut grouping_type_parameter = 0u32;
let mut sub_track_id = 0u32;
match assignment_type {
0 => {
if cursor + 4 > payload.len() {
return Err(Error::invalid(
"MP4: leva truncated grouping_type for assignment_type=0",
));
}
grouping_type = u32::from_be_bytes([
payload[cursor],
payload[cursor + 1],
payload[cursor + 2],
payload[cursor + 3],
]);
cursor += 4;
}
1 => {
if cursor + 8 > payload.len() {
return Err(Error::invalid(
"MP4: leva truncated grouping_type/parameter for assignment_type=1",
));
}
grouping_type = u32::from_be_bytes([
payload[cursor],
payload[cursor + 1],
payload[cursor + 2],
payload[cursor + 3],
]);
grouping_type_parameter = u32::from_be_bytes([
payload[cursor + 4],
payload[cursor + 5],
payload[cursor + 6],
payload[cursor + 7],
]);
cursor += 8;
}
2 | 3 => {
}
4 => {
if cursor + 4 > payload.len() {
return Err(Error::invalid(
"MP4: leva truncated sub_track_id for assignment_type=4",
));
}
sub_track_id = u32::from_be_bytes([
payload[cursor],
payload[cursor + 1],
payload[cursor + 2],
payload[cursor + 3],
]);
cursor += 4;
}
_ => {
}
}
entries.push(LevaEntry {
track_id,
padding_flag,
assignment_type,
grouping_type,
grouping_type_parameter,
sub_track_id,
});
}
Ok(LevaRecord { entries })
}
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, (group_type, group_id)) in t.trgr.iter().enumerate() {
let type_str = std::str::from_utf8(group_type)
.map(|s| s.to_string())
.unwrap_or_else(|_| {
format!(
"{:02x}{:02x}{:02x}{:02x}",
group_type[0], group_type[1], group_type[2], group_type[3]
)
});
params
.options
.insert(format!("trgr_{}", i), format!("{} {}", type_str, group_id));
}
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);
}
for (i, c) in t.copyrights.iter().enumerate() {
let lang_str = if c.language.iter().all(|&b| b.is_ascii_lowercase()) {
std::str::from_utf8(&c.language).unwrap().to_string()
} else {
let raw: u32 = ((c.language[0].wrapping_sub(0x60) as u32) << 10)
| ((c.language[1].wrapping_sub(0x60) as u32) << 5)
| (c.language[2].wrapping_sub(0x60) as u32);
format!("{:08x}", raw)
};
if !c.notice.is_empty() {
params
.options
.insert(format!("copyright_{}", i), c.notice.clone());
}
params
.options
.insert(format!("copyright_{}_lang", i), lang_str);
}
if let Some(tsel) = &t.tsel {
params
.options
.insert("tsel_switch_group", tsel.switch_group.to_string());
if !tsel.attribute_list.is_empty() {
let v = tsel
.attribute_list
.iter()
.map(|a| {
std::str::from_utf8(a)
.ok()
.filter(|s| s.chars().all(|c| !c.is_control()))
.map(|s| s.to_string())
.unwrap_or_else(|| {
format!("{:02x}{:02x}{:02x}{:02x}", a[0], a[1], a[2], a[3])
})
})
.collect::<Vec<_>>()
.join(" ");
params.options.insert("tsel_attributes", v);
}
}
for (n, st) in t.sub_tracks.iter().enumerate() {
use std::fmt::Write;
let mut s = format!(
"id={} switch={} alt={}",
st.sub_track_id, st.switch_group, st.alternate_group
);
if !st.attribute_list.is_empty() {
let attrs = st
.attribute_list
.iter()
.map(fourcc_token)
.collect::<Vec<_>>()
.join(" ");
let _ = write!(s, " attrs={attrs}");
}
if !st.sample_groups.is_empty() {
let groups = st
.sample_groups
.iter()
.map(|sg| {
let idxs = sg
.group_description_indices
.iter()
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join(",");
format!("{}:{}", fourcc_token(&sg.grouping_type), idxs)
})
.collect::<Vec<_>>()
.join(";");
let _ = write!(s, " stsg={groups}");
}
params.options.insert(format!("subtrack_{n}"), s);
}
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(),
);
}
if let Some(v) = &t.vmhd {
params
.options
.insert("vmhd_graphicsmode", v.graphicsmode.to_string());
params.options.insert(
"vmhd_opcolor",
format!("{} {} {}", v.opcolor[0], v.opcolor[1], v.opcolor[2]),
);
}
if let Some(d) = &t.dref {
let all_self_contained =
!d.entries.is_empty() && d.entries.iter().all(|e| e.is_self_contained());
let single_self_contained = d.entries.len() == 1 && all_self_contained;
params.options.insert(
"dref_self_contained",
if all_self_contained { "true" } else { "false" }.to_string(),
);
if !single_self_contained {
params
.options
.insert("dref_count".to_string(), d.entries.len().to_string());
for (i, e) in d.entries.iter().enumerate() {
let mut s = format!("{} self={}", fourcc_token(&e.kind), e.is_self_contained());
if let Some(name) = &e.name {
s.push_str(" name=");
s.push_str(name);
}
if let Some(loc) = &e.location {
s.push_str(" loc=");
s.push_str(loc);
}
params.options.insert(format!("dref_{}", i + 1), s);
}
}
}
for (i, (shadowed, sync)) in t.stsh.iter().enumerate() {
params
.options
.insert(format!("stsh_{}", i), format!("{} {}", shadowed, sync));
}
if t.stsd_entries.len() > 1 {
params
.options
.insert("stsd_count".to_string(), t.stsd_entries.len().to_string());
for (i, e) in t.stsd_entries.iter().enumerate() {
params.options.insert(
format!("stsd_{}", i + 1),
format!(
"{} dref={}",
fourcc_token(&e.format),
e.data_reference_index
),
);
}
}
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);
}
for (i, cg) in t.csgp.iter().enumerate() {
let mut v = render_grouping_type(&cg.grouping_type);
if let Some(p) = cg.grouping_type_parameter {
v.push_str(&format!(" param={}", p));
}
for pat in &cg.patterns {
v.push(' ');
v.push_str(&format!("{}*", pat.sample_count));
let idx_list = pat
.indices
.iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(",");
v.push_str(&idx_list);
}
params.options.insert(format!("csgp_{}", 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());
}
if !t.stdp.is_empty() {
let mut lo = u16::MAX;
let mut hi = 0u16;
let mut sum: u64 = 0;
for &p in &t.stdp {
if p < lo {
lo = p;
}
if p > hi {
hi = p;
}
sum += p as u64;
}
params
.options
.insert("stdp_count".to_string(), t.stdp.len().to_string());
params
.options
.insert("stdp_min".to_string(), lo.to_string());
params
.options
.insert("stdp_max".to_string(), hi.to_string());
params
.options
.insert("stdp_sum".to_string(), sum.to_string());
}
if !t.padb.is_empty() {
let mut hist = [0u64; 8];
let mut max_pad = 0u8;
let mut nonzero: u64 = 0;
for &p in &t.padb {
let bucket = (p & 0x07) as usize;
hist[bucket] = hist[bucket].saturating_add(1);
if p > max_pad {
max_pad = p;
}
if p != 0 {
nonzero += 1;
}
}
params
.options
.insert("padb_count".to_string(), t.padb.len().to_string());
params
.options
.insert("padb_max".to_string(), max_pad.to_string());
params
.options
.insert("padb_nonzero_count".to_string(), nonzero.to_string());
params.options.insert(
"padb_hist".to_string(),
format!(
"{}:{}:{}:{}:{}:{}:{}:{}",
hist[0], hist[1], hist[2], hist[3], hist[4], hist[5], hist[6], hist[7]
),
);
}
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);
}
for (i, sz) in t.saiz.iter().enumerate() {
let mut v = String::new();
if let Some(t4) = &sz.aux_info_type {
v.push_str(&format!(
"type={} ",
std::str::from_utf8(t4).unwrap_or("????")
));
}
if let Some(p) = sz.aux_info_type_parameter {
v.push_str(&format!("param={p} "));
}
v.push_str(&format!(
"default_size={} count={}",
sz.default_sample_info_size, sz.sample_count
));
if !sz.per_sample.is_empty() {
v.push_str(" sizes=");
let mut first = true;
for s in &sz.per_sample {
if !first {
v.push(',');
}
first = false;
v.push_str(&format!("{s}"));
}
}
params.options.insert(format!("saiz_{}", i), v);
}
for (i, so) in t.saio.iter().enumerate() {
let mut v = format!("v{}", so.version);
if let Some(t4) = &so.aux_info_type {
v.push_str(&format!(
" type={}",
std::str::from_utf8(t4).unwrap_or("????")
));
}
if let Some(p) = so.aux_info_type_parameter {
v.push_str(&format!(" param={p}"));
}
v.push_str(" offsets=");
let mut first = true;
for o in &so.offsets {
if !first {
v.push(',');
}
first = false;
v.push_str(&format!("{o}"));
}
params.options.insert(format!("saio_{}", 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>,
#[allow(dead_code)]
ssixes: Vec<SsixRecord>,
tfras: Vec<TfraRecord>,
#[allow(dead_code)]
prfts: Vec<PrftRecord>,
#[allow(dead_code)]
pdin: Option<PdinRecord>,
#[allow(dead_code)]
leva: Option<LevaRecord>,
#[allow(dead_code)]
treps: Vec<TrepRecord>,
#[allow(dead_code)]
psshes: Vec<PsshBox>,
#[allow(dead_code)]
moof_psshes: Vec<MoofPsshRecord>,
#[allow(dead_code)]
senc_records: Vec<SencRecord>,
#[allow(dead_code)]
sai_records: Vec<SaiRecord>,
#[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 moof_psshes(&self) -> &[MoofPsshRecord] {
&self.moof_psshes
}
#[allow(dead_code)]
pub fn senc_records(&self) -> &[SencRecord] {
&self.senc_records
}
#[allow(dead_code)]
pub fn pdin_entries(&self) -> Option<&[PdinEntry]> {
self.pdin.as_ref().map(|p| p.entries.as_slice())
}
#[allow(dead_code)]
pub fn leva_entries(&self) -> Option<&[LevaEntry]> {
self.leva.as_ref().map(|l| l.entries.as_slice())
}
#[allow(dead_code)]
pub fn treps(&self) -> &[TrepRecord] {
&self.treps
}
#[allow(dead_code)]
pub fn ssixes(&self) -> &[SsixRecord] {
&self.ssixes
}
#[allow(dead_code)]
pub fn sai_records(&self) -> &[SaiRecord] {
&self.sai_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_ssix_box(body: &[u8]) -> Result<SsixRecord> {
parse_ssix(body)
}
pub fn build_ssix_box(record: &SsixRecord) -> Result<Vec<u8>> {
let mut body = Vec::with_capacity(
8 + record
.subsegments
.iter()
.map(|s| 4 + 4 * s.ranges.len())
.sum::<usize>(),
);
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&(record.subsegments.len() as u32).to_be_bytes());
for s in &record.subsegments {
body.extend_from_slice(&(s.ranges.len() as u32).to_be_bytes());
for r in &s.ranges {
if r.range_size > 0xFF_FFFF {
return Err(Error::invalid("MP4: ssix range_size exceeds 24 bits"));
}
let b = r.range_size.to_be_bytes();
body.extend_from_slice(&[r.level, b[1], b[2], b[3]]);
}
}
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"ssix");
out.extend_from_slice(&body);
Ok(out)
}
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)
}
pub fn parse_pdin_box(body: &[u8]) -> Result<PdinRecord> {
parse_pdin(body)
}
pub fn parse_leva_box(body: &[u8]) -> Result<LevaRecord> {
parse_leva(body)
}
pub fn parse_trep_box(body: &[u8]) -> Result<TrepRecord> {
parse_trep(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(),
trgr: Vec::new(),
elng: None,
kinds: Vec::new(),
copyrights: Vec::new(),
tsel: None,
sub_tracks: Vec::new(),
cslg: None,
vmhd: None,
dref: None,
stsh: Vec::new(),
sbgp: Vec::new(),
sgpd: Vec::new(),
csgp: Vec::new(),
sdtp: Vec::new(),
stdp: Vec::new(),
padb: Vec::new(),
subs: Vec::new(),
saiz: Vec::new(),
saio: Vec::new(),
stsd_entries: 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 audio_sample_entry_body(channels: u16, dref: u16) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&[0u8; 6]); out.extend_from_slice(&dref.to_be_bytes()); out.extend_from_slice(&[0u8; 8]); out.extend_from_slice(&channels.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()); out
}
fn boxed_sample_entry(fourcc: &[u8; 4], body: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let total = (8 + body.len()) as u32;
out.extend_from_slice(&total.to_be_bytes());
out.extend_from_slice(fourcc);
out.extend_from_slice(body);
out
}
fn build_stsd(entries: &[Vec<u8>]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&[0u8; 4]); out.extend_from_slice(&(entries.len() as u32).to_be_bytes());
for e in entries {
out.extend_from_slice(e);
}
out
}
#[test]
fn stsd_records_all_entries_first_drives_active_codec() {
let e0 = boxed_sample_entry(b"mp4a", &audio_sample_entry_body(2, 1));
let e1 = boxed_sample_entry(b"ac-3", &audio_sample_entry_body(6, 1));
let e2 = boxed_sample_entry(&[0x00, 0x01, 0x02, 0x03], &audio_sample_entry_body(1, 2));
let stsd = build_stsd(&[e0, e1, e2]);
let mut t = fresh_track();
super::parse_stsd(&stsd, &mut t).unwrap();
assert_eq!(&t.codec_id_fourcc, b"mp4a");
assert_eq!(t.channels, Some(2));
assert_eq!(t.stsd_entries.len(), 3);
assert_eq!(&t.stsd_entries[0].format, b"mp4a");
assert_eq!(t.stsd_entries[0].data_reference_index, 1);
assert_eq!(&t.stsd_entries[1].format, b"ac-3");
assert_eq!(t.stsd_entries[1].data_reference_index, 1);
assert_eq!(t.stsd_entries[2].format, [0x00, 0x01, 0x02, 0x03]);
assert_eq!(t.stsd_entries[2].data_reference_index, 2);
}
#[test]
fn build_stream_info_surfaces_multi_stsd_on_options() {
let e0 = boxed_sample_entry(b"mp4a", &audio_sample_entry_body(2, 1));
let e1 = boxed_sample_entry(b"ac-3", &audio_sample_entry_body(6, 3));
let stsd = build_stsd(&[e0, e1]);
let mut t = fresh_track();
t.timescale = 48_000;
super::parse_stsd(&stsd, &mut t).unwrap();
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("stsd_count"), Some("2"));
assert_eq!(info.params.options.get("stsd_1"), Some("mp4a dref=1"));
assert_eq!(info.params.options.get("stsd_2"), Some("ac-3 dref=3"));
assert_eq!(info.params.options.get("stsd_3"), None);
}
#[test]
fn build_stream_info_single_stsd_emits_no_keys() {
let e0 = boxed_sample_entry(b"mp4a", &audio_sample_entry_body(2, 1));
let stsd = build_stsd(&[e0]);
let mut t = fresh_track();
super::parse_stsd(&stsd, &mut t).unwrap();
assert_eq!(t.stsd_entries.len(), 1);
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("stsd_count"), None);
assert_eq!(info.params.options.get("stsd_1"), None);
}
#[test]
fn stsd_overcount_stops_at_present_entries() {
let e0 = boxed_sample_entry(b"mp4a", &audio_sample_entry_body(2, 1));
let e1 = boxed_sample_entry(b"ac-3", &audio_sample_entry_body(6, 1));
let mut stsd = Vec::new();
stsd.extend_from_slice(&[0u8; 4]); stsd.extend_from_slice(&9u32.to_be_bytes()); stsd.extend_from_slice(&e0);
stsd.extend_from_slice(&e1);
let mut t = fresh_track();
super::parse_stsd(&stsd, &mut t).unwrap();
assert_eq!(t.stsd_entries.len(), 2);
assert_eq!(&t.codec_id_fourcc, b"mp4a");
}
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_trgr_msrc_single_child() {
let mut inner_body = Vec::new();
inner_body.extend_from_slice(&[0u8; 4]); inner_body.extend_from_slice(&42u32.to_be_bytes());
let body = wrap_box_full_size(b"msrc", &inner_body);
let mut t = fresh_track();
super::parse_trgr(&body, &mut t).unwrap();
assert_eq!(t.trgr.len(), 1);
assert_eq!(&t.trgr[0].0, b"msrc");
assert_eq!(t.trgr[0].1, 42);
}
#[test]
fn parse_trgr_multiple_types() {
let mut body = Vec::new();
let mut msrc_payload = Vec::new();
msrc_payload.extend_from_slice(&[0u8; 4]); msrc_payload.extend_from_slice(&7u32.to_be_bytes());
body.extend(wrap_box_full_size(b"msrc", &msrc_payload));
let mut foo_payload = Vec::new();
foo_payload.extend_from_slice(&[0u8; 4]); foo_payload.extend_from_slice(&99u32.to_be_bytes());
body.extend(wrap_box_full_size(b"foo ", &foo_payload));
let mut t = fresh_track();
super::parse_trgr(&body, &mut t).unwrap();
assert_eq!(t.trgr.len(), 2);
assert_eq!(&t.trgr[0].0, b"msrc");
assert_eq!(t.trgr[0].1, 7);
assert_eq!(&t.trgr[1].0, b"foo ");
assert_eq!(t.trgr[1].1, 99);
}
#[test]
fn parse_trgr_repeated_type_kept_in_order() {
let mut body = Vec::new();
let mut p1 = Vec::new();
p1.extend_from_slice(&[0u8; 4]);
p1.extend_from_slice(&1u32.to_be_bytes());
body.extend(wrap_box_full_size(b"msrc", &p1));
let mut p2 = Vec::new();
p2.extend_from_slice(&[0u8; 4]);
p2.extend_from_slice(&2u32.to_be_bytes());
body.extend(wrap_box_full_size(b"msrc", &p2));
let mut t = fresh_track();
super::parse_trgr(&body, &mut t).unwrap();
assert_eq!(t.trgr.len(), 2);
assert_eq!(t.trgr[0].1, 1);
assert_eq!(t.trgr[1].1, 2);
}
#[test]
fn parse_trgr_short_child_rejected() {
let body = wrap_box_full_size(b"msrc", &[0u8, 0, 0, 0, 0, 0, 0]);
let mut t = fresh_track();
let err = super::parse_trgr(&body, &mut t).unwrap_err();
assert!(matches!(err, oxideav_core::Error::InvalidData(_)));
}
#[test]
fn parse_trgr_skips_unknown_version_child() {
let mut body = Vec::new();
let mut p1 = Vec::new();
p1.extend_from_slice(&[1u8, 0, 0, 0]); p1.extend_from_slice(&5u32.to_be_bytes());
body.extend(wrap_box_full_size(b"msrc", &p1));
let mut p2 = Vec::new();
p2.extend_from_slice(&[0u8; 4]);
p2.extend_from_slice(&8u32.to_be_bytes());
body.extend(wrap_box_full_size(b"msrc", &p2));
let mut t = fresh_track();
super::parse_trgr(&body, &mut t).unwrap();
assert_eq!(t.trgr.len(), 1);
assert_eq!(t.trgr[0].1, 8);
}
#[test]
fn parse_trgr_ignores_trailing_extension_bytes() {
let mut inner_body = Vec::new();
inner_body.extend_from_slice(&[0u8; 4]); inner_body.extend_from_slice(&13u32.to_be_bytes());
inner_body.extend_from_slice(&[0xCA, 0xFE, 0xBA, 0xBE]); let body = wrap_box_full_size(b"msrc", &inner_body);
let mut t = fresh_track();
super::parse_trgr(&body, &mut t).unwrap();
assert_eq!(t.trgr.len(), 1);
assert_eq!(t.trgr[0].1, 13);
}
#[test]
fn parse_trgr_empty_body_is_ok() {
let mut t = fresh_track();
super::parse_trgr(&[], &mut t).unwrap();
assert!(t.trgr.is_empty());
}
#[test]
fn parse_trak_picks_up_nested_trgr() {
let mut tkhd = vec![0u8; 92]; tkhd[12..16].copy_from_slice(&1u32.to_be_bytes());
let mut mdhd = vec![0u8; 24];
mdhd[12..16].copy_from_slice(&1000u32.to_be_bytes());
let mut hdlr = Vec::new();
hdlr.extend_from_slice(&[0u8; 8]); hdlr.extend_from_slice(b"vide"); hdlr.extend_from_slice(&[0u8; 12]); hdlr.extend_from_slice(b"\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 trgr_body = Vec::new();
let mut msrc_body = Vec::new();
msrc_body.extend_from_slice(&[0u8; 4]); msrc_body.extend_from_slice(&555u32.to_be_bytes());
trgr_body.extend(wrap_box_full_size(b"msrc", &msrc_body));
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"trgr", &trgr_body));
let t = super::parse_trak(&trak).unwrap().unwrap();
assert_eq!(t.trgr.len(), 1);
assert_eq!(&t.trgr[0].0, b"msrc");
assert_eq!(t.trgr[0].1, 555);
}
#[test]
fn build_stream_info_surfaces_trgr_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 90_000;
t.trgr.push((*b"msrc", 7));
t.trgr.push((*b"msrc", 42)); let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("trgr_0"), Some("msrc 7"));
assert_eq!(info.params.options.get("trgr_1"), Some("msrc 42"));
}
#[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");
}
fn pack_lang(tag: &[u8; 3]) -> [u8; 2] {
let c0 = (tag[0] - 0x60) as u16;
let c1 = (tag[1] - 0x60) as u16;
let c2 = (tag[2] - 0x60) as u16;
let packed = (c0 << 10) | (c1 << 5) | c2;
packed.to_be_bytes()
}
#[test]
fn parse_cprt_ascii_eng_notice() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&pack_lang(b"eng"));
body.extend_from_slice(b"(c) 2026 Example\0");
let mut t = fresh_track();
super::parse_cprt(&body, &mut t);
assert_eq!(t.copyrights.len(), 1);
assert_eq!(&t.copyrights[0].language, b"eng");
assert_eq!(t.copyrights[0].notice, "(c) 2026 Example");
}
#[test]
fn parse_cprt_utf16be_notice_with_bom() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&pack_lang(b"jpn"));
body.extend_from_slice(&[0xFE, 0xFF]);
body.extend_from_slice(&[0x30, 0x42]);
body.extend_from_slice(&[0x00, 0x00]);
let mut t = fresh_track();
super::parse_cprt(&body, &mut t);
assert_eq!(t.copyrights.len(), 1);
assert_eq!(&t.copyrights[0].language, b"jpn");
assert_eq!(t.copyrights[0].notice, "あ");
}
#[test]
fn parse_cprt_unknown_version_is_dropped() {
let mut body = Vec::new();
body.push(1); body.extend_from_slice(&[0u8; 3]); body.extend_from_slice(&pack_lang(b"eng"));
body.extend_from_slice(b"ignored\0");
let mut t = fresh_track();
super::parse_cprt(&body, &mut t);
assert!(t.copyrights.is_empty());
}
#[test]
fn parse_cprt_too_short_is_silently_dropped() {
let mut t = fresh_track();
super::parse_cprt(&[0, 0, 0, 0, 0], &mut t);
assert!(t.copyrights.is_empty());
}
#[test]
fn parse_cprt_empty_notice_preserves_language() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]);
body.extend_from_slice(&pack_lang(b"fra"));
body.push(0); let mut t = fresh_track();
super::parse_cprt(&body, &mut t);
assert_eq!(t.copyrights.len(), 1);
assert_eq!(&t.copyrights[0].language, b"fra");
assert_eq!(t.copyrights[0].notice, "");
}
#[test]
fn parse_track_udta_collects_multiple_cprts() {
let mut c1 = Vec::new();
c1.extend_from_slice(&[0u8; 4]);
c1.extend_from_slice(&pack_lang(b"eng"));
c1.extend_from_slice(b"(c) 2026 Example\0");
let mut c2 = Vec::new();
c2.extend_from_slice(&[0u8; 4]);
c2.extend_from_slice(&pack_lang(b"fra"));
c2.extend_from_slice(b"(c) 2026 Exemple\0");
let mut udta = Vec::new();
udta.extend(wrap_box_full_size(b"cprt", &c1));
udta.extend(wrap_box_full_size(b"cprt", &c2));
let mut t = fresh_track();
super::parse_track_udta(&udta, &mut t);
assert_eq!(t.copyrights.len(), 2);
assert_eq!(&t.copyrights[0].language, b"eng");
assert_eq!(t.copyrights[0].notice, "(c) 2026 Example");
assert_eq!(&t.copyrights[1].language, b"fra");
assert_eq!(t.copyrights[1].notice, "(c) 2026 Exemple");
}
#[test]
fn build_stream_info_surfaces_copyrights_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Audio;
t.codec_id_fourcc = *b"mp4a";
t.timescale = 48000;
t.copyrights.push(super::CopyrightRecord {
language: *b"eng",
notice: "(c) 2026 Example".to_string(),
});
t.copyrights.push(super::CopyrightRecord {
language: *b"jpn",
notice: "".to_string(),
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(
info.params.options.get("copyright_0"),
Some("(c) 2026 Example")
);
assert_eq!(info.params.options.get("copyright_0_lang"), Some("eng"));
assert_eq!(info.params.options.get("copyright_1"), None);
assert_eq!(info.params.options.get("copyright_1_lang"), Some("jpn"));
assert_eq!(info.params.options.get("copyright_2"), None);
}
#[test]
fn build_stream_info_no_copyrights_no_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 90000;
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("copyright_0"), None);
assert_eq!(info.params.options.get("copyright_0_lang"), None);
}
#[test]
fn parse_trak_picks_up_nested_cprt() {
let mut tkhd = vec![0u8; 84];
tkhd[12..16].copy_from_slice(&11u32.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 cprt = Vec::new();
cprt.extend_from_slice(&[0u8; 4]);
cprt.extend_from_slice(&pack_lang(b"eng"));
cprt.extend_from_slice(b"(c) 2026 Example\0");
let mut udta = Vec::new();
udta.extend(wrap_box_full_size(b"cprt", &cprt));
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, 11);
assert_eq!(t.copyrights.len(), 1);
assert_eq!(&t.copyrights[0].language, b"eng");
assert_eq!(t.copyrights[0].notice, "(c) 2026 Example");
}
#[test]
fn parse_tsel_switch_group_and_one_attribute() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&1i32.to_be_bytes()); body.extend_from_slice(b"bitr"); let mut t = fresh_track();
super::parse_tsel(&body, &mut t);
let tsel = t.tsel.as_ref().unwrap();
assert_eq!(tsel.switch_group, 1);
assert_eq!(tsel.attribute_list, vec![*b"bitr"]);
}
#[test]
fn parse_tsel_multiple_attributes_preserve_order() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&42i32.to_be_bytes()); body.extend_from_slice(b"bitr"); body.extend_from_slice(b"cdec"); body.extend_from_slice(b"lang"); let mut t = fresh_track();
super::parse_tsel(&body, &mut t);
let tsel = t.tsel.as_ref().unwrap();
assert_eq!(tsel.switch_group, 42);
assert_eq!(tsel.attribute_list, vec![*b"bitr", *b"cdec", *b"lang"]);
}
#[test]
fn parse_tsel_zero_switch_group_no_attributes() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&0i32.to_be_bytes()); let mut t = fresh_track();
super::parse_tsel(&body, &mut t);
let tsel = t.tsel.as_ref().unwrap();
assert_eq!(tsel.switch_group, 0);
assert!(tsel.attribute_list.is_empty());
}
#[test]
fn parse_tsel_negative_switch_group_preserved() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&(-7i32).to_be_bytes()); let mut t = fresh_track();
super::parse_tsel(&body, &mut t);
let tsel = t.tsel.as_ref().unwrap();
assert_eq!(tsel.switch_group, -7);
}
#[test]
fn parse_tsel_too_short_silently_dropped() {
let mut t = fresh_track();
super::parse_tsel(&[0u8; 7], &mut t);
assert!(t.tsel.is_none());
}
#[test]
fn parse_tsel_unknown_version_silently_dropped() {
let mut body = Vec::new();
body.extend_from_slice(&[1, 0, 0, 0]); body.extend_from_slice(&5i32.to_be_bytes()); let mut t = fresh_track();
super::parse_tsel(&body, &mut t);
assert!(t.tsel.is_none());
}
#[test]
fn parse_tsel_trailing_partial_fourcc_ignored() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&3i32.to_be_bytes()); body.extend_from_slice(b"bitr"); body.extend_from_slice(b"xy"); let mut t = fresh_track();
super::parse_tsel(&body, &mut t);
let tsel = t.tsel.as_ref().unwrap();
assert_eq!(tsel.switch_group, 3);
assert_eq!(tsel.attribute_list, vec![*b"bitr"]);
}
#[test]
fn parse_track_udta_picks_up_tsel() {
let mut tsel = Vec::new();
tsel.extend_from_slice(&[0u8; 4]); tsel.extend_from_slice(&9i32.to_be_bytes()); tsel.extend_from_slice(b"bitr");
tsel.extend_from_slice(b"lang");
let mut udta = Vec::new();
udta.extend(wrap_box_full_size(b"tsel", &tsel));
let mut t = fresh_track();
super::parse_track_udta(&udta, &mut t);
let tb = t.tsel.as_ref().unwrap();
assert_eq!(tb.switch_group, 9);
assert_eq!(tb.attribute_list, vec![*b"bitr", *b"lang"]);
}
#[test]
fn parse_track_udta_collects_tsel_alongside_kind() {
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"alternate\0");
let mut tsel = Vec::new();
tsel.extend_from_slice(&[0u8; 4]);
tsel.extend_from_slice(&2i32.to_be_bytes());
tsel.extend_from_slice(b"cdec");
let mut udta = Vec::new();
udta.extend(wrap_box_full_size(b"kind", &kind));
udta.extend(wrap_box_full_size(b"tsel", &tsel));
let mut t = fresh_track();
super::parse_track_udta(&udta, &mut t);
assert_eq!(t.kinds.len(), 1);
assert_eq!(t.kinds[0].1, "alternate");
let tb = t.tsel.as_ref().unwrap();
assert_eq!(tb.switch_group, 2);
assert_eq!(tb.attribute_list, vec![*b"cdec"]);
}
#[test]
fn build_stream_info_surfaces_tsel_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 90000;
t.tsel = Some(super::TselBox {
switch_group: 11,
attribute_list: vec![*b"bitr", *b"cdec"],
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("tsel_switch_group"), Some("11"));
assert_eq!(
info.params.options.get("tsel_attributes"),
Some("bitr cdec")
);
}
#[test]
fn build_stream_info_tsel_zero_no_attributes() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Audio;
t.codec_id_fourcc = *b"mp4a";
t.timescale = 48000;
t.tsel = Some(super::TselBox {
switch_group: 0,
attribute_list: Vec::new(),
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("tsel_switch_group"), Some("0"));
assert_eq!(info.params.options.get("tsel_attributes"), None);
}
#[test]
fn build_stream_info_no_tsel_no_tsel_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 90000;
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("tsel_switch_group"), None);
assert_eq!(info.params.options.get("tsel_attributes"), None);
}
#[test]
fn build_stream_info_tsel_attribute_non_printable_renders_as_hex() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 90000;
t.tsel = Some(super::TselBox {
switch_group: 1,
attribute_list: vec![[0x00, 0x01, 0x02, 0x03], *b"bitr"],
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(
info.params.options.get("tsel_attributes"),
Some("00010203 bitr")
);
}
#[test]
fn parse_trak_picks_up_nested_tsel() {
let mut tkhd = vec![0u8; 84];
tkhd[12..16].copy_from_slice(&13u32.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"soun");
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 tsel = Vec::new();
tsel.extend_from_slice(&[0u8; 4]);
tsel.extend_from_slice(&100i32.to_be_bytes());
tsel.extend_from_slice(b"bitr");
tsel.extend_from_slice(b"lang");
let mut udta = Vec::new();
udta.extend(wrap_box_full_size(b"tsel", &tsel));
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, 13);
let tb = t.tsel.as_ref().unwrap();
assert_eq!(tb.switch_group, 100);
assert_eq!(tb.attribute_list, vec![*b"bitr", *b"lang"]);
}
fn stri_body(switch: i16, alt: i16, id: u32, attrs: &[&[u8; 4]]) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&[0u8; 4]); b.extend_from_slice(&switch.to_be_bytes());
b.extend_from_slice(&alt.to_be_bytes());
b.extend_from_slice(&id.to_be_bytes());
for a in attrs {
b.extend_from_slice(*a);
}
b
}
fn stsg_body(grouping_type: &[u8; 4], indices: &[u32]) -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&[0u8; 4]); b.extend_from_slice(grouping_type);
b.extend_from_slice(&(indices.len() as u16).to_be_bytes());
for i in indices {
b.extend_from_slice(&i.to_be_bytes());
}
b
}
#[test]
fn parse_stri_fields_and_attributes() {
let body = stri_body(1, 1, 2, &[b"vwsc", b"nvws"]);
let st = super::parse_stri(&body).unwrap();
assert_eq!(st.switch_group, 1);
assert_eq!(st.alternate_group, 1);
assert_eq!(st.sub_track_id, 2);
assert_eq!(st.attribute_list, vec![*b"vwsc", *b"nvws"]);
assert!(st.sample_groups.is_empty());
}
#[test]
fn parse_stri_negative_groups_no_attributes() {
let body = stri_body(-3, -5, 0, &[]);
let st = super::parse_stri(&body).unwrap();
assert_eq!(st.switch_group, -3);
assert_eq!(st.alternate_group, -5);
assert_eq!(st.sub_track_id, 0);
assert!(st.attribute_list.is_empty());
}
#[test]
fn parse_stri_too_short_rejected() {
assert!(super::parse_stri(&[0u8; 11]).is_none());
}
#[test]
fn parse_stri_unknown_version_rejected() {
let mut body = stri_body(1, 1, 1, &[b"bitr"]);
body[0] = 1; assert!(super::parse_stri(&body).is_none());
}
#[test]
fn parse_stri_trailing_partial_fourcc_ignored() {
let mut body = stri_body(7, 0, 9, &[b"frar"]);
body.extend_from_slice(b"xy"); let st = super::parse_stri(&body).unwrap();
assert_eq!(st.attribute_list, vec![*b"frar"]);
}
#[test]
fn parse_stsg_grouping_and_indices() {
let body = stsg_body(b"tele", &[1, 3, 7]);
let sg = super::parse_stsg(&body).unwrap();
assert_eq!(&sg.grouping_type, b"tele");
assert_eq!(sg.group_description_indices, vec![1, 3, 7]);
}
#[test]
fn parse_stsg_empty_index_list() {
let body = stsg_body(b"roll", &[]);
let sg = super::parse_stsg(&body).unwrap();
assert_eq!(&sg.grouping_type, b"roll");
assert!(sg.group_description_indices.is_empty());
}
#[test]
fn parse_stsg_overrunning_item_count_rejected() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(b"tele");
body.extend_from_slice(&3u16.to_be_bytes()); body.extend_from_slice(&1u32.to_be_bytes()); assert!(super::parse_stsg(&body).is_none());
}
#[test]
fn parse_stsg_unknown_version_rejected() {
let mut body = stsg_body(b"tele", &[1]);
body[0] = 2;
assert!(super::parse_stsg(&body).is_none());
}
#[test]
fn parse_strk_with_stri_and_strd_stsg() {
let stri = stri_body(2, 2, 5, &[b"tesc"]);
let stsg0 = stsg_body(b"tele", &[1, 2]);
let stsg1 = stsg_body(b"roll", &[4]);
let mut strd = Vec::new();
strd.extend(wrap_box_full_size(b"stsg", &stsg0));
strd.extend(wrap_box_full_size(b"stsg", &stsg1));
let mut strk = Vec::new();
strk.extend(wrap_box_full_size(b"stri", &stri));
strk.extend(wrap_box_full_size(b"strd", &strd));
let mut t = fresh_track();
super::parse_strk(&strk, &mut t);
assert_eq!(t.sub_tracks.len(), 1);
let st = &t.sub_tracks[0];
assert_eq!(st.switch_group, 2);
assert_eq!(st.alternate_group, 2);
assert_eq!(st.sub_track_id, 5);
assert_eq!(st.attribute_list, vec![*b"tesc"]);
assert_eq!(st.sample_groups.len(), 2);
assert_eq!(&st.sample_groups[0].grouping_type, b"tele");
assert_eq!(st.sample_groups[0].group_description_indices, vec![1, 2]);
assert_eq!(&st.sample_groups[1].grouping_type, b"roll");
assert_eq!(st.sample_groups[1].group_description_indices, vec![4]);
}
#[test]
fn parse_strk_stri_only_no_strd() {
let stri = stri_body(1, 0, 3, &[]);
let mut strk = Vec::new();
strk.extend(wrap_box_full_size(b"stri", &stri));
let mut t = fresh_track();
super::parse_strk(&strk, &mut t);
assert_eq!(t.sub_tracks.len(), 1);
assert_eq!(t.sub_tracks[0].sub_track_id, 3);
assert!(t.sub_tracks[0].sample_groups.is_empty());
}
#[test]
fn parse_strk_without_stri_dropped() {
let strd = Vec::new();
let mut strk = Vec::new();
strk.extend(wrap_box_full_size(b"strd", &strd));
let mut t = fresh_track();
super::parse_strk(&strk, &mut t);
assert!(t.sub_tracks.is_empty());
}
#[test]
fn parse_track_udta_collects_multiple_strk() {
let strk_a = {
let stri = stri_body(1, 1, 1, &[b"tesc"]);
wrap_box_full_size(b"stri", &stri)
};
let strk_b = {
let stri = stri_body(2, 1, 2, &[b"spsc"]);
wrap_box_full_size(b"stri", &stri)
};
let mut udta = Vec::new();
udta.extend(wrap_box_full_size(b"strk", &strk_a));
udta.extend(wrap_box_full_size(b"strk", &strk_b));
let mut t = fresh_track();
super::parse_track_udta(&udta, &mut t);
assert_eq!(t.sub_tracks.len(), 2);
assert_eq!(t.sub_tracks[0].sub_track_id, 1);
assert_eq!(t.sub_tracks[0].attribute_list, vec![*b"tesc"]);
assert_eq!(t.sub_tracks[1].sub_track_id, 2);
assert_eq!(t.sub_tracks[1].attribute_list, vec![*b"spsc"]);
}
#[test]
fn build_stream_info_surfaces_subtrack_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 90000;
t.sub_tracks.push(super::SubTrack {
switch_group: 4,
alternate_group: 2,
sub_track_id: 7,
attribute_list: vec![*b"tesc", *b"bitr"],
sample_groups: vec![super::SubTrackSampleGroup {
grouping_type: *b"tele",
group_description_indices: vec![1, 2],
}],
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(
info.params.options.get("subtrack_0"),
Some("id=7 switch=4 alt=2 attrs=tesc bitr stsg=tele:1,2")
);
}
#[test]
fn build_stream_info_subtrack_minimal() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Audio;
t.codec_id_fourcc = *b"mp4a";
t.timescale = 48000;
t.sub_tracks.push(super::SubTrack {
switch_group: 0,
alternate_group: 0,
sub_track_id: 0,
attribute_list: Vec::new(),
sample_groups: Vec::new(),
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(
info.params.options.get("subtrack_0"),
Some("id=0 switch=0 alt=0")
);
}
#[test]
fn build_stream_info_no_strk_no_subtrack_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 90000;
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("subtrack_0"), None);
}
#[test]
fn parse_trak_picks_up_nested_strk() {
let mut tkhd = vec![0u8; 84];
tkhd[12..16].copy_from_slice(&21u32.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 stri = stri_body(5, 5, 8, &[b"spsc"]);
let stsg = stsg_body(b"tele", &[2]);
let mut strd = Vec::new();
strd.extend(wrap_box_full_size(b"stsg", &stsg));
let mut strk = Vec::new();
strk.extend(wrap_box_full_size(b"stri", &stri));
strk.extend(wrap_box_full_size(b"strd", &strd));
let mut udta = Vec::new();
udta.extend(wrap_box_full_size(b"strk", &strk));
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, 21);
assert_eq!(t.sub_tracks.len(), 1);
let st = &t.sub_tracks[0];
assert_eq!(st.switch_group, 5);
assert_eq!(st.alternate_group, 5);
assert_eq!(st.sub_track_id, 8);
assert_eq!(st.attribute_list, vec![*b"spsc"]);
assert_eq!(st.sample_groups.len(), 1);
assert_eq!(&st.sample_groups[0].grouping_type, b"tele");
assert_eq!(st.sample_groups[0].group_description_indices, vec![2]);
}
#[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 parse_vmhd_graphicsmode_and_opcolor() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 1]); body.extend_from_slice(&0u16.to_be_bytes()); body.extend_from_slice(&0x1234u16.to_be_bytes()); body.extend_from_slice(&0x5678u16.to_be_bytes()); body.extend_from_slice(&0x9abcu16.to_be_bytes()); let v = super::parse_vmhd(&body).unwrap();
assert_eq!(v.graphicsmode, 0);
assert_eq!(v.opcolor, [0x1234, 0x5678, 0x9abc]);
}
#[test]
fn parse_vmhd_nonzero_graphicsmode_preserved() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 1]);
body.extend_from_slice(&0x0100u16.to_be_bytes()); body.extend_from_slice(&[0u8; 6]); let v = super::parse_vmhd(&body).unwrap();
assert_eq!(v.graphicsmode, 0x0100);
assert_eq!(v.opcolor, [0, 0, 0]);
}
#[test]
fn parse_vmhd_tolerates_nonzero_version() {
let mut body = Vec::new();
body.extend_from_slice(&[3u8, 0, 0, 1]); body.extend_from_slice(&7u16.to_be_bytes());
body.extend_from_slice(&[0u8; 6]);
let v = super::parse_vmhd(&body).unwrap();
assert_eq!(v.graphicsmode, 7);
}
#[test]
fn parse_vmhd_too_short_is_rejected() {
let body = vec![0u8; 11];
assert!(super::parse_vmhd(&body).is_err());
}
#[test]
fn parse_minf_picks_up_vmhd() {
let mut vmhd = Vec::new();
vmhd.extend_from_slice(&[0u8, 0, 0, 1]);
vmhd.extend_from_slice(&0u16.to_be_bytes());
vmhd.extend_from_slice(&10u16.to_be_bytes());
vmhd.extend_from_slice(&20u16.to_be_bytes());
vmhd.extend_from_slice(&30u16.to_be_bytes());
let mut minf = Vec::new();
minf.extend(wrap_box_full_size(b"vmhd", &vmhd));
let mut t = fresh_track();
super::parse_minf(&minf, &mut t).unwrap();
let v = t.vmhd.expect("vmhd should be parsed");
assert_eq!(v.graphicsmode, 0);
assert_eq!(v.opcolor, [10, 20, 30]);
}
#[test]
fn parse_minf_keeps_first_vmhd_ignores_duplicate() {
let mut first = Vec::new();
first.extend_from_slice(&[0u8, 0, 0, 1]);
first.extend_from_slice(&1u16.to_be_bytes());
first.extend_from_slice(&[0u8; 6]);
let mut second = Vec::new();
second.extend_from_slice(&[0u8, 0, 0, 1]);
second.extend_from_slice(&2u16.to_be_bytes());
second.extend_from_slice(&[0u8; 6]);
let mut minf = Vec::new();
minf.extend(wrap_box_full_size(b"vmhd", &first));
minf.extend(wrap_box_full_size(b"vmhd", &second));
let mut t = fresh_track();
super::parse_minf(&minf, &mut t).unwrap();
assert_eq!(t.vmhd.unwrap().graphicsmode, 1);
}
#[test]
fn parse_minf_drops_malformed_vmhd() {
let short = vec![0u8; 5]; let mut minf = Vec::new();
minf.extend(wrap_box_full_size(b"vmhd", &short));
let mut t = fresh_track();
super::parse_minf(&minf, &mut t).unwrap();
assert!(t.vmhd.is_none());
}
#[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 build_stream_info_surfaces_vmhd_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 1000;
t.vmhd = Some(super::VmhdBox {
graphicsmode: 0,
opcolor: [10, 20, 30],
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("vmhd_graphicsmode"), Some("0"));
assert_eq!(info.params.options.get("vmhd_opcolor"), Some("10 20 30"));
}
#[test]
fn build_stream_info_no_vmhd_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("vmhd_graphicsmode"), None);
assert_eq!(info.params.options.get("vmhd_opcolor"), None);
}
fn build_dref(children: &[Vec<u8>]) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&(children.len() as u32).to_be_bytes());
for c in children {
body.extend_from_slice(c);
}
body
}
fn url_self_contained() -> Vec<u8> {
wrap_box_full_size(b"url ", &[0u8, 0, 0, 1])
}
fn url_external(loc: &str) -> Vec<u8> {
let mut payload = vec![0u8; 4]; payload.extend_from_slice(loc.as_bytes());
payload.push(0);
wrap_box_full_size(b"url ", &payload)
}
#[test]
fn parse_dref_single_self_contained_url() {
let body = build_dref(&[url_self_contained()]);
let d = super::parse_dref(&body).unwrap();
assert_eq!(d.entries.len(), 1);
assert_eq!(&d.entries[0].kind, b"url ");
assert!(d.entries[0].is_self_contained());
assert_eq!(d.entries[0].location, None);
assert_eq!(d.entries[0].name, None);
}
#[test]
fn parse_dref_external_url_location() {
let body = build_dref(&[url_external("http://example.com/a.mp4")]);
let d = super::parse_dref(&body).unwrap();
assert_eq!(d.entries.len(), 1);
assert!(!d.entries[0].is_self_contained());
assert_eq!(
d.entries[0].location.as_deref(),
Some("http://example.com/a.mp4")
);
}
#[test]
fn parse_dref_urn_name_and_location() {
let mut payload = vec![0u8; 4]; payload.extend_from_slice(b"urn:example:res");
payload.push(0);
payload.extend_from_slice(b"ftp://host/res");
payload.push(0);
let body = build_dref(&[wrap_box_full_size(b"urn ", &payload)]);
let d = super::parse_dref(&body).unwrap();
assert_eq!(d.entries.len(), 1);
assert_eq!(&d.entries[0].kind, b"urn ");
assert_eq!(d.entries[0].name.as_deref(), Some("urn:example:res"));
assert_eq!(d.entries[0].location.as_deref(), Some("ftp://host/res"));
}
#[test]
fn parse_dref_urn_name_only() {
let mut payload = vec![0u8; 4];
payload.extend_from_slice(b"urn:example:res");
payload.push(0);
let body = build_dref(&[wrap_box_full_size(b"urn ", &payload)]);
let d = super::parse_dref(&body).unwrap();
assert_eq!(d.entries[0].name.as_deref(), Some("urn:example:res"));
assert_eq!(d.entries[0].location, None);
}
#[test]
fn parse_dref_multiple_entries_order_preserved() {
let body = build_dref(&[url_self_contained(), url_external("rel/path.bin")]);
let d = super::parse_dref(&body).unwrap();
assert_eq!(d.entries.len(), 2);
assert!(d.entries[0].is_self_contained());
assert!(!d.entries[1].is_self_contained());
assert_eq!(d.entries[1].location.as_deref(), Some("rel/path.bin"));
}
#[test]
fn parse_dref_drops_unknown_child() {
let bogus = wrap_box_full_size(b"foo ", &[0u8; 4]);
let body = build_dref(&[bogus, url_self_contained()]);
let d = super::parse_dref(&body).unwrap();
assert_eq!(d.entries.len(), 1);
assert!(d.entries[0].is_self_contained());
}
#[test]
fn parse_dref_too_short_is_rejected() {
assert!(super::parse_dref(&[0u8; 7]).is_err());
}
#[test]
fn parse_dref_forged_count_does_not_overrun() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]);
body.extend_from_slice(&0xFFFF_FFFFu32.to_be_bytes()); body.extend_from_slice(&url_self_contained());
let d = super::parse_dref(&body).unwrap();
assert_eq!(d.entries.len(), 1);
}
#[test]
fn parse_minf_picks_up_dref_via_dinf() {
let dref = build_dref(&[url_external("http://h/v.mp4")]);
let dref_box = wrap_box_full_size(b"dref", &dref);
let dinf = wrap_box_full_size(b"dinf", &dref_box);
let mut t = fresh_track();
super::parse_minf(&dinf, &mut t).unwrap();
let d = t.dref.expect("dref should be parsed");
assert_eq!(d.entries.len(), 1);
assert_eq!(d.entries[0].location.as_deref(), Some("http://h/v.mp4"));
}
#[test]
fn build_stream_info_surfaces_single_self_contained_dref() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 1000;
t.dref = Some(super::DrefBox {
entries: vec![super::DataEntry {
kind: *b"url ",
flags: 1,
name: None,
location: None,
}],
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("dref_self_contained"), Some("true"));
assert_eq!(info.params.options.get("dref_count"), None);
assert_eq!(info.params.options.get("dref_1"), None);
}
#[test]
fn build_stream_info_surfaces_external_dref_entries() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 1000;
t.dref = Some(super::DrefBox {
entries: vec![
super::DataEntry {
kind: *b"url ",
flags: 1,
name: None,
location: None,
},
super::DataEntry {
kind: *b"urn ",
flags: 0,
name: Some("urn:x".to_string()),
location: Some("http://h/r".to_string()),
},
],
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(
info.params.options.get("dref_self_contained"),
Some("false")
);
assert_eq!(info.params.options.get("dref_count"), Some("2"));
assert_eq!(info.params.options.get("dref_1"), Some("url self=true"));
assert_eq!(
info.params.options.get("dref_2"),
Some("urn self=false name=urn:x loc=http://h/r")
);
}
#[test]
fn build_stream_info_no_dref_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("dref_self_contained"), None);
assert_eq!(info.params.options.get("dref_count"), 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 bit_cursor_reads_msb_first() {
let data = [0xA6u8, 0xC1];
let mut c = super::BitCursor::new(&data, 0);
assert_eq!(c.read(0), Some(0)); assert_eq!(c.read(4), Some(0b1010));
assert_eq!(c.read(4), Some(0b0110));
assert_eq!(c.read(8), Some(0b1100_0001));
assert_eq!(c.read(1), None); }
#[test]
fn bit_cursor_anchor_and_overrun() {
let data = [0x00, 0xFF, 0x0F];
let mut c = super::BitCursor::new(&data, 1); assert_eq!(c.read(12), Some(0xFF0)); assert_eq!(c.read(8), None); assert_eq!(c.read(4), Some(0x0F));
}
#[test]
fn parse_csgp_16bit_widths() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0x2A]); body.extend_from_slice(b"roll"); body.extend_from_slice(&2u32.to_be_bytes()); body.extend_from_slice(&1u16.to_be_bytes());
body.extend_from_slice(&3u16.to_be_bytes());
body.extend_from_slice(&2u16.to_be_bytes());
body.extend_from_slice(&1u16.to_be_bytes());
body.extend_from_slice(&5u16.to_be_bytes());
body.extend_from_slice(&0u16.to_be_bytes());
body.extend_from_slice(&7u16.to_be_bytes());
let cg = super::parse_csgp(&body).unwrap();
assert_eq!(&cg.grouping_type, b"roll");
assert_eq!(cg.grouping_type_parameter, None);
assert_eq!(cg.patterns.len(), 2);
assert_eq!(cg.patterns[0].sample_count, 3);
assert_eq!(cg.patterns[0].indices, vec![5]);
assert_eq!(cg.patterns[1].sample_count, 1);
assert_eq!(cg.patterns[1].indices, vec![0, 7]);
}
#[test]
fn parse_csgp_4bit_packed() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0]); body.extend_from_slice(b"sync"); body.extend_from_slice(&1u32.to_be_bytes()); body.push(0x32);
body.push(0x12);
body.push(0x40); let cg = super::parse_csgp(&body).unwrap();
assert_eq!(&cg.grouping_type, b"sync");
assert_eq!(cg.patterns.len(), 1);
assert_eq!(cg.patterns[0].sample_count, 2);
assert_eq!(cg.patterns[0].indices, vec![1, 2, 4]);
}
#[test]
fn parse_csgp_with_grouping_type_parameter() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0x6A]);
body.extend_from_slice(b"rap ");
body.extend_from_slice(&9u32.to_be_bytes()); body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&1u16.to_be_bytes()); body.extend_from_slice(&4u16.to_be_bytes()); body.extend_from_slice(&2u16.to_be_bytes()); let cg = super::parse_csgp(&body).unwrap();
assert_eq!(&cg.grouping_type, b"rap ");
assert_eq!(cg.grouping_type_parameter, Some(9));
assert_eq!(cg.patterns[0].sample_count, 4);
assert_eq!(cg.patterns[0].indices, vec![2]);
}
#[test]
fn parse_csgp_fragment_local_index_preserved() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0x2B]);
body.extend_from_slice(b"seig");
body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&1u16.to_be_bytes()); body.extend_from_slice(&1u16.to_be_bytes()); body.extend_from_slice(&0x8000_0001u32.to_be_bytes()); let cg = super::parse_csgp(&body).unwrap();
assert_eq!(cg.patterns[0].indices, vec![0x8000_0001]);
}
#[test]
fn parse_csgp_truncated_index_rejected() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0x2A]); body.extend_from_slice(b"roll");
body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&2u16.to_be_bytes()); body.extend_from_slice(&1u16.to_be_bytes()); body.extend_from_slice(&5u16.to_be_bytes()); assert!(super::parse_csgp(&body).is_err());
}
#[test]
fn parse_csgp_too_short() {
assert!(super::parse_csgp(&[0u8, 0]).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);
}
#[test]
fn parse_stdp_three_entries() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&0x0001u16.to_be_bytes());
body.extend_from_slice(&0x0102u16.to_be_bytes());
body.extend_from_slice(&0xFFFFu16.to_be_bytes());
let v = super::parse_stdp(&body).unwrap();
assert_eq!(v, vec![0x0001u16, 0x0102, 0xFFFF]);
}
#[test]
fn parse_stdp_empty_table() {
let body = vec![0u8; 4];
let v = super::parse_stdp(&body).unwrap();
assert!(v.is_empty());
}
#[test]
fn parse_stdp_too_short() {
assert!(super::parse_stdp(&[0u8, 0, 0]).is_err());
}
#[test]
fn parse_stdp_trailing_odd_byte_ignored() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&0x0001u16.to_be_bytes());
body.extend_from_slice(&0x0002u16.to_be_bytes());
body.push(0xAA); let v = super::parse_stdp(&body).unwrap();
assert_eq!(v, vec![0x0001u16, 0x0002]);
}
#[test]
fn parse_stbl_picks_up_stdp() {
let mut stdp = Vec::new();
stdp.extend_from_slice(&[0u8; 4]); stdp.extend_from_slice(&0x0001u16.to_be_bytes());
stdp.extend_from_slice(&0x0005u16.to_be_bytes());
let mut stbl = Vec::new();
stbl.extend(wrap_box_full_size(b"stdp", &stdp));
let mut t = fresh_track();
super::parse_stbl(&stbl, &mut t).unwrap();
assert_eq!(t.stdp, vec![0x0001u16, 0x0005]);
}
#[test]
fn build_stream_info_surfaces_stdp_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.stdp = vec![1u16, 5, 7, 3];
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("stdp_count"), Some("4"));
assert_eq!(info.params.options.get("stdp_min"), Some("1"));
assert_eq!(info.params.options.get("stdp_max"), Some("7"));
assert_eq!(info.params.options.get("stdp_sum"), Some("16"));
}
#[test]
fn build_stream_info_stdp_single_entry_min_eq_max() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 1000;
t.stdp = vec![42u16];
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("stdp_count"), Some("1"));
assert_eq!(info.params.options.get("stdp_min"), Some("42"));
assert_eq!(info.params.options.get("stdp_max"), Some("42"));
assert_eq!(info.params.options.get("stdp_sum"), Some("42"));
}
#[test]
fn build_stream_info_no_stdp_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("stdp_count"), None);
assert_eq!(info.params.options.get("stdp_min"), None);
assert_eq!(info.params.options.get("stdp_max"), None);
assert_eq!(info.params.options.get("stdp_sum"), None);
}
fn pack_padb(pad1: u8, pad2: u8) -> u8 {
((pad1 & 0x07) << 4) | (pad2 & 0x07)
}
#[test]
fn parse_padb_four_samples_two_bytes() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&4u32.to_be_bytes()); body.push(pack_padb(1, 2)); body.push(pack_padb(3, 7)); let v = super::parse_padb(&body).unwrap();
assert_eq!(v, vec![1u8, 2, 3, 7]);
}
#[test]
fn parse_padb_odd_sample_count_drops_trailing_nibble() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&3u32.to_be_bytes()); body.push(pack_padb(2, 4)); body.push(pack_padb(6, 5)); let v = super::parse_padb(&body).unwrap();
assert_eq!(v, vec![2u8, 4, 6]);
}
#[test]
fn parse_padb_zero_samples() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&0u32.to_be_bytes()); let v = super::parse_padb(&body).unwrap();
assert!(v.is_empty());
}
#[test]
fn parse_padb_too_short_preamble() {
assert!(super::parse_padb(&[0u8; 4]).is_err());
assert!(super::parse_padb(&[0u8; 7]).is_err());
}
#[test]
fn parse_padb_truncated_payload() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&10u32.to_be_bytes()); body.push(0); body.push(0);
assert!(super::parse_padb(&body).is_err());
}
#[test]
fn parse_padb_masks_reserved_bit() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]);
body.extend_from_slice(&2u32.to_be_bytes()); body.push(0xDA);
let v = super::parse_padb(&body).unwrap();
assert_eq!(v, vec![5u8, 2]);
}
#[test]
fn parse_stbl_picks_up_padb() {
let mut padb = Vec::new();
padb.extend_from_slice(&[0u8; 4]); padb.extend_from_slice(&2u32.to_be_bytes()); padb.push(pack_padb(4, 1));
let mut stbl = Vec::new();
stbl.extend(wrap_box_full_size(b"padb", &padb));
let mut t = fresh_track();
super::parse_stbl(&stbl, &mut t).unwrap();
assert_eq!(t.padb, vec![4u8, 1]);
}
#[test]
fn build_stream_info_surfaces_padb_summary_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Audio;
t.codec_id_fourcc = *b"mp4a";
t.timescale = 48_000;
t.padb = vec![0u8, 0, 1, 2, 2, 7, 7, 7];
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("padb_count"), Some("8"));
assert_eq!(info.params.options.get("padb_max"), Some("7"));
assert_eq!(info.params.options.get("padb_nonzero_count"), Some("6"));
assert_eq!(
info.params.options.get("padb_hist"),
Some("2:1:2:0:0:0:0:3")
);
}
#[test]
fn build_stream_info_padb_all_zero_emits_keys() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Audio;
t.codec_id_fourcc = *b"mp4a";
t.timescale = 48_000;
t.padb = vec![0u8; 5];
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("padb_count"), Some("5"));
assert_eq!(info.params.options.get("padb_max"), Some("0"));
assert_eq!(info.params.options.get("padb_nonzero_count"), Some("0"));
assert_eq!(
info.params.options.get("padb_hist"),
Some("5:0:0:0:0:0:0:0")
);
}
#[test]
fn build_stream_info_no_padb_no_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Audio;
t.codec_id_fourcc = *b"mp4a";
t.timescale = 48_000;
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("padb_count"), None);
assert_eq!(info.params.options.get("padb_max"), None);
assert_eq!(info.params.options.get("padb_nonzero_count"), None);
assert_eq!(info.params.options.get("padb_hist"), None);
}
fn build_pdin(entries: &[(u32, u32)]) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); for (rate, delay) in entries {
body.extend_from_slice(&rate.to_be_bytes());
body.extend_from_slice(&delay.to_be_bytes());
}
body
}
#[test]
fn parse_pdin_two_entries_preserves_order() {
let body = build_pdin(&[(125_000, 2_500), (250_000, 1_200)]);
let r = super::parse_pdin(&body).unwrap();
assert_eq!(r.entries.len(), 2);
assert_eq!(
r.entries[0],
super::PdinEntry {
rate: 125_000,
initial_delay: 2_500,
}
);
assert_eq!(
r.entries[1],
super::PdinEntry {
rate: 250_000,
initial_delay: 1_200,
}
);
}
#[test]
fn parse_pdin_empty_body_yields_empty_entries() {
let body = build_pdin(&[]);
let r = super::parse_pdin(&body).unwrap();
assert!(r.entries.is_empty());
}
#[test]
fn parse_pdin_too_short_preamble() {
for n in 0..4 {
let body = vec![0u8; n];
assert!(super::parse_pdin(&body).is_err(), "len {n} must err");
}
}
#[test]
fn parse_pdin_unaligned_payload_is_rejected() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]);
body.extend_from_slice(&[0u8; 12]);
assert!(super::parse_pdin(&body).is_err());
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]);
body.extend_from_slice(&[0u8; 9]);
assert!(super::parse_pdin(&body).is_err());
}
#[test]
fn parse_pdin_tolerates_nonzero_version() {
let mut body = Vec::new();
body.push(0xAA); body.extend_from_slice(&[0u8; 3]); body.extend_from_slice(&1u32.to_be_bytes());
body.extend_from_slice(&2u32.to_be_bytes());
let r = super::parse_pdin(&body).unwrap();
assert_eq!(
r.entries,
vec![super::PdinEntry {
rate: 1,
initial_delay: 2,
}]
);
}
#[test]
fn parse_pdin_big_endian_byte_order() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]);
body.extend_from_slice(&[0x05, 0x06, 0x07, 0x08]);
let r = super::parse_pdin(&body).unwrap();
assert_eq!(r.entries.len(), 1);
assert_eq!(r.entries[0].rate, 0x01_02_03_04);
assert_eq!(r.entries[0].initial_delay, 0x05_06_07_08);
}
#[test]
fn parse_pdin_box_public_entry_matches_internal() {
let body = build_pdin(&[(64_000, 5_000), (128_000, 2_400), (256_000, 1_000)]);
let r_public = super::parse_pdin_box(&body).unwrap();
let r_internal = super::parse_pdin(&body).unwrap();
assert_eq!(r_public.entries, r_internal.entries);
assert_eq!(r_public.entries.len(), 3);
}
#[derive(Clone, Copy, Debug)]
struct LevaBuild {
track_id: u32,
padding_flag: bool,
assignment_type: u8,
grouping_type: u32,
grouping_type_parameter: u32,
sub_track_id: u32,
}
fn build_leva(entries: &[LevaBuild]) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.push(entries.len() as u8); for e in entries {
body.extend_from_slice(&e.track_id.to_be_bytes());
let packed = (if e.padding_flag { 0x80 } else { 0 }) | (e.assignment_type & 0x7f);
body.push(packed);
match e.assignment_type {
0 => body.extend_from_slice(&e.grouping_type.to_be_bytes()),
1 => {
body.extend_from_slice(&e.grouping_type.to_be_bytes());
body.extend_from_slice(&e.grouping_type_parameter.to_be_bytes());
}
2 | 3 => {}
4 => body.extend_from_slice(&e.sub_track_id.to_be_bytes()),
_ => {}
}
}
body
}
#[test]
fn parse_leva_mixed_assignment_types_preserved() {
let entries = vec![
LevaBuild {
track_id: 1,
padding_flag: false,
assignment_type: 0,
grouping_type: u32::from_be_bytes(*b"roll"),
grouping_type_parameter: 0,
sub_track_id: 0,
},
LevaBuild {
track_id: 7,
padding_flag: true,
assignment_type: 4,
grouping_type: 0,
grouping_type_parameter: 0,
sub_track_id: 0xAABB_CCDD,
},
];
let body = build_leva(&entries);
let r = super::parse_leva(&body).unwrap();
assert_eq!(r.entries.len(), 2);
assert_eq!(r.entries[0].track_id, 1);
assert!(!r.entries[0].padding_flag);
assert_eq!(r.entries[0].assignment_type, 0);
assert_eq!(r.entries[0].grouping_type, u32::from_be_bytes(*b"roll"));
assert_eq!(r.entries[0].grouping_type_parameter, 0);
assert_eq!(r.entries[0].sub_track_id, 0);
assert_eq!(r.entries[1].track_id, 7);
assert!(r.entries[1].padding_flag);
assert_eq!(r.entries[1].assignment_type, 4);
assert_eq!(r.entries[1].sub_track_id, 0xAABB_CCDD);
}
#[test]
fn parse_leva_assignment_type_one_carries_both_grouping_fields() {
let entries = vec![LevaBuild {
track_id: 42,
padding_flag: false,
assignment_type: 1,
grouping_type: u32::from_be_bytes(*b"sync"),
grouping_type_parameter: 0xDEAD_BEEF,
sub_track_id: 0,
}];
let body = build_leva(&entries);
let r = super::parse_leva(&body).unwrap();
assert_eq!(r.entries.len(), 1);
assert_eq!(r.entries[0].assignment_type, 1);
assert_eq!(r.entries[0].grouping_type, u32::from_be_bytes(*b"sync"));
assert_eq!(r.entries[0].grouping_type_parameter, 0xDEAD_BEEF);
}
#[test]
fn parse_leva_assignment_types_two_three_have_no_tail() {
let entries = vec![
LevaBuild {
track_id: 11,
padding_flag: false,
assignment_type: 2,
grouping_type: 0,
grouping_type_parameter: 0,
sub_track_id: 0,
},
LevaBuild {
track_id: 12,
padding_flag: true,
assignment_type: 3,
grouping_type: 0,
grouping_type_parameter: 0,
sub_track_id: 0,
},
];
let body = build_leva(&entries);
assert_eq!(body.len(), 4 + 1 + 2 * 5);
let r = super::parse_leva(&body).unwrap();
assert_eq!(r.entries.len(), 2);
assert_eq!(r.entries[0].assignment_type, 2);
assert_eq!(r.entries[0].track_id, 11);
assert_eq!(r.entries[1].assignment_type, 3);
assert_eq!(r.entries[1].track_id, 12);
assert!(r.entries[1].padding_flag);
}
#[test]
fn parse_leva_zero_level_count_yields_empty_entries() {
let body = build_leva(&[]);
let r = super::parse_leva(&body).unwrap();
assert!(r.entries.is_empty());
}
#[test]
fn parse_leva_too_short_preamble() {
for n in 0..4 {
let body = vec![0u8; n];
assert!(super::parse_leva(&body).is_err(), "len {n} must err");
}
}
#[test]
fn parse_leva_missing_level_count_byte() {
let body = vec![0u8; 4];
assert!(super::parse_leva(&body).is_err());
}
#[test]
fn parse_leva_truncated_entry_header_is_rejected() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.push(1); body.extend_from_slice(&[0u8; 4]);
assert!(super::parse_leva(&body).is_err());
}
#[test]
fn parse_leva_truncated_grouping_type_for_at0_is_rejected() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.push(1); body.extend_from_slice(&5u32.to_be_bytes()); body.push(0); body.extend_from_slice(&[0u8; 3]);
assert!(super::parse_leva(&body).is_err());
}
#[test]
fn parse_leva_truncated_grouping_parameter_for_at1_is_rejected() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.push(1); body.extend_from_slice(&5u32.to_be_bytes()); body.push(1); body.extend_from_slice(&7u32.to_be_bytes()); assert!(super::parse_leva(&body).is_err());
}
#[test]
fn parse_leva_truncated_sub_track_id_for_at4_is_rejected() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.push(1); body.extend_from_slice(&5u32.to_be_bytes()); body.push(4); body.extend_from_slice(&[0u8; 2]); assert!(super::parse_leva(&body).is_err());
}
#[test]
fn parse_leva_reserved_assignment_type_consumes_no_tail() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.push(2); body.extend_from_slice(&9u32.to_be_bytes());
body.push(0x80 | 5);
body.extend_from_slice(&10u32.to_be_bytes());
body.push(2);
let r = super::parse_leva(&body).unwrap();
assert_eq!(r.entries.len(), 2);
assert_eq!(r.entries[0].track_id, 9);
assert!(r.entries[0].padding_flag);
assert_eq!(r.entries[0].assignment_type, 5);
assert_eq!(r.entries[0].grouping_type, 0);
assert_eq!(r.entries[0].grouping_type_parameter, 0);
assert_eq!(r.entries[0].sub_track_id, 0);
assert_eq!(r.entries[1].track_id, 10);
assert_eq!(r.entries[1].assignment_type, 2);
}
#[test]
fn parse_leva_tolerates_nonzero_version() {
let mut body = Vec::new();
body.push(0xCC); body.extend_from_slice(&[0u8; 3]); body.push(1); body.extend_from_slice(&42u32.to_be_bytes()); body.push(0x80 | 3); let r = super::parse_leva(&body).unwrap();
assert_eq!(r.entries.len(), 1);
assert_eq!(r.entries[0].track_id, 42);
assert!(r.entries[0].padding_flag);
assert_eq!(r.entries[0].assignment_type, 3);
}
#[test]
fn parse_leva_big_endian_byte_order() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.push(1); body.extend_from_slice(&[0x11, 0x22, 0x33, 0x44]); body.push(0); body.extend_from_slice(&[0xAB, 0xCD, 0xEF, 0x01]); let r = super::parse_leva(&body).unwrap();
assert_eq!(r.entries.len(), 1);
assert_eq!(r.entries[0].track_id, 0x1122_3344);
assert_eq!(r.entries[0].grouping_type, 0xABCD_EF01);
}
#[test]
fn parse_leva_packed_byte_splits_padding_and_assignment_type() {
for padding in [false, true] {
for at in 0u8..=4 {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]);
body.push(1);
body.extend_from_slice(&3u32.to_be_bytes());
body.push((if padding { 0x80 } else { 0 }) | at);
match at {
0 => body.extend_from_slice(&0u32.to_be_bytes()),
1 => body.extend_from_slice(&[0u8; 8]),
2 | 3 => {}
4 => body.extend_from_slice(&0u32.to_be_bytes()),
_ => {}
}
let r = super::parse_leva(&body).unwrap();
assert_eq!(r.entries.len(), 1);
assert_eq!(r.entries[0].padding_flag, padding);
assert_eq!(r.entries[0].assignment_type, at);
}
}
}
#[test]
fn parse_leva_box_public_entry_matches_internal() {
let entries = vec![
LevaBuild {
track_id: 1,
padding_flag: true,
assignment_type: 0,
grouping_type: u32::from_be_bytes(*b"rap "),
grouping_type_parameter: 0,
sub_track_id: 0,
},
LevaBuild {
track_id: 2,
padding_flag: false,
assignment_type: 1,
grouping_type: u32::from_be_bytes(*b"alst"),
grouping_type_parameter: 0x1234_5678,
sub_track_id: 0,
},
LevaBuild {
track_id: 3,
padding_flag: false,
assignment_type: 4,
grouping_type: 0,
grouping_type_parameter: 0,
sub_track_id: 99,
},
];
let body = build_leva(&entries);
let r_public = super::parse_leva_box(&body).unwrap();
let r_internal = super::parse_leva(&body).unwrap();
assert_eq!(r_public.entries, r_internal.entries);
assert_eq!(r_public.entries.len(), 3);
}
fn build_child_box(fourcc: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let size = 8 + payload.len();
let mut v = Vec::with_capacity(size);
v.extend_from_slice(&(size as u32).to_be_bytes());
v.extend_from_slice(fourcc);
v.extend_from_slice(payload);
v
}
fn build_trep(track_id: u32, children: &[Vec<u8>]) -> Vec<u8> {
let mut v = Vec::new();
v.extend_from_slice(&[0u8; 4]); v.extend_from_slice(&track_id.to_be_bytes());
for c in children {
v.extend_from_slice(c);
}
v
}
#[test]
fn parse_trep_track_id_only() {
let body = build_trep(0x0102_0304, &[]);
assert_eq!(body.len(), 8);
let r = super::parse_trep(&body).unwrap();
assert_eq!(r.track_id, 0x0102_0304);
assert!(r.children.is_empty());
}
#[test]
fn parse_trep_records_children_in_order() {
let assp = build_child_box(b"assp", &[0xaa; 12]);
let unkn = build_child_box(b"xyzw", &[]);
let body = build_trep(7, &[assp, unkn]);
let r = super::parse_trep(&body).unwrap();
assert_eq!(r.track_id, 7);
assert_eq!(r.children.len(), 2);
assert_eq!(&r.children[0].fourcc, b"assp");
assert_eq!(r.children[0].payload_len, 12);
assert_eq!(&r.children[1].fourcc, b"xyzw");
assert_eq!(r.children[1].payload_len, 0);
}
#[test]
fn parse_trep_too_short_preamble() {
for n in 0..4 {
let body = vec![0u8; n];
assert!(super::parse_trep(&body).is_err(), "len {n} must err");
}
}
#[test]
fn parse_trep_truncated_track_id() {
for extra in 0..4 {
let mut body = vec![0u8; 4]; body.extend_from_slice(&vec![0u8; extra]);
assert!(
super::parse_trep(&body).is_err(),
"track_id with {extra} bytes must err",
);
}
}
#[test]
fn parse_trep_tolerates_nonzero_version() {
let mut body = build_trep(42, &[build_child_box(b"assp", &[1, 2, 3, 4])]);
body[0] = 0x01; let r = super::parse_trep(&body).unwrap();
assert_eq!(r.track_id, 42);
assert_eq!(r.children.len(), 1);
assert_eq!(r.children[0].payload_len, 4);
}
#[test]
fn parse_trep_clamps_overlong_child() {
let mut body = build_trep(3, &[]);
body.extend_from_slice(&64u32.to_be_bytes());
body.extend_from_slice(b"assp");
body.extend_from_slice(&[0xff; 4]);
let r = super::parse_trep(&body).unwrap();
assert_eq!(r.children.len(), 1);
assert_eq!(&r.children[0].fourcc, b"assp");
assert_eq!(r.children[0].payload_len, 4);
}
#[test]
fn parse_trep_trailing_partial_child_header_ignored() {
let mut body = build_trep(9, &[build_child_box(b"assp", &[0; 4])]);
body.extend_from_slice(&[0x11; 5]);
let r = super::parse_trep(&body).unwrap();
assert_eq!(r.track_id, 9);
assert_eq!(r.children.len(), 1);
assert_eq!(&r.children[0].fourcc, b"assp");
}
#[test]
fn parse_trep_child_largesize_form() {
let mut body = build_trep(5, &[]);
let payload = [0x55u8; 6];
let total: u64 = (16 + payload.len()) as u64;
body.extend_from_slice(&1u32.to_be_bytes());
body.extend_from_slice(b"assp");
body.extend_from_slice(&total.to_be_bytes());
body.extend_from_slice(&payload);
let r = super::parse_trep(&body).unwrap();
assert_eq!(r.children.len(), 1);
assert_eq!(&r.children[0].fourcc, b"assp");
assert_eq!(r.children[0].payload_len, 6);
}
#[test]
fn parse_trep_box_public_entry_matches_internal() {
let body = build_trep(123, &[build_child_box(b"assp", &[7; 8])]);
let r_public = super::parse_trep_box(&body).unwrap();
let r_internal = super::parse_trep(&body).unwrap();
assert_eq!(r_public.track_id, r_internal.track_id);
assert_eq!(r_public.children, r_internal.children);
assert_eq!(r_public.track_id, 123);
}
#[test]
fn ssix_public_entries_round_trip() {
let record = super::SsixRecord {
subsegments: vec![
super::SsixSubsegment {
ranges: vec![
super::SsixRange {
level: 0,
range_size: 1_024,
},
super::SsixRange {
level: 1,
range_size: 0xFF_FFFF,
},
],
},
super::SsixSubsegment {
ranges: vec![
super::SsixRange {
level: 0,
range_size: 512,
},
super::SsixRange {
level: 2,
range_size: 7,
},
],
},
],
};
let boxed = super::build_ssix_box(&record).unwrap();
assert_eq!(&boxed[4..8], b"ssix");
assert_eq!(
boxed.len(),
u32::from_be_bytes(boxed[..4].try_into().unwrap()) as usize
);
let body = &boxed[8..];
let r_public = super::parse_ssix_box(body).unwrap();
let r_internal = super::parse_ssix(body).unwrap();
assert_eq!(r_public, r_internal);
assert_eq!(r_public, record);
}
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);
}
#[test]
fn parse_saiz_constant_size_no_aux_type_key() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.push(16u8); body.extend_from_slice(&5u32.to_be_bytes()); let sz = super::parse_saiz(&body).expect("saiz parses");
assert_eq!(sz.aux_info_type, None);
assert_eq!(sz.aux_info_type_parameter, None);
assert_eq!(sz.default_sample_info_size, 16);
assert_eq!(sz.sample_count, 5);
assert!(sz.per_sample.is_empty());
}
#[test]
fn parse_saiz_variable_size_with_aux_type_key() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 1]); body.extend_from_slice(b"cenc"); body.extend_from_slice(&0u32.to_be_bytes()); body.push(0u8); body.extend_from_slice(&3u32.to_be_bytes()); body.extend_from_slice(&[12u8, 24u8, 36u8]); let sz = super::parse_saiz(&body).expect("saiz parses");
assert_eq!(sz.aux_info_type, Some(*b"cenc"));
assert_eq!(sz.aux_info_type_parameter, Some(0));
assert_eq!(sz.default_sample_info_size, 0);
assert_eq!(sz.sample_count, 3);
assert_eq!(sz.per_sample, vec![12, 24, 36]);
}
#[test]
fn parse_saiz_truncated_per_sample_returns_err() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.push(0u8); body.extend_from_slice(&100u32.to_be_bytes()); body.extend_from_slice(&[1u8, 2u8, 3u8]); let err = super::parse_saiz(&body).expect_err("truncation must err");
assert!(format!("{err}").contains("MP4: saiz"));
}
#[test]
fn parse_saio_v0_single_offset_no_aux_type() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&1u32.to_be_bytes()); body.extend_from_slice(&0xdead_beefu32.to_be_bytes()); let so = super::parse_saio(&body).expect("saio v0 parses");
assert_eq!(so.version, 0);
assert_eq!(so.aux_info_type, None);
assert_eq!(so.aux_info_type_parameter, None);
assert_eq!(so.offsets, vec![0xdead_beef]);
}
#[test]
fn parse_saio_v1_two_64bit_offsets_with_aux_type() {
let mut body = Vec::new();
body.extend_from_slice(&[1u8, 0, 0, 1]); body.extend_from_slice(b"cenc");
body.extend_from_slice(&7u32.to_be_bytes());
body.extend_from_slice(&2u32.to_be_bytes()); body.extend_from_slice(&0x0000_0001_0000_0000u64.to_be_bytes());
body.extend_from_slice(&0xffff_ffff_ffff_0000u64.to_be_bytes());
let so = super::parse_saio(&body).expect("saio v1 parses");
assert_eq!(so.version, 1);
assert_eq!(so.aux_info_type, Some(*b"cenc"));
assert_eq!(so.aux_info_type_parameter, Some(7));
assert_eq!(
so.offsets,
vec![0x0000_0001_0000_0000, 0xffff_ffff_ffff_0000]
);
}
#[test]
fn parse_saio_truncated_offsets_returns_err() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&100u32.to_be_bytes()); body.extend_from_slice(&1u32.to_be_bytes()); let err = super::parse_saio(&body).expect_err("truncation must err");
assert!(format!("{err}").contains("MP4: saio"));
}
#[test]
fn parse_stbl_collects_saiz_and_saio() {
let mut saiz_body = Vec::new();
saiz_body.extend_from_slice(&[0u8; 4]);
saiz_body.push(8u8);
saiz_body.extend_from_slice(&4u32.to_be_bytes());
let mut saio_body = Vec::new();
saio_body.extend_from_slice(&[0u8; 4]);
saio_body.extend_from_slice(&1u32.to_be_bytes());
saio_body.extend_from_slice(&0x1000u32.to_be_bytes());
let mut stbl = Vec::new();
stbl.extend(wrap_box_full_size(b"saiz", &saiz_body));
stbl.extend(wrap_box_full_size(b"saio", &saio_body));
let mut t = fresh_track();
super::parse_stbl(&stbl, &mut t).unwrap();
assert_eq!(t.saiz.len(), 1);
assert_eq!(t.saiz[0].default_sample_info_size, 8);
assert_eq!(t.saiz[0].sample_count, 4);
assert_eq!(t.saio.len(), 1);
assert_eq!(t.saio[0].version, 0);
assert_eq!(t.saio[0].offsets, vec![0x1000]);
}
#[test]
fn build_stream_info_surfaces_saiz_and_saio_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"encv";
t.timescale = 1000;
t.saiz.push(super::SaizBox {
aux_info_type: Some(*b"cenc"),
aux_info_type_parameter: Some(0),
default_sample_info_size: 0,
sample_count: 3,
per_sample: vec![18, 18, 26],
});
t.saio.push(super::SaioBox {
version: 1,
aux_info_type: Some(*b"cenc"),
aux_info_type_parameter: Some(0),
offsets: vec![0x1_0000, 0x2_0000],
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(
info.params.options.get("saiz_0").unwrap(),
"type=cenc param=0 default_size=0 count=3 sizes=18,18,26"
);
assert_eq!(
info.params.options.get("saio_0").unwrap(),
"v1 type=cenc param=0 offsets=65536,131072"
);
}
#[test]
fn build_stream_info_no_saiz_saio_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("saiz_0"), None);
assert_eq!(info.params.options.get("saio_0"), None);
}
#[test]
fn build_stream_info_surfaces_saiz_constant_no_key() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Audio;
t.codec_id_fourcc = *b"mp4a";
t.timescale = 48_000;
t.saiz.push(super::SaizBox {
aux_info_type: None,
aux_info_type_parameter: None,
default_sample_info_size: 22,
sample_count: 100,
per_sample: Vec::new(),
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(
info.params.options.get("saiz_0").unwrap(),
"default_size=22 count=100"
);
}
#[test]
fn parse_mehd_v0_reads_32_bit_fragment_duration() {
let mut body = vec![0u8, 0, 0, 0];
body.extend_from_slice(&0xDEAD_BEEFu32.to_be_bytes());
let d = super::parse_mehd(&body).expect("v0 mehd parses");
assert_eq!(d, 0xDEAD_BEEF);
}
#[test]
fn parse_mehd_v1_reads_64_bit_fragment_duration() {
let mut body = vec![1u8, 0, 0, 0];
let dur: u64 = 0x0123_4567_89AB_CDEF;
body.extend_from_slice(&dur.to_be_bytes());
let d = super::parse_mehd(&body).expect("v1 mehd parses");
assert_eq!(d, dur);
}
#[test]
fn parse_mehd_rejects_empty_body() {
assert!(super::parse_mehd(&[]).is_err());
}
#[test]
fn parse_mehd_rejects_truncated_v0() {
let body = vec![0u8, 0, 0, 0, 0x12, 0x34];
assert!(super::parse_mehd(&body).is_err());
}
#[test]
fn parse_mehd_rejects_truncated_v1() {
let body = vec![1u8, 0, 0, 0, 0, 0, 0, 0];
assert!(super::parse_mehd(&body).is_err());
}
#[test]
fn parse_mehd_rejects_unknown_version() {
let mut body = vec![2u8, 0, 0, 0];
body.extend_from_slice(&0u64.to_be_bytes());
assert!(super::parse_mehd(&body).is_err());
}
fn build_moof_body_with_pssh(seq: u32, psshes: &[Vec<u8>]) -> Vec<u8> {
let mut mfhd_body = vec![0u8; 4];
mfhd_body.extend_from_slice(&seq.to_be_bytes());
let mfhd = wrap_box(b"mfhd", &mfhd_body);
let mut body = Vec::new();
body.extend_from_slice(&mfhd);
for pssh_body in psshes {
body.extend_from_slice(&wrap_box(b"pssh", pssh_body));
}
body
}
fn wrap_box(fourcc: &[u8; 4], body: &[u8]) -> Vec<u8> {
let total = (8 + body.len()) as u32;
let mut out = Vec::with_capacity(8 + body.len());
out.extend_from_slice(&total.to_be_bytes());
out.extend_from_slice(fourcc);
out.extend_from_slice(body);
out
}
fn pssh_v0_body(system_id: &[u8; 16], data: &[u8]) -> Vec<u8> {
let mut body = vec![0u8; 4]; body.extend_from_slice(system_id);
body.extend_from_slice(&(data.len() as u32).to_be_bytes());
body.extend_from_slice(data);
body
}
fn pssh_v1_body(system_id: &[u8; 16], kids: &[[u8; 16]], data: &[u8]) -> Vec<u8> {
let mut body = vec![1u8, 0, 0, 0]; body.extend_from_slice(system_id);
body.extend_from_slice(&(kids.len() as u32).to_be_bytes());
for k in kids {
body.extend_from_slice(k);
}
body.extend_from_slice(&(data.len() as u32).to_be_bytes());
body.extend_from_slice(data);
body
}
#[test]
fn parse_moof_collects_one_pssh_keyed_by_mfhd_sequence() {
let sysid: [u8; 16] = [
0xed, 0xef, 0x8b, 0xa9, 0x79, 0xd6, 0x4a, 0xce, 0xa3, 0xc8, 0x27, 0xdc, 0xd5, 0x1d,
0x21, 0xed,
];
let data = b"opaque-drm-blob".to_vec();
let body = build_moof_body_with_pssh(7, &[pssh_v0_body(&sysid, &data)]);
let moof = super::MoofRecord {
moof_start: 0,
body,
};
let mut samples: Vec<super::SampleRef> = Vec::new();
let mut next_dts: Vec<i64> = Vec::new();
let mut senc: Vec<super::SencRecord> = Vec::new();
let mut sai: Vec<super::SaiRecord> = Vec::new();
let mut moof_psshes: Vec<super::MoofPsshRecord> = Vec::new();
super::parse_moof(
&moof,
&[],
&mut samples,
&mut next_dts,
&mut senc,
&mut sai,
&mut moof_psshes,
)
.expect("moof walk succeeds");
assert_eq!(moof_psshes.len(), 1);
let r = &moof_psshes[0];
assert_eq!(r.moof_sequence, 7);
assert_eq!(r.pssh.version, 0);
assert_eq!(r.pssh.system_id, sysid);
assert!(r.pssh.kids.is_empty());
assert_eq!(r.pssh.data, data);
}
#[test]
fn parse_moof_collects_two_psshes_v0_and_v1_in_walk_order() {
let sysid_a: [u8; 16] = [0x11; 16];
let sysid_b: [u8; 16] = [0x22; 16];
let kid_x: [u8; 16] = [0xAA; 16];
let kid_y: [u8; 16] = [0xBB; 16];
let data_a = b"sys-a-data".to_vec();
let data_b = b"sys-b-data-longer".to_vec();
let body = build_moof_body_with_pssh(
42,
&[
pssh_v0_body(&sysid_a, &data_a),
pssh_v1_body(&sysid_b, &[kid_x, kid_y], &data_b),
],
);
let moof = super::MoofRecord {
moof_start: 0,
body,
};
let mut samples: Vec<super::SampleRef> = Vec::new();
let mut next_dts: Vec<i64> = Vec::new();
let mut senc: Vec<super::SencRecord> = Vec::new();
let mut sai: Vec<super::SaiRecord> = Vec::new();
let mut moof_psshes: Vec<super::MoofPsshRecord> = Vec::new();
super::parse_moof(
&moof,
&[],
&mut samples,
&mut next_dts,
&mut senc,
&mut sai,
&mut moof_psshes,
)
.expect("moof walk succeeds");
assert_eq!(moof_psshes.len(), 2);
assert_eq!(moof_psshes[0].moof_sequence, 42);
assert_eq!(moof_psshes[1].moof_sequence, 42);
assert_eq!(moof_psshes[0].pssh.version, 0);
assert_eq!(moof_psshes[0].pssh.system_id, sysid_a);
assert!(moof_psshes[0].pssh.kids.is_empty());
assert_eq!(moof_psshes[0].pssh.data, data_a);
assert_eq!(moof_psshes[1].pssh.version, 1);
assert_eq!(moof_psshes[1].pssh.system_id, sysid_b);
assert_eq!(moof_psshes[1].pssh.kids, vec![kid_x, kid_y]);
assert_eq!(moof_psshes[1].pssh.data, data_b);
}
#[test]
fn parse_moof_pssh_before_mfhd_still_binds_to_sequence() {
let sysid: [u8; 16] = [0xCC; 16];
let data = b"pre-mfhd-pssh".to_vec();
let mut mfhd_body = vec![0u8; 4];
mfhd_body.extend_from_slice(&99u32.to_be_bytes());
let mfhd = wrap_box(b"mfhd", &mfhd_body);
let pssh = wrap_box(b"pssh", &pssh_v0_body(&sysid, &data));
let mut body = Vec::new();
body.extend_from_slice(&pssh);
body.extend_from_slice(&mfhd);
let moof = super::MoofRecord {
moof_start: 0,
body,
};
let mut samples: Vec<super::SampleRef> = Vec::new();
let mut next_dts: Vec<i64> = Vec::new();
let mut senc: Vec<super::SencRecord> = Vec::new();
let mut sai: Vec<super::SaiRecord> = Vec::new();
let mut moof_psshes: Vec<super::MoofPsshRecord> = Vec::new();
super::parse_moof(
&moof,
&[],
&mut samples,
&mut next_dts,
&mut senc,
&mut sai,
&mut moof_psshes,
)
.expect("moof walk succeeds");
assert_eq!(moof_psshes.len(), 1);
assert_eq!(moof_psshes[0].moof_sequence, 99);
assert_eq!(moof_psshes[0].pssh.system_id, sysid);
assert_eq!(moof_psshes[0].pssh.data, data);
}
#[test]
fn parse_moof_drops_malformed_pssh_silently() {
let bad = vec![0u8; 4];
let body = build_moof_body_with_pssh(3, &[bad]);
let moof = super::MoofRecord {
moof_start: 0,
body,
};
let mut samples: Vec<super::SampleRef> = Vec::new();
let mut next_dts: Vec<i64> = Vec::new();
let mut senc: Vec<super::SencRecord> = Vec::new();
let mut sai: Vec<super::SaiRecord> = Vec::new();
let mut moof_psshes: Vec<super::MoofPsshRecord> = Vec::new();
super::parse_moof(
&moof,
&[],
&mut samples,
&mut next_dts,
&mut senc,
&mut sai,
&mut moof_psshes,
)
.expect("walk succeeds despite bad pssh");
assert!(moof_psshes.is_empty(), "malformed pssh dropped silently");
}
#[test]
fn sample_flags_all_zero_decodes_to_sync_sample() {
let f = super::SampleFlags::from_u32(0);
assert_eq!(f.is_leading, 0);
assert_eq!(f.sample_depends_on, 0);
assert_eq!(f.sample_is_depended_on, 0);
assert_eq!(f.sample_has_redundancy, 0);
assert_eq!(f.sample_padding_value, 0);
assert!(!f.sample_is_non_sync_sample);
assert!(f.is_sync_sample());
assert_eq!(f.sample_degradation_priority, 0);
}
#[test]
fn sample_flags_non_sync_bit_only() {
let f = super::SampleFlags::from_u32(0x0001_0000);
assert!(f.sample_is_non_sync_sample);
assert!(!f.is_sync_sample());
assert_eq!(f.is_leading, 0);
assert_eq!(f.sample_depends_on, 0);
assert_eq!(f.sample_degradation_priority, 0);
}
#[test]
fn sample_flags_i_picture_pattern() {
let raw: u32 = 0x0A40_0000;
let f = super::SampleFlags::from_u32(raw);
assert_eq!(f.is_leading, 2);
assert_eq!(f.sample_depends_on, 2);
assert_eq!(f.sample_is_depended_on, 1);
assert_eq!(f.sample_has_redundancy, 0);
assert!(!f.sample_is_non_sync_sample);
assert!(f.is_sync_sample());
assert_eq!(f.sample_padding_value, 0);
assert_eq!(f.sample_degradation_priority, 0);
assert_eq!(f.to_u32(), raw);
}
#[test]
fn sample_flags_b_picture_pattern() {
let raw: u32 = 0x0181_0000;
let f = super::SampleFlags::from_u32(raw);
assert_eq!(f.is_leading, 0);
assert_eq!(f.sample_depends_on, 1);
assert_eq!(f.sample_is_depended_on, 2);
assert_eq!(f.sample_has_redundancy, 0);
assert!(f.sample_is_non_sync_sample);
assert!(!f.is_sync_sample());
assert_eq!(f.sample_padding_value, 0);
assert_eq!(f.sample_degradation_priority, 0);
assert_eq!(f.to_u32(), raw);
}
#[test]
fn sample_flags_degradation_priority_field_is_low_16_bits() {
let f = super::SampleFlags::from_u32(0x0000_BEEF);
assert_eq!(f.sample_degradation_priority, 0xBEEF);
assert_eq!(f.is_leading, 0);
assert_eq!(f.sample_depends_on, 0);
assert_eq!(f.sample_is_depended_on, 0);
assert_eq!(f.sample_has_redundancy, 0);
assert_eq!(f.sample_padding_value, 0);
assert!(!f.sample_is_non_sync_sample);
}
#[test]
fn sample_flags_padding_field_round_trips() {
for pad in 0u8..=7 {
let f = super::SampleFlags {
sample_padding_value: pad,
..Default::default()
};
let round = super::SampleFlags::from_u32(f.to_u32());
assert_eq!(round.sample_padding_value, pad);
assert_eq!(round, f);
}
}
#[test]
fn sample_flags_reserved_bits_ignored_on_decode() {
let raw = 0xF000_0000_u32 | 0x0001_0000_u32; let f = super::SampleFlags::from_u32(raw);
assert!(f.sample_is_non_sync_sample);
assert_eq!(f.to_u32(), 0x0001_0000);
}
#[test]
fn sample_flags_all_field_widths_round_trip() {
let f = super::SampleFlags {
is_leading: 3,
sample_depends_on: 3,
sample_is_depended_on: 3,
sample_has_redundancy: 3,
sample_padding_value: 7,
sample_is_non_sync_sample: true,
sample_degradation_priority: 0xFFFF,
};
let raw = f.to_u32();
assert_eq!(raw, 0x0FFF_FFFF);
assert_eq!(super::SampleFlags::from_u32(raw), f);
}
#[test]
fn parse_sample_flags_public_helper_matches_struct() {
let raw = 0x0244_0000_u32;
assert_eq!(
super::parse_sample_flags(raw),
super::SampleFlags::from_u32(raw)
);
}
}