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_piff_pssh, parse_piff_senc, parse_piff_tenc, parse_pssh, parse_senc, parse_tenc,
PiffSencBox, PiffTencBox, PsshBox, SencBox, TencBox, PIFF_PSSH_USERTYPE, PIFF_SENC_USERTYPE,
PIFF_TENC_USERTYPE,
};
use crate::codec_id::{from_sample_entry, from_sample_entry_with_oti};
use crate::emsg::{parse_emsg_box, EmsgBox};
pub fn open(input: Box<dyn ReadSeek>, codecs: &dyn CodecResolver) -> Result<Box<dyn Demuxer>> {
Ok(Box::new(open_typed(input, codecs)?))
}
pub fn open_typed(mut input: Box<dyn ReadSeek>, codecs: &dyn CodecResolver) -> Result<Mp4Demuxer> {
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 mfro_size: Option<u32> = None;
let mut mfra_actual_size: Option<u64> = None;
let mut prfts: Vec<PrftRecord> = Vec::new();
let mut pdin: Option<PdinRecord> = None;
let mut pnot: Option<PnotRecord> = None;
let mut meta_items = MetaItems::default();
let mut meco: MecoBox = MecoBox::default();
let mut emsgs: Vec<EmsgRecord> = Vec::new();
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)?);
}
}
PNOT => {
let body = read_box_body(&mut *input, &hdr)?;
if pnot.is_none() {
pnot = parse_pnot(&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)?;
mfra_actual_size = Some(hdr.header_len + body.len() as u64);
parse_mfra(&body, &mut tfras, &mut mfro_size)?;
}
EMSG => {
let body = read_box_body(&mut *input, &hdr)?;
if let Ok(e) = parse_emsg_box(&body) {
emsgs.push(EmsgRecord {
next_moof_index: moofs.len(),
emsg: e,
});
}
}
PRFT => {
let body = read_box_body(&mut *input, &hdr)?;
if let Some(r) = parse_prft(&body)? {
prfts.push(r);
}
}
META => {
let body = read_box_body(&mut *input, &hdr)?;
if meta_items.is_empty() && meta_items.handler_type == [0; 4] {
meta_items = parse_meta_items(&body);
}
}
MECO => {
let body = read_box_body(&mut *input, &hdr)?;
if meco.is_empty() {
meco = parse_meco_box(&body);
}
}
_ => 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 file_size = input.seek(std::io::SeekFrom::End(0))?;
let mut parsed = parse_moov(&moov, file_size)?;
if parsed.tracks.is_empty() {
return Err(Error::invalid("MP4: no tracks"));
}
let movie_timescale = parsed.movie_timescale;
for t in &mut parsed.tracks {
t.elst_timeline = build_elst_timeline(&t.elst, movie_timescale, t.timescale);
}
let parsed = parsed;
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();
let mut traf_sample_groups: Vec<TrafSampleGroupRecord> = Vec::new();
let mut piff_senc_records: Vec<PiffSencRecord> = Vec::new();
let mut piff_moof_psshes: Vec<MoofPsshRecord> = Vec::new();
let mut empty_durations: Vec<EmptyDurationRecord> = Vec::new();
let mut moof_earliest: Vec<Option<(i64, u32)>> = Vec::with_capacity(moofs.len());
let mut sample_budget: u64 = {
let end = input.seek(std::io::SeekFrom::End(0))?;
end.max(4096)
};
for moof in &moofs {
let before = samples.len();
parse_moof(
moof,
&parsed.tracks,
&mut samples,
&mut next_dts,
&mut senc_records,
&mut sai_records,
&mut moof_psshes,
&mut traf_sample_groups,
&mut piff_senc_records,
&mut piff_moof_psshes,
&mut empty_durations,
&mut sample_budget,
)?;
let mut best: Option<(i64, u32)> = None;
for s in &samples[before..] {
let ts = parsed.tracks[s.track_idx as usize].timescale;
if ts == 0 {
continue;
}
best = match best {
None => Some((s.pts, ts)),
Some((b_pts, b_ts)) => {
if (s.pts as i128) * (b_ts as i128) < (b_pts as i128) * (ts as i128) {
Some((s.pts, ts))
} else {
Some((b_pts, b_ts))
}
}
};
}
moof_earliest.push(best);
}
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(p) = pnot.as_ref() {
metadata.push((
"pnot".to_string(),
format!(
"{} {} mod={}",
String::from_utf8_lossy(&p.atom_type),
p.atom_index,
p.modification_date
),
));
}
surface_meta_items(&meta_items, &mut metadata);
if !meco.is_empty() {
if !meco.metas.is_empty() {
metadata.push(("meco_meta_count".to_string(), meco.metas.len().to_string()));
for (n, m) in meco.metas.iter().enumerate() {
metadata.push((
format!("meco_meta_{n}"),
String::from_utf8_lossy(&m.handler_type).to_string(),
));
}
}
if !meco.relations.is_empty() {
metadata.push((
"meco_relation_count".to_string(),
meco.relations.len().to_string(),
));
for (n, r) in meco.relations.iter().enumerate() {
metadata.push((
format!("meco_relation_{n}"),
format!(
"{}-{}={}",
String::from_utf8_lossy(&r.first_metabox_handler_type),
String::from_utf8_lossy(&r.second_metabox_handler_type),
r.metabox_relation
),
));
}
}
}
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));
if let Some(a) = &c.assp {
if a.version == 0 {
if let Some(e) = a.entries.first() {
let _ = write!(s, "(off={})", e.min_initial_alt_startup_offset);
}
} else {
let _ = write!(s, "(");
for (i, e) in a.entries.iter().enumerate() {
if i > 0 {
let _ = write!(s, " ");
}
let _ = write!(
s,
"{}:{}",
e.grouping_type_parameter.unwrap_or(0),
e.min_initial_alt_startup_offset
);
}
let _ = write!(s, ")");
}
}
}
metadata.push((format!("trep_{n}"), s));
}
for (n, p) in prfts.iter().enumerate() {
let mut value = format!(
"{} {} {}",
p.reference_track_id, p.ntp_timestamp, p.media_time
);
if p.is_realtime_offset() {
value.push_str(" realtime_offset");
} else {
if p.is_finalization_time() {
value.push_str(" finalization_time");
}
if p.is_arbitrary_association() {
value.push_str(" arbitrary_association");
}
}
if p.is_encoder_input_output() {
value.push_str(" encoder_input_output");
}
if p.is_file_write_time() {
value.push_str(" file_write_time");
}
metadata.push((format!("prft_{n}"), value));
}
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),
));
}
if let Some(declared) = mfro_size {
metadata.push(("mfro_size".to_string(), declared.to_string()));
if let Some(actual) = mfra_actual_size {
if declared as u64 != actual {
metadata.push((
"mfro_size_mismatch".to_string(),
format!("declared={declared} actual={actual}"),
));
}
}
}
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 empty_durations.iter().enumerate() {
metadata.push((
format!("frag_empty_duration_{n}"),
format!(
"track={} seq={} duration={}",
r.track_idx, r.moof_sequence, r.duration,
),
));
}
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(),
),
));
}
for (n, p) in parsed.piff_psshes.iter().enumerate() {
let sysid_hex: String = p.system_id.iter().map(|b| format!("{b:02x}")).collect();
metadata.push((
format!("piff_pssh_{n}"),
format!("{} {} {}", sysid_hex, p.kids.len(), p.data.len()),
));
}
for (n, r) in piff_moof_psshes.iter().enumerate() {
let sysid_hex: String = r
.pssh
.system_id
.iter()
.map(|b| format!("{b:02x}"))
.collect();
metadata.push((
format!("piff_moof_pssh_{n}"),
format!(
"systemid={} seq={} kids={} data={}",
sysid_hex,
r.moof_sequence,
r.pssh.kids.len(),
r.pssh.data.len(),
),
));
}
for (n, r) in piff_senc_records.iter().enumerate() {
metadata.push((
format!("piff_senc_{n}"),
format!(
"track={} seq={} samples={} flags=0x{:08x} override={}",
r.track_idx,
r.moof_sequence,
r.senc.samples.len(),
r.senc.flags,
u8::from(r.senc.has_override()),
),
));
}
for (n, r) in emsgs.iter().enumerate() {
let time_token = match r.emsg.presentation {
crate::emsg::EmsgTime::Delta(d) => format!("delta={d}"),
crate::emsg::EmsgTime::Absolute(t) => format!("time={t}"),
};
metadata.push((
format!("emsg_{n}"),
format!(
"scheme={} value={} timescale={} {} duration={} id={} bytes={} before_moof={}",
r.emsg.scheme_id_uri,
r.emsg.value,
r.emsg.timescale,
time_token,
r.emsg.event_duration,
r.emsg.id,
r.emsg.message_data.len(),
r.next_moof_index,
),
));
}
for (n, r) in traf_sample_groups.iter().enumerate() {
metadata.push((
format!("frag_sample_group_{n}"),
format!(
"track={} seq={} sgpd={} sbgp={} csgp={}",
r.track_idx,
r.moof_sequence,
r.sgpd.len(),
r.sbgp.len(),
r.csgp.len(),
),
));
}
Ok(Mp4Demuxer {
input,
input_len: file_size,
streams,
samples,
cursor: 0,
metadata,
duration_micros,
sidxes,
ssixes,
tfras,
prfts,
pdin,
pnot,
leva: parsed.leva,
treps: parsed.treps,
psshes: parsed.psshes,
moof_psshes,
senc_records,
sai_records,
empty_durations,
track_tenc_iv_sizes: parsed
.tracks
.iter()
.map(|t| t.tenc.as_ref().map(|x| x.default_per_sample_iv_size))
.collect(),
traf_sample_groups,
piff_psshes: parsed.piff_psshes,
piff_moof_psshes,
piff_senc_records,
piff_tencs: parsed.tracks.iter().map(|t| t.piff_tenc.clone()).collect(),
emsgs,
moof_earliest,
meta_items,
meco,
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(),
edit_lists: parsed
.tracks
.iter()
.map(|t| t.elst.iter().map(elst_entry_to_public).collect())
.collect(),
last_sdi: None,
})
}
#[derive(Clone, Debug)]
pub struct EmsgRecord {
pub next_moof_index: usize,
pub emsg: EmsgBox,
}
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, PartialEq, Eq)]
pub struct PrftRecord {
pub reference_track_id: u32,
pub ntp_timestamp: u64,
pub media_time: u64,
pub version: u8,
pub flags: u32,
}
impl PrftRecord {
pub fn is_encoder_input_output(&self) -> bool {
self.flags & 0x00_0001 != 0
}
pub fn is_finalization_time(&self) -> bool {
self.flags & 0x00_0002 != 0
}
pub fn is_file_write_time(&self) -> bool {
self.flags & 0x00_0004 != 0
}
pub fn is_arbitrary_association(&self) -> bool {
self.flags & 0x00_0008 != 0
}
pub fn is_realtime_offset(&self) -> bool {
self.flags & 0x00_0018 == 0x00_0018
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AmveRecord {
pub ambient_illuminance: u32,
pub ambient_light_x: u16,
pub ambient_light_y: u16,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BtrtRecord {
pub buffer_size_db: u32,
pub max_bitrate: u32,
pub avg_bitrate: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PaspRecord {
pub h_spacing: u32,
pub v_spacing: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ClapRecord {
pub width_n: u32,
pub width_d: u32,
pub height_n: u32,
pub height_d: u32,
pub horiz_off_n: u32,
pub horiz_off_d: u32,
pub vert_off_n: u32,
pub vert_off_d: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ColrRecord {
Nclx {
colour_primaries: u16,
transfer_characteristics: u16,
matrix_coefficients: u16,
full_range: bool,
},
RestrictedIcc(Vec<u8>),
UnrestrictedIcc(Vec<u8>),
Other { colour_type: [u8; 4], data: Vec<u8> },
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StviRecord {
pub single_view_allowed: u8,
pub stereo_scheme: u32,
pub stereo_indication_type: Vec<u8>,
}
impl StviRecord {
pub fn right_view_monoscopic_allowed(&self) -> bool {
self.single_view_allowed & 1 != 0
}
pub fn left_view_monoscopic_allowed(&self) -> bool {
self.single_view_allowed & 2 != 0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PdinEntry {
pub rate: u32,
pub initial_delay: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PdinRecord {
pub entries: Vec<PdinEntry>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PnotRecord {
pub modification_date: u32,
pub version: u16,
pub atom_type: [u8; 4],
pub atom_index: u16,
}
#[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, PartialEq, Eq)]
pub struct LevaRecord {
pub entries: Vec<LevaEntry>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AsspEntry {
pub grouping_type_parameter: Option<u32>,
pub min_initial_alt_startup_offset: i32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AsspRecord {
pub version: u8,
pub entries: Vec<AsspEntry>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TrepChild {
pub fourcc: [u8; 4],
pub payload_len: usize,
pub assp: Option<AsspRecord>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
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>,
piff_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>,
elst_timeline: ElstTimeline,
trex: TrexDefaults,
protection_scheme: Option<[u8; 4]>,
tenc: Option<TencBox>,
piff_tenc: Option<PiffTencBox>,
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>,
amve: Option<AmveRecord>,
stvi: Option<StviRecord>,
btrt: Option<BtrtRecord>,
pasp: Option<PaspRecord>,
clap: Option<ClapRecord>,
colr: Option<ColrRecord>,
hmhd: Option<HmhdBox>,
gmin: Option<GminBox>,
tcmi: Option<TcmiBox>,
tmcd: Option<TmcdSampleEntry>,
text_entry: Option<TextSampleEntry>,
load_settings: Option<LoadSettingsBox>,
rtp_hint: Option<crate::hint::RtpHintSampleEntry>,
mpeg2ts_hint: Option<crate::hint::Mpeg2TsHintSampleEntry>,
hint_stats: crate::hint::HintStatistics,
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)]
pub struct SbgpBox {
pub grouping_type: [u8; 4],
pub grouping_type_parameter: Option<u32>,
pub entries: Vec<(u32, u32)>,
}
#[derive(Clone, Debug, Default)]
pub struct SgpdBox {
pub grouping_type: [u8; 4],
pub default_sample_description_index: Option<u32>,
pub entries: Vec<Vec<u8>>,
}
#[derive(Clone, Debug, Default)]
pub struct CsgpPattern {
pub sample_count: u32,
pub indices: Vec<u32>,
}
#[derive(Clone, Debug, Default)]
pub struct CsgpBox {
pub grouping_type: [u8; 4],
pub grouping_type_parameter: Option<u32>,
pub index_msb_indicates_fragment_local_description: bool,
pub index_field_bits: u8,
pub patterns: Vec<CsgpPattern>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct CsgpResolvedIndex {
pub fragment_local: bool,
pub value: u32,
}
impl CsgpPattern {
pub fn resolve_index(
&self,
n: usize,
index_msb_indicates_fragment_local_description: bool,
index_field_bits: u8,
) -> Option<CsgpResolvedIndex> {
let raw = *self.indices.get(n)?;
Some(resolve_csgp_index(
raw,
index_msb_indicates_fragment_local_description,
index_field_bits,
))
}
}
fn resolve_csgp_index(
raw: u32,
msb_is_fragment_local: bool,
index_field_bits: u8,
) -> CsgpResolvedIndex {
if !msb_is_fragment_local || index_field_bits == 0 || index_field_bits > 32 {
return CsgpResolvedIndex {
fragment_local: false,
value: raw,
};
}
let msb_pos = index_field_bits - 1;
let msb_mask = 1u32 << msb_pos;
CsgpResolvedIndex {
fragment_local: raw & msb_mask != 0,
value: raw & (msb_mask - 1),
}
}
impl CsgpBox {
pub fn resolve_samples(&self, total_samples: usize) -> Vec<CsgpResolvedIndex> {
let mut out: Vec<CsgpResolvedIndex> = Vec::with_capacity(total_samples);
'patterns: for pat in &self.patterns {
let plen = pat.indices.len();
if plen == 0 {
continue;
}
for s in 0..pat.sample_count as usize {
if out.len() >= total_samples {
break 'patterns;
}
let raw = pat.indices[s % plen];
out.push(resolve_csgp_index(
raw,
self.index_msb_indicates_fragment_local_description,
self.index_field_bits,
));
}
}
while out.len() < total_samples {
out.push(CsgpResolvedIndex {
fragment_local: false,
value: 0,
});
}
out
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SubSampleEntry {
pub subsample_size: u32,
pub subsample_priority: u8,
pub discardable: u8,
pub codec_specific_parameters: u32,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SubsEntry {
pub sample_delta: u32,
pub subsamples: Vec<SubSampleEntry>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SubsBox {
pub version: u8,
pub flags: u32,
pub entries: Vec<SubsEntry>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SaizBox {
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, PartialEq, Eq)]
pub struct SaioBox {
pub version: u8,
pub aux_info_type: Option<[u8; 4]>,
pub aux_info_type_parameter: Option<u32>,
pub 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, Copy, Debug, Default, PartialEq, Eq)]
struct HmhdBox {
max_pdu_size: u16,
avg_pdu_size: u16,
max_bitrate: u32,
avg_bitrate: u32,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct GminBox {
pub graphicsmode: u16,
pub opcolor: [u16; 3],
pub balance: i16,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TcmiBox {
pub text_font: u16,
pub text_face: u16,
pub text_size: u16,
pub text_color: [u16; 3],
pub background_color: [u16; 3],
pub font_name: String,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TmcdSampleEntry {
pub flags: u32,
pub timescale: u32,
pub frame_duration: u32,
pub number_of_frames: u8,
}
impl TmcdSampleEntry {
pub fn drop_frame(&self) -> bool {
self.flags & 0x0001 != 0
}
pub fn twenty_four_hour_max(&self) -> bool {
self.flags & 0x0002 != 0
}
pub fn negative_times_ok(&self) -> bool {
self.flags & 0x0004 != 0
}
pub fn counter(&self) -> bool {
self.flags & 0x0008 != 0
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TextSampleEntry {
pub display_flags: u32,
pub text_justification: i32,
pub background_color: [u16; 3],
pub default_text_box: [i16; 4],
pub font_number: u16,
pub font_face: u16,
pub foreground_color: [u16; 3],
pub text_name: String,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct LoadSettingsBox {
pub preload_start_time: i32,
pub preload_duration: i32,
pub preload_flags: i32,
pub default_hints: i32,
}
impl LoadSettingsBox {
pub fn preload_always(&self) -> bool {
self.preload_flags == 1
}
pub fn preload_if_enabled(&self) -> bool {
self.preload_flags == 2
}
pub fn double_buffer(&self) -> bool {
self.default_hints & 0x0020 != 0
}
pub fn high_quality(&self) -> bool {
self.default_hints & 0x0100 != 0
}
}
#[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)]
struct ElstEntry {
segment_duration: u64,
media_time: i64,
media_rate: u32,
}
#[derive(Clone, Copy, Debug)]
struct ElstMappedSegment {
pres_start: i64,
media_start: i64,
dur: i64,
}
#[derive(Clone, Debug, Default)]
struct ElstTimeline {
segments: Vec<ElstMappedSegment>,
leading_shift: i64,
mapped: bool,
}
impl ElstTimeline {
fn delta_for_cts(&self, cts: i64) -> (i64, bool) {
if !self.mapped || self.segments.is_empty() {
return (self.leading_shift.saturating_neg(), true);
}
let first = &self.segments[0];
if cts < first.media_start {
return (first.pres_start.saturating_sub(first.media_start), false);
}
let mut chosen = 0usize;
for (i, s) in self.segments.iter().enumerate() {
if s.media_start <= cts {
chosen = i;
} else {
break;
}
}
let s = &self.segments[chosen];
let delta = s.pres_start.saturating_sub(s.media_start);
let in_range = s.dur == 0 || cts < s.media_start.saturating_add(s.dur);
if in_range {
(delta, true)
} else {
(delta, chosen == self.segments.len() - 1)
}
}
}
fn build_elst_timeline(
elst: &[ElstEntry],
movie_timescale: u32,
media_timescale: u32,
) -> ElstTimeline {
let leading_shift = elst_leading_media_time(elst);
let mut segments: Vec<ElstMappedSegment> = Vec::new();
let mut pres: i128 = 0;
for e in elst {
let dur_media: i64 = if movie_timescale > 0 {
let d = (e.segment_duration as u128).saturating_mul(media_timescale as u128)
/ movie_timescale as u128;
i64::try_from(d).unwrap_or(i64::MAX)
} else {
0
};
let rate_integer = (e.media_rate >> 16) as i16;
if e.media_time == -1 || rate_integer == 0 {
pres = pres.saturating_add(dur_media as i128);
continue;
}
segments.push(ElstMappedSegment {
pres_start: i64::try_from(pres).unwrap_or(i64::MAX),
media_start: e.media_time,
dur: dur_media,
});
pres = pres.saturating_add(dur_media as i128);
}
let mut mapped = !segments.is_empty();
for w in segments.windows(2) {
let d0 = w[0].pres_start.saturating_sub(w[0].media_start);
let d1 = w[1].pres_start.saturating_sub(w[1].media_start);
if w[1].media_start < w[0].media_start || d1 < d0 {
mapped = false;
break;
}
}
ElstTimeline {
segments,
leading_shift,
mapped,
}
}
#[derive(Clone, Copy, Debug, Default)]
struct TrexDefaults {
default_sample_description_index: u32,
default_sample_duration: u32,
default_sample_size: u32,
default_sample_flags: u32,
}
fn parse_moov(moov: &[u8], file_size: u64) -> 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, file_size)? {
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);
}
}
UUID => {
let body = read_bytes_vec(&mut cur, psz)?;
if body.len() >= 16 && body[..16] == PIFF_PSSH_USERTYPE {
if let Ok(p) = parse_piff_pssh(&body[16..]) {
out.piff_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()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct IlocExtent {
pub extent_index: u64,
pub extent_offset: u64,
pub extent_length: u64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IlocItem {
pub item_id: u32,
pub construction_method: u8,
pub data_reference_index: u16,
pub base_offset: u64,
pub extents: Vec<IlocExtent>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IlocBox {
pub version: u8,
pub offset_size: u8,
pub length_size: u8,
pub base_offset_size: u8,
pub index_size: u8,
pub items: Vec<IlocItem>,
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct ItemInfoEntry {
pub item_id: u32,
pub protection_index: u16,
pub item_type: [u8; 4],
pub item_name: String,
pub content_type: String,
pub content_encoding: String,
pub version: u8,
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct IinfBox {
pub entries: Vec<ItemInfoEntry>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ItemReference {
pub reference_type: [u8; 4],
pub from_item_id: u32,
pub to_item_ids: Vec<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IrefBox {
pub version: u8,
pub references: Vec<ItemReference>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ItemProperty {
Ispe { image_width: u32, image_height: u32 },
Pixi { bits_per_channel: Vec<u8> },
Rloc {
horizontal_offset: u32,
vertical_offset: u32,
},
AuxC {
aux_type: String,
aux_subtype: Vec<u8>,
},
Irot { angle: u8 },
Imir { axis: u8 },
Lsel { layer_id: u16 },
Pasp(PaspRecord),
Clap(ClapRecord),
Colr(ColrRecord),
Udes {
lang: String,
name: String,
description: String,
tags: String,
},
Altt { alt_text: String, alt_lang: String },
Iscl {
target_width_numerator: u16,
target_width_denominator: u16,
target_height_numerator: u16,
target_height_denominator: u16,
},
Rref { reference_types: Vec<[u8; 4]> },
Crtt { creation_time: u64 },
Mdft { modification_time: u64 },
Other { box_type: [u8; 4], body: Vec<u8> },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PropertyAssociation {
pub essential: bool,
pub property_index: u16,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ItemPropertyAssociationEntry {
pub item_id: u32,
pub associations: Vec<PropertyAssociation>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ItemProperties {
pub properties: Vec<ItemProperty>,
pub associations: Vec<ItemPropertyAssociationEntry>,
}
impl ItemProperties {
pub fn is_empty(&self) -> bool {
self.properties.is_empty() && self.associations.is_empty()
}
pub fn property(&self, property_index: u16) -> Option<&ItemProperty> {
if property_index == 0 {
return None;
}
self.properties.get((property_index - 1) as usize)
}
pub fn properties_for(&self, item_id: u32) -> Vec<(bool, &ItemProperty)> {
let mut out = Vec::new();
for entry in &self.associations {
if entry.item_id != item_id {
continue;
}
for a in &entry.associations {
if let Some(p) = self.property(a.property_index) {
out.push((a.essential, p));
}
}
}
out
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EntityToGroup {
pub grouping_type: [u8; 4],
pub group_id: u32,
pub entity_ids: Vec<u32>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct EntityGroups {
pub groups: Vec<EntityToGroup>,
}
impl EntityGroups {
pub fn is_empty(&self) -> bool {
self.groups.is_empty()
}
pub fn by_type(&self, grouping_type: [u8; 4]) -> impl Iterator<Item = &EntityToGroup> {
self.groups
.iter()
.filter(move |g| g.grouping_type == grouping_type)
}
pub fn by_id(&self, group_id: u32) -> Option<&EntityToGroup> {
self.groups.iter().find(|g| g.group_id == group_id)
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MetaItems {
pub primary_item_id: Option<u32>,
pub handler_type: [u8; 4],
pub iloc: Option<IlocBox>,
pub iinf: Option<IinfBox>,
pub iref: Option<IrefBox>,
pub idat: Vec<u8>,
pub fiin: Option<crate::fd::FiinBox>,
pub iprp: Option<ItemProperties>,
pub grpl: Option<EntityGroups>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ItemByteRange {
pub offset: u64,
pub length: u64,
}
impl MetaItems {
pub fn is_empty(&self) -> bool {
self.primary_item_id.is_none()
&& self.iloc.is_none()
&& self.iinf.is_none()
&& self.iref.is_none()
&& self.idat.is_empty()
&& self.fiin.is_none()
&& self.iprp.is_none()
&& self.grpl.is_none()
}
pub fn iloc_item(&self, item_id: u32) -> Option<&IlocItem> {
self.iloc
.as_ref()?
.items
.iter()
.find(|it| it.item_id == item_id)
}
pub fn item_info(&self, item_id: u32) -> Option<&ItemInfoEntry> {
self.iinf
.as_ref()?
.entries
.iter()
.find(|e| e.item_id == item_id)
}
pub fn item_byte_ranges(&self, item_id: u32) -> Option<Vec<ItemByteRange>> {
let it = self.iloc_item(item_id)?;
Some(
it.extents
.iter()
.map(|ex| ItemByteRange {
offset: it.base_offset.saturating_add(ex.extent_offset),
length: ex.extent_length,
})
.collect(),
)
}
pub fn item_data_from_idat(&self, item_id: u32) -> Option<Vec<u8>> {
let it = self.iloc_item(item_id)?;
if it.construction_method != 1 {
return None;
}
let mut out = Vec::new();
for ex in &it.extents {
let start = it.base_offset.checked_add(ex.extent_offset)? as usize;
let len = if ex.extent_length == 0 && it.extents.len() == 1 {
self.idat.len().checked_sub(start)?
} else {
ex.extent_length as usize
};
let end = start.checked_add(len)?;
if end > self.idat.len() {
return None;
}
out.extend_from_slice(&self.idat[start..end]);
}
Some(out)
}
}
fn read_sized_be(buf: &[u8], pos: &mut usize, n: usize) -> Option<u64> {
if n == 0 {
return Some(0);
}
if *pos + n > buf.len() {
return None;
}
let mut v: u64 = 0;
for &b in &buf[*pos..*pos + n] {
v = (v << 8) | b as u64;
}
*pos += n;
Some(v)
}
pub fn parse_iloc_box(body: &[u8]) -> Option<IlocBox> {
if body.len() < 6 {
return None;
}
let version = body[0];
if version > 2 {
return None;
}
let mut pos = 4usize;
let b = body[pos];
let offset_size = b >> 4;
let length_size = b & 0x0F;
pos += 1;
let b = body[pos];
let base_offset_size = b >> 4;
let index_size = if version == 1 || version == 2 {
b & 0x0F
} else {
0
};
pos += 1;
for &s in &[offset_size, length_size, base_offset_size, index_size] {
if s != 0 && s != 4 && s != 8 {
return None;
}
}
let item_count = if version < 2 {
read_sized_be(body, &mut pos, 2)?
} else {
read_sized_be(body, &mut pos, 4)?
};
let mut items = Vec::new();
for _ in 0..item_count {
let item_id = if version < 2 {
read_sized_be(body, &mut pos, 2)? as u32
} else {
read_sized_be(body, &mut pos, 4)? as u32
};
let construction_method = if version == 1 || version == 2 {
let w = read_sized_be(body, &mut pos, 2)?;
(w & 0x0F) as u8
} else {
0
};
let data_reference_index = read_sized_be(body, &mut pos, 2)? as u16;
let base_offset = read_sized_be(body, &mut pos, base_offset_size as usize)?;
let extent_count = read_sized_be(body, &mut pos, 2)?;
let mut extents = Vec::new();
for _ in 0..extent_count {
let extent_index = if (version == 1 || version == 2) && index_size > 0 {
read_sized_be(body, &mut pos, index_size as usize)?
} else {
0
};
let extent_offset = read_sized_be(body, &mut pos, offset_size as usize)?;
let extent_length = read_sized_be(body, &mut pos, length_size as usize)?;
extents.push(IlocExtent {
extent_index,
extent_offset,
extent_length,
});
}
items.push(IlocItem {
item_id,
construction_method,
data_reference_index,
base_offset,
extents,
});
}
Some(IlocBox {
version,
offset_size,
length_size,
base_offset_size,
index_size,
items,
})
}
pub fn parse_pitm_box(body: &[u8]) -> Option<u32> {
if body.len() < 4 {
return None;
}
let version = body[0];
let mut pos = 4usize;
if version == 0 {
read_sized_be(body, &mut pos, 2).map(|v| v as u32)
} else {
read_sized_be(body, &mut pos, 4).map(|v| v as u32)
}
}
fn read_c_string_at(buf: &[u8], pos: &mut usize) -> String {
let start = *pos;
let mut i = start;
while i < buf.len() && buf[i] != 0 {
i += 1;
}
let s = String::from_utf8_lossy(&buf[start..i]).into_owned();
*pos = if i < buf.len() { i + 1 } else { buf.len() };
s
}
fn parse_infe(body: &[u8]) -> Option<ItemInfoEntry> {
if body.len() < 4 {
return None;
}
let version = body[0];
let mut pos = 4usize;
let mut e = ItemInfoEntry {
version,
..Default::default()
};
if version == 0 || version == 1 {
e.item_id = read_sized_be(body, &mut pos, 2)? as u32;
e.protection_index = read_sized_be(body, &mut pos, 2)? as u16;
e.item_name = read_c_string_at(body, &mut pos);
e.content_type = read_c_string_at(body, &mut pos);
if pos < body.len() {
e.content_encoding = read_c_string_at(body, &mut pos);
}
} else {
e.item_id = if version == 2 {
read_sized_be(body, &mut pos, 2)? as u32
} else {
read_sized_be(body, &mut pos, 4)? as u32
};
e.protection_index = read_sized_be(body, &mut pos, 2)? as u16;
if pos + 4 > body.len() {
return None;
}
e.item_type.copy_from_slice(&body[pos..pos + 4]);
pos += 4;
e.item_name = read_c_string_at(body, &mut pos);
if &e.item_type == b"mime" {
e.content_type = read_c_string_at(body, &mut pos);
if pos < body.len() {
e.content_encoding = read_c_string_at(body, &mut pos);
}
} else if &e.item_type == b"uri " {
e.content_encoding = read_c_string_at(body, &mut pos);
}
}
Some(e)
}
pub fn parse_iinf_box(body: &[u8]) -> Option<IinfBox> {
if body.len() < 4 {
return None;
}
let version = body[0];
let mut pos = 4usize;
let entry_count = if version == 0 {
read_sized_be(body, &mut pos, 2)?
} else {
read_sized_be(body, &mut pos, 4)?
};
let mut out = IinfBox::default();
let mut cur = std::io::Cursor::new(&body[pos..]);
let region = &body[pos..];
let end = region.len() as u64;
let mut seen = 0u64;
while cur.position() < end && seen < entry_count {
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 > region.len() {
break;
}
cur.set_position((start + psz) as u64);
if hdr.fourcc == INFE {
if let Some(e) = parse_infe(®ion[start..start + psz]) {
out.entries.push(e);
}
}
seen += 1;
}
Some(out)
}
pub fn parse_iref_box(body: &[u8]) -> Option<IrefBox> {
if body.len() < 4 {
return None;
}
let version = body[0];
if version > 1 {
return None;
}
let id_bytes = if version == 0 { 2usize } else { 4usize };
let region = &body[4..];
let mut cur = std::io::Cursor::new(region);
let end = region.len() as u64;
let mut references = 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;
let start = cur.position() as usize;
if start + psz > region.len() {
break;
}
cur.set_position((start + psz) as u64);
let sub = ®ion[start..start + psz];
let mut p = 0usize;
let from_item_id = match read_sized_be(sub, &mut p, id_bytes) {
Some(v) => v as u32,
None => continue,
};
let ref_count = match read_sized_be(sub, &mut p, 2) {
Some(v) => v,
None => continue,
};
let mut to_item_ids = Vec::new();
for _ in 0..ref_count {
match read_sized_be(sub, &mut p, id_bytes) {
Some(v) => to_item_ids.push(v as u32),
None => break,
}
}
references.push(ItemReference {
reference_type: hdr.fourcc,
from_item_id,
to_item_ids,
});
}
Some(IrefBox {
version,
references,
})
}
pub fn parse_meta_items(body: &[u8]) -> MetaItems {
let mut out = MetaItems::default();
if body.len() < 4 {
return out;
}
let region = &body[4..];
let mut cur = std::io::Cursor::new(region);
let end = region.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 > region.len() {
break;
}
cur.set_position((start + psz) as u64);
let child = ®ion[start..start + psz];
match hdr.fourcc {
HDLR if child.len() >= 12 => {
out.handler_type.copy_from_slice(&child[8..12]);
}
PITM => {
if let Some(id) = parse_pitm_box(child) {
out.primary_item_id = Some(id);
}
}
ILOC => out.iloc = parse_iloc_box(child),
IINF => out.iinf = parse_iinf_box(child),
IREF => out.iref = parse_iref_box(child),
IDAT => out.idat = child.to_vec(),
FIIN => out.fiin = crate::fd::parse_fiin_box(child),
IPRP => {
let p = parse_iprp_box(child);
if !p.is_empty() {
out.iprp = Some(p);
}
}
GRPL => {
let g = parse_grpl_box(child);
if !g.is_empty() {
out.grpl = Some(g);
}
}
_ => {}
}
}
out
}
pub fn parse_iprp_box(body: &[u8]) -> ItemProperties {
let mut out = ItemProperties::default();
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 = match hdr.payload_size() {
Some(p) => p as usize,
None => break,
};
let start = cur.position() as usize;
if start + psz > body.len() {
break;
}
cur.set_position((start + psz) as u64);
let child = &body[start..start + psz];
match hdr.fourcc {
IPCO => out.properties = parse_ipco_box(child),
IPMA => out.associations.extend(parse_ipma_box(child)),
_ => {}
}
}
out
}
pub fn parse_ipco_box(body: &[u8]) -> Vec<ItemProperty> {
let mut out = 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).ok().flatten() {
Some(h) => h,
None => break,
};
let psz = match hdr.payload_size() {
Some(p) => p as usize,
None => break,
};
let start = cur.position() as usize;
if start + psz > body.len() {
break;
}
cur.set_position((start + psz) as u64);
let child = &body[start..start + psz];
out.push(parse_item_property(hdr.fourcc, child));
}
out
}
fn parse_item_property(box_type: [u8; 4], body: &[u8]) -> ItemProperty {
let other = || ItemProperty::Other {
box_type,
body: body.to_vec(),
};
match box_type {
ISPE => {
if body.len() < 12 {
return other();
}
ItemProperty::Ispe {
image_width: u32::from_be_bytes([body[4], body[5], body[6], body[7]]),
image_height: u32::from_be_bytes([body[8], body[9], body[10], body[11]]),
}
}
PIXI => {
if body.len() < 5 {
return other();
}
let num = body[4] as usize;
if 5 + num > body.len() {
return other();
}
ItemProperty::Pixi {
bits_per_channel: body[5..5 + num].to_vec(),
}
}
RLOC => {
if body.len() < 12 {
return other();
}
ItemProperty::Rloc {
horizontal_offset: u32::from_be_bytes([body[4], body[5], body[6], body[7]]),
vertical_offset: u32::from_be_bytes([body[8], body[9], body[10], body[11]]),
}
}
AUXC => {
if body.len() < 4 {
return other();
}
let rest = &body[4..];
let nul = rest.iter().position(|&b| b == 0);
let (type_bytes, sub) = match nul {
Some(i) => (&rest[..i], &rest[i + 1..]),
None => (rest, &rest[rest.len()..]),
};
ItemProperty::AuxC {
aux_type: String::from_utf8_lossy(type_bytes).to_string(),
aux_subtype: sub.to_vec(),
}
}
IROT => {
if body.is_empty() {
return other();
}
ItemProperty::Irot {
angle: body[0] & 0x03,
}
}
IMIR => {
if body.is_empty() {
return other();
}
ItemProperty::Imir {
axis: body[0] & 0x01,
}
}
LSEL => {
if body.len() < 2 {
return other();
}
ItemProperty::Lsel {
layer_id: u16::from_be_bytes([body[0], body[1]]),
}
}
UDES => {
if body.len() < 4 {
return other();
}
let mut p = 4usize;
let lang = read_c_string_at(body, &mut p);
let name = read_c_string_at(body, &mut p);
let description = read_c_string_at(body, &mut p);
let tags = read_c_string_at(body, &mut p);
ItemProperty::Udes {
lang,
name,
description,
tags,
}
}
ALTT => {
if body.len() < 4 {
return other();
}
let mut p = 4usize;
let alt_text = read_c_string_at(body, &mut p);
let alt_lang = read_c_string_at(body, &mut p);
ItemProperty::Altt { alt_text, alt_lang }
}
ISCL => {
if body.len() < 12 {
return other();
}
ItemProperty::Iscl {
target_width_numerator: u16::from_be_bytes([body[4], body[5]]),
target_width_denominator: u16::from_be_bytes([body[6], body[7]]),
target_height_numerator: u16::from_be_bytes([body[8], body[9]]),
target_height_denominator: u16::from_be_bytes([body[10], body[11]]),
}
}
RREF => {
if body.len() < 5 {
return other();
}
let count = body[4] as usize;
if 5 + count * 4 > body.len() {
return other();
}
let mut reference_types = Vec::with_capacity(count);
for i in 0..count {
let o = 5 + i * 4;
reference_types.push([body[o], body[o + 1], body[o + 2], body[o + 3]]);
}
ItemProperty::Rref { reference_types }
}
CRTT => {
if body.len() < 12 {
return other();
}
ItemProperty::Crtt {
creation_time: u64::from_be_bytes([
body[4], body[5], body[6], body[7], body[8], body[9], body[10], body[11],
]),
}
}
MDFT => {
if body.len() < 12 {
return other();
}
ItemProperty::Mdft {
modification_time: u64::from_be_bytes([
body[4], body[5], body[6], body[7], body[8], body[9], body[10], body[11],
]),
}
}
_ if &box_type == b"pasp" => parse_pasp(body)
.map(ItemProperty::Pasp)
.unwrap_or_else(other),
_ if &box_type == b"clap" => parse_clap(body)
.map(ItemProperty::Clap)
.unwrap_or_else(other),
_ if &box_type == b"colr" => parse_colr(body)
.map(ItemProperty::Colr)
.unwrap_or_else(other),
_ => other(),
}
}
pub fn parse_ipma_box(body: &[u8]) -> Vec<ItemPropertyAssociationEntry> {
let mut out = Vec::new();
if body.len() < 8 {
return out;
}
let version = body[0];
let flags = u32::from_be_bytes([0, body[1], body[2], body[3]]);
let wide_index = flags & 1 == 1;
let mut pos = 4usize;
let entry_count = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
pos += 4;
for _ in 0..entry_count {
let item_id = if version == 0 {
if pos + 2 > body.len() {
break;
}
let v = u16::from_be_bytes([body[pos], body[pos + 1]]) as u32;
pos += 2;
v
} else {
if pos + 4 > body.len() {
break;
}
let v = u32::from_be_bytes([body[pos], body[pos + 1], body[pos + 2], body[pos + 3]]);
pos += 4;
v
};
if pos >= body.len() {
break;
}
let assoc_count = body[pos] as usize;
pos += 1;
let mut associations = Vec::with_capacity(assoc_count);
let mut truncated = false;
for _ in 0..assoc_count {
if wide_index {
if pos + 2 > body.len() {
truncated = true;
break;
}
let w = u16::from_be_bytes([body[pos], body[pos + 1]]);
pos += 2;
associations.push(PropertyAssociation {
essential: w & 0x8000 != 0,
property_index: w & 0x7FFF,
});
} else {
if pos + 1 > body.len() {
truncated = true;
break;
}
let b = body[pos];
pos += 1;
associations.push(PropertyAssociation {
essential: b & 0x80 != 0,
property_index: (b & 0x7F) as u16,
});
}
}
out.push(ItemPropertyAssociationEntry {
item_id,
associations,
});
if truncated {
break;
}
}
out
}
pub fn parse_grpl_box(body: &[u8]) -> EntityGroups {
let mut out = EntityGroups::default();
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 = match hdr.payload_size() {
Some(p) => p as usize,
None => break,
};
let start = cur.position() as usize;
if start + psz > body.len() {
break;
}
cur.set_position((start + psz) as u64);
let child = &body[start..start + psz];
if let Some(g) = parse_entity_to_group(hdr.fourcc, child) {
out.groups.push(g);
}
}
out
}
fn parse_entity_to_group(grouping_type: [u8; 4], body: &[u8]) -> Option<EntityToGroup> {
if body.len() < 12 {
return None;
}
let group_id = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
let num = u32::from_be_bytes([body[8], body[9], body[10], body[11]]) as usize;
let mut pos = 12usize;
if pos + num * 4 > body.len() {
return None;
}
let mut entity_ids = Vec::with_capacity(num);
for _ in 0..num {
entity_ids.push(u32::from_be_bytes([
body[pos],
body[pos + 1],
body[pos + 2],
body[pos + 3],
]));
pos += 4;
}
Some(EntityToGroup {
grouping_type,
group_id,
entity_ids,
})
}
fn surface_meta_items(m: &MetaItems, metadata: &mut Vec<(String, String)>) {
if m.handler_type == [0; 4] && m.is_empty() {
return;
}
if m.handler_type != [0; 4] {
metadata.push((
"meta_handler".to_string(),
String::from_utf8_lossy(&m.handler_type).to_string(),
));
}
if let Some(id) = m.primary_item_id {
metadata.push(("meta_primary_item".to_string(), id.to_string()));
}
if let Some(iinf) = m.iinf.as_ref() {
metadata.push((
"meta_item_count".to_string(),
iinf.entries.len().to_string(),
));
for (n, e) in iinf.entries.iter().enumerate() {
let type_tok = if e.version >= 2 {
String::from_utf8_lossy(&e.item_type).to_string()
} else {
format!("v{}", e.version)
};
metadata.push((
format!("meta_item_{n}"),
format!("id={} type={} name={}", e.item_id, type_tok, e.item_name),
));
}
}
if let Some(iloc) = m.iloc.as_ref() {
metadata.push(("meta_iloc_count".to_string(), iloc.items.len().to_string()));
for (n, it) in iloc.items.iter().enumerate() {
let total_len: u64 = it.extents.iter().map(|e| e.extent_length).sum();
metadata.push((
format!("meta_iloc_{n}"),
format!(
"id={} method={} extents={} length={}",
it.item_id,
it.construction_method,
it.extents.len(),
total_len
),
));
}
}
if let Some(iref) = m.iref.as_ref() {
metadata.push((
"meta_iref_count".to_string(),
iref.references.len().to_string(),
));
for (n, r) in iref.references.iter().enumerate() {
let to: Vec<String> = r.to_item_ids.iter().map(|id| id.to_string()).collect();
metadata.push((
format!("meta_iref_{n}"),
format!(
"type={} from={} to={}",
String::from_utf8_lossy(&r.reference_type),
r.from_item_id,
to.join(",")
),
));
}
}
if !m.idat.is_empty() {
metadata.push(("meta_idat_len".to_string(), m.idat.len().to_string()));
}
if let Some(fiin) = m.fiin.as_ref() {
metadata.push((
"meta_fiin_partitions".to_string(),
fiin.partition_entries.len().to_string(),
));
if let Some(segr) = fiin.session_info.as_ref() {
metadata.push((
"meta_fiin_session_groups".to_string(),
segr.session_groups.len().to_string(),
));
}
if let Some(gitn) = fiin.group_id_to_name.as_ref() {
metadata.push((
"meta_fiin_group_names".to_string(),
gitn.entries.len().to_string(),
));
}
}
if let Some(iprp) = m.iprp.as_ref() {
metadata.push((
"meta_iprp_property_count".to_string(),
iprp.properties.len().to_string(),
));
for (n, p) in iprp.properties.iter().enumerate() {
metadata.push((format!("meta_iprp_property_{n}"), item_property_token(p)));
}
for (n, e) in iprp.associations.iter().enumerate() {
let assoc: Vec<String> = e
.associations
.iter()
.map(|a| {
if a.essential {
format!("{}*", a.property_index)
} else {
a.property_index.to_string()
}
})
.collect();
metadata.push((
format!("meta_iprp_item_{n}"),
format!("id={} props={}", e.item_id, assoc.join(",")),
));
}
}
if let Some(grpl) = m.grpl.as_ref() {
metadata.push((
"meta_grpl_group_count".to_string(),
grpl.groups.len().to_string(),
));
for (n, g) in grpl.groups.iter().enumerate() {
let ids: Vec<String> = g.entity_ids.iter().map(|e| e.to_string()).collect();
metadata.push((
format!("meta_grpl_group_{n}"),
format!(
"type={} id={} entities={}",
String::from_utf8_lossy(&g.grouping_type),
g.group_id,
ids.join(",")
),
));
}
}
}
fn item_property_token(p: &ItemProperty) -> String {
match p {
ItemProperty::Ispe {
image_width,
image_height,
} => format!("ispe {image_width}x{image_height}"),
ItemProperty::Pixi { bits_per_channel } => {
let bits: Vec<String> = bits_per_channel.iter().map(|b| b.to_string()).collect();
format!("pixi {}", bits.join(":"))
}
ItemProperty::Rloc {
horizontal_offset,
vertical_offset,
} => format!("rloc {horizontal_offset},{vertical_offset}"),
ItemProperty::AuxC { aux_type, .. } => format!("auxC {aux_type}"),
ItemProperty::Irot { angle } => format!("irot {}", *angle as u32 * 90),
ItemProperty::Imir { axis } => {
format!("imir {}", if *axis == 0 { "v" } else { "h" })
}
ItemProperty::Lsel { layer_id } => format!("lsel {layer_id}"),
ItemProperty::Pasp(r) => format!("pasp {}:{}", r.h_spacing, r.v_spacing),
ItemProperty::Clap(r) => format!("clap {}/{}", r.width_n, r.width_d),
ItemProperty::Colr(c) => match c {
ColrRecord::Nclx {
colour_primaries,
transfer_characteristics,
matrix_coefficients,
..
} => format!(
"colr nclx {colour_primaries}/{transfer_characteristics}/{matrix_coefficients}"
),
ColrRecord::RestrictedIcc(d) => format!("colr rICC {}", d.len()),
ColrRecord::UnrestrictedIcc(d) => format!("colr prof {}", d.len()),
ColrRecord::Other { colour_type, .. } => {
format!("colr {}", String::from_utf8_lossy(colour_type))
}
},
ItemProperty::Udes { name, lang, .. } => {
if lang.is_empty() {
format!("udes {name}")
} else {
format!("udes [{lang}] {name}")
}
}
ItemProperty::Altt { alt_text, .. } => format!("altt {alt_text}"),
ItemProperty::Iscl {
target_width_numerator,
target_width_denominator,
target_height_numerator,
target_height_denominator,
} => format!(
"iscl {target_width_numerator}/{target_width_denominator}x{target_height_numerator}/{target_height_denominator}"
),
ItemProperty::Rref { reference_types } => {
let types: Vec<String> = reference_types
.iter()
.map(|t| String::from_utf8_lossy(t).to_string())
.collect();
format!("rref {}", types.join(","))
}
ItemProperty::Crtt { creation_time } => format!("crtt {creation_time}"),
ItemProperty::Mdft { modification_time } => format!("mdft {modification_time}"),
ItemProperty::Other { box_type, .. } => String::from_utf8_lossy(box_type).to_string(),
}
}
fn write_sized_be(out: &mut Vec<u8>, value: u64, n: usize) {
if n == 0 {
return;
}
let bytes = value.to_be_bytes();
out.extend_from_slice(&bytes[8 - n..]);
}
fn wrap_box(fourcc: &[u8; 4], body: &[u8]) -> Vec<u8> {
let total = (8 + body.len()) as u32;
let mut v = Vec::with_capacity(8 + body.len());
v.extend_from_slice(&total.to_be_bytes());
v.extend_from_slice(fourcc);
v.extend_from_slice(body);
v
}
pub fn build_iloc_box(b: &IlocBox) -> Option<Vec<u8>> {
for &s in &[
b.offset_size,
b.length_size,
b.base_offset_size,
b.index_size,
] {
if s != 0 && s != 4 && s != 8 {
return None;
}
}
if b.version > 2 {
return None;
}
if b.version == 0 && b.index_size != 0 {
return None;
}
let mut body = Vec::new();
body.push(b.version);
body.extend_from_slice(&[0, 0, 0]); body.push((b.offset_size << 4) | (b.length_size & 0x0F));
let lo = if b.version == 1 || b.version == 2 {
b.index_size & 0x0F
} else {
0
};
body.push((b.base_offset_size << 4) | lo);
if b.version < 2 {
write_sized_be(&mut body, b.items.len() as u64, 2);
} else {
write_sized_be(&mut body, b.items.len() as u64, 4);
}
for it in &b.items {
if b.version < 2 {
write_sized_be(&mut body, it.item_id as u64, 2);
} else {
write_sized_be(&mut body, it.item_id as u64, 4);
}
if b.version == 1 || b.version == 2 {
if it.construction_method > 0x0F {
return None;
}
write_sized_be(&mut body, it.construction_method as u64, 2);
} else if it.construction_method != 0 {
return None;
}
write_sized_be(&mut body, it.data_reference_index as u64, 2);
write_sized_be(&mut body, it.base_offset, b.base_offset_size as usize);
write_sized_be(&mut body, it.extents.len() as u64, 2);
for ex in &it.extents {
if (b.version == 1 || b.version == 2) && b.index_size > 0 {
write_sized_be(&mut body, ex.extent_index, b.index_size as usize);
}
write_sized_be(&mut body, ex.extent_offset, b.offset_size as usize);
write_sized_be(&mut body, ex.extent_length, b.length_size as usize);
}
}
Some(wrap_box(&ILOC, &body))
}
pub fn build_pitm_box(item_id: u32) -> Vec<u8> {
let mut body = Vec::new();
if item_id <= u16::MAX as u32 {
body.push(0); body.extend_from_slice(&[0, 0, 0]);
write_sized_be(&mut body, item_id as u64, 2);
} else {
body.push(1); body.extend_from_slice(&[0, 0, 0]);
write_sized_be(&mut body, item_id as u64, 4);
}
wrap_box(&PITM, &body)
}
fn build_infe(e: &ItemInfoEntry) -> Option<Vec<u8>> {
if e.version > 3 {
return None;
}
for s in [&e.item_name, &e.content_type, &e.content_encoding] {
if s.as_bytes().contains(&0) {
return None;
}
}
let mut body = vec![e.version, 0, 0, 0];
if e.version == 0 || e.version == 1 {
write_sized_be(&mut body, e.item_id as u64, 2);
write_sized_be(&mut body, e.protection_index as u64, 2);
body.extend_from_slice(e.item_name.as_bytes());
body.push(0);
body.extend_from_slice(e.content_type.as_bytes());
body.push(0);
if !e.content_encoding.is_empty() {
body.extend_from_slice(e.content_encoding.as_bytes());
body.push(0);
}
} else {
if e.version == 2 {
write_sized_be(&mut body, e.item_id as u64, 2);
} else {
write_sized_be(&mut body, e.item_id as u64, 4);
}
write_sized_be(&mut body, e.protection_index as u64, 2);
body.extend_from_slice(&e.item_type);
body.extend_from_slice(e.item_name.as_bytes());
body.push(0);
if &e.item_type == b"mime" {
body.extend_from_slice(e.content_type.as_bytes());
body.push(0);
if !e.content_encoding.is_empty() {
body.extend_from_slice(e.content_encoding.as_bytes());
body.push(0);
}
} else if &e.item_type == b"uri " {
body.extend_from_slice(e.content_encoding.as_bytes());
body.push(0);
}
}
Some(wrap_box(&INFE, &body))
}
pub fn build_iinf_box(b: &IinfBox) -> Option<Vec<u8>> {
let mut body = Vec::new();
if b.entries.len() <= u16::MAX as usize {
body.push(0); body.extend_from_slice(&[0, 0, 0]);
write_sized_be(&mut body, b.entries.len() as u64, 2);
} else {
body.push(1); body.extend_from_slice(&[0, 0, 0]);
write_sized_be(&mut body, b.entries.len() as u64, 4);
}
for e in &b.entries {
body.extend_from_slice(&build_infe(e)?);
}
Some(wrap_box(&IINF, &body))
}
pub fn build_iref_box(b: &IrefBox) -> Option<Vec<u8>> {
if b.version > 1 {
return None;
}
let id_bytes = if b.version == 0 { 2 } else { 4 };
let mut body = vec![b.version, 0, 0, 0];
for r in &b.references {
if r.to_item_ids.len() > u16::MAX as usize {
return None;
}
let mut sub = Vec::new();
write_sized_be(&mut sub, r.from_item_id as u64, id_bytes);
write_sized_be(&mut sub, r.to_item_ids.len() as u64, 2);
for &to in &r.to_item_ids {
write_sized_be(&mut sub, to as u64, id_bytes);
}
body.extend_from_slice(&wrap_box(&r.reference_type, &sub));
}
Some(wrap_box(&IREF, &body))
}
pub fn build_idat_box(data: &[u8]) -> Vec<u8> {
wrap_box(&IDAT, data)
}
pub fn build_item_property(p: &ItemProperty) -> Vec<u8> {
match p {
ItemProperty::Ispe {
image_width,
image_height,
} => {
let mut body = vec![0u8; 4]; body.extend_from_slice(&image_width.to_be_bytes());
body.extend_from_slice(&image_height.to_be_bytes());
wrap_box(&ISPE, &body)
}
ItemProperty::Pixi { bits_per_channel } => {
let mut body = vec![0u8; 4]; body.push(bits_per_channel.len() as u8);
body.extend_from_slice(bits_per_channel);
wrap_box(&PIXI, &body)
}
ItemProperty::Rloc {
horizontal_offset,
vertical_offset,
} => {
let mut body = vec![0u8; 4]; body.extend_from_slice(&horizontal_offset.to_be_bytes());
body.extend_from_slice(&vertical_offset.to_be_bytes());
wrap_box(&RLOC, &body)
}
ItemProperty::AuxC {
aux_type,
aux_subtype,
} => {
let mut body = vec![0u8; 4]; body.extend_from_slice(aux_type.as_bytes());
body.push(0); body.extend_from_slice(aux_subtype);
wrap_box(&AUXC, &body)
}
ItemProperty::Irot { angle } => wrap_box(&IROT, &[angle & 0x03]),
ItemProperty::Imir { axis } => wrap_box(&IMIR, &[axis & 0x01]),
ItemProperty::Lsel { layer_id } => wrap_box(&LSEL, &layer_id.to_be_bytes()),
ItemProperty::Pasp(r) => build_pasp_box(r),
ItemProperty::Clap(r) => build_clap_box(r),
ItemProperty::Colr(c) => build_colr_box(c),
ItemProperty::Udes {
lang,
name,
description,
tags,
} => {
let mut body = vec![0u8; 4]; for s in [lang, name, description, tags] {
body.extend_from_slice(s.as_bytes());
body.push(0);
}
wrap_box(&UDES, &body)
}
ItemProperty::Altt { alt_text, alt_lang } => {
let mut body = vec![0u8; 4]; for s in [alt_text, alt_lang] {
body.extend_from_slice(s.as_bytes());
body.push(0);
}
wrap_box(&ALTT, &body)
}
ItemProperty::Iscl {
target_width_numerator,
target_width_denominator,
target_height_numerator,
target_height_denominator,
} => {
let mut body = vec![0u8; 4]; body.extend_from_slice(&target_width_numerator.to_be_bytes());
body.extend_from_slice(&target_width_denominator.to_be_bytes());
body.extend_from_slice(&target_height_numerator.to_be_bytes());
body.extend_from_slice(&target_height_denominator.to_be_bytes());
wrap_box(&ISCL, &body)
}
ItemProperty::Rref { reference_types } => {
let mut body = vec![0u8; 4]; body.push(reference_types.len() as u8);
for rt in reference_types {
body.extend_from_slice(rt);
}
wrap_box(&RREF, &body)
}
ItemProperty::Crtt { creation_time } => {
let mut body = vec![0u8; 4]; body.extend_from_slice(&creation_time.to_be_bytes());
wrap_box(&CRTT, &body)
}
ItemProperty::Mdft { modification_time } => {
let mut body = vec![0u8; 4]; body.extend_from_slice(&modification_time.to_be_bytes());
wrap_box(&MDFT, &body)
}
ItemProperty::Other { box_type, body } => wrap_box(box_type, body),
}
}
pub fn build_ipco_box(properties: &[ItemProperty]) -> Vec<u8> {
let mut body = Vec::new();
for p in properties {
body.extend_from_slice(&build_item_property(p));
}
wrap_box(&IPCO, &body)
}
pub fn build_ipma_box(entries: &[ItemPropertyAssociationEntry]) -> Option<Vec<u8>> {
let mut sorted: Vec<&ItemPropertyAssociationEntry> = entries.iter().collect();
sorted.sort_by_key(|e| e.item_id);
let need_wide_id = sorted.iter().any(|e| e.item_id > 0xFFFF);
let mut need_wide_index = false;
for e in &sorted {
for a in &e.associations {
if a.property_index > 0x7FFF {
return None;
}
if a.property_index > 0x7F {
need_wide_index = true;
}
}
}
let version: u8 = if need_wide_id { 1 } else { 0 };
let flags: u32 = if need_wide_index { 1 } else { 0 };
let mut body = Vec::new();
body.push(version);
body.extend_from_slice(&flags.to_be_bytes()[1..]); body.extend_from_slice(&(sorted.len() as u32).to_be_bytes());
for e in &sorted {
if version == 0 {
body.extend_from_slice(&(e.item_id as u16).to_be_bytes());
} else {
body.extend_from_slice(&e.item_id.to_be_bytes());
}
body.push(e.associations.len() as u8);
for a in &e.associations {
if need_wide_index {
let w = (if a.essential { 0x8000 } else { 0 }) | (a.property_index & 0x7FFF);
body.extend_from_slice(&w.to_be_bytes());
} else {
let b = (if a.essential { 0x80 } else { 0 }) | (a.property_index as u8 & 0x7F);
body.push(b);
}
}
}
Some(wrap_box(&IPMA, &body))
}
pub fn build_iprp_box(p: &ItemProperties) -> Option<Vec<u8>> {
let mut body = build_ipco_box(&p.properties);
if !p.associations.is_empty() {
body.extend_from_slice(&build_ipma_box(&p.associations)?);
}
Some(wrap_box(&IPRP, &body))
}
pub fn build_entity_to_group_box(g: &EntityToGroup) -> Vec<u8> {
let mut body = vec![0u8; 4]; body.extend_from_slice(&g.group_id.to_be_bytes());
body.extend_from_slice(&(g.entity_ids.len() as u32).to_be_bytes());
for &e in &g.entity_ids {
body.extend_from_slice(&e.to_be_bytes());
}
wrap_box(&g.grouping_type, &body)
}
pub fn build_grpl_box(g: &EntityGroups) -> Vec<u8> {
let mut body = Vec::new();
for grp in &g.groups {
body.extend_from_slice(&build_entity_to_group_box(grp));
}
wrap_box(&GRPL, &body)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct MereRelation {
pub first_metabox_handler_type: [u8; 4],
pub second_metabox_handler_type: [u8; 4],
pub metabox_relation: u8,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MecoBox {
pub metas: Vec<MetaItems>,
pub relations: Vec<MereRelation>,
}
impl MecoBox {
pub fn is_empty(&self) -> bool {
self.metas.is_empty() && self.relations.is_empty()
}
}
pub fn parse_mere_box(body: &[u8]) -> Option<MereRelation> {
if body.len() < 13 {
return None;
}
let mut first = [0u8; 4];
let mut second = [0u8; 4];
first.copy_from_slice(&body[4..8]);
second.copy_from_slice(&body[8..12]);
Some(MereRelation {
first_metabox_handler_type: first,
second_metabox_handler_type: second,
metabox_relation: body[12],
})
}
pub fn build_mere_box(r: &MereRelation) -> Vec<u8> {
let mut body = Vec::with_capacity(13);
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&r.first_metabox_handler_type);
body.extend_from_slice(&r.second_metabox_handler_type);
body.push(r.metabox_relation);
wrap_box(&MERE, &body)
}
pub fn parse_meco_box(body: &[u8]) -> MecoBox {
let mut out = MecoBox::default();
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 child = &body[start..start + psz];
match hdr.fourcc {
META => out.metas.push(parse_meta_items(child)),
MERE => {
if let Some(r) = parse_mere_box(child) {
out.relations.push(r);
}
}
_ => {}
}
}
out
}
fn parse_trak(body: &[u8], file_size: u64) -> 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(),
elst_timeline: ElstTimeline::default(),
trex: TrexDefaults::default(),
protection_scheme: None,
tenc: None,
piff_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,
amve: None,
stvi: None,
btrt: None,
pasp: None,
clap: None,
colr: None,
hmhd: None,
gmin: None,
tcmi: None,
tmcd: None,
text_entry: None,
load_settings: None,
rtp_hint: None,
mpeg2ts_hint: None,
hint_stats: crate::hint::HintStatistics::default(),
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, file_size)?;
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)?;
}
LOAD => {
let sub = read_bytes_vec(&mut cur, psz)?;
if t.load_settings.is_none() {
t.load_settings = parse_load_settings(&sub);
}
}
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),
HINF if t.hint_stats.is_empty() => {
t.hint_stats = crate::hint::parse_hinf_box(&body[start..start + psz]);
}
_ => {
}
}
}
}
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,
});
}
pub fn build_cprt_box(language: &[u8; 3], notice: &str) -> Option<Vec<u8>> {
let mut packed: u16 = 0;
for &ch in language {
if !ch.is_ascii_lowercase() {
return None;
}
packed = (packed << 5) | u16::from(ch - 0x60);
}
if notice.as_bytes().contains(&0) {
return None;
}
let mut body = Vec::with_capacity(4 + 2 + notice.len() + 1);
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&packed.to_be_bytes());
body.extend_from_slice(notice.as_bytes());
body.push(0); Some(wrap_box(&crate::boxes::CPRT, &body))
}
pub fn build_kind_box(scheme_uri: &str, value: &str) -> Option<Vec<u8>> {
if scheme_uri.is_empty() || scheme_uri.as_bytes().contains(&0) || value.as_bytes().contains(&0)
{
return None;
}
let mut body = Vec::with_capacity(4 + scheme_uri.len() + value.len() + 2);
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(scheme_uri.as_bytes());
body.push(0);
body.extend_from_slice(value.as_bytes());
body.push(0);
Some(wrap_box(&crate::boxes::KIND, &body))
}
pub fn build_tsel_box(switch_group: i32, attribute_list: &[[u8; 4]]) -> Vec<u8> {
let mut body = Vec::with_capacity(8 + attribute_list.len() * 4);
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&switch_group.to_be_bytes());
for attr in attribute_list {
body.extend_from_slice(attr);
}
wrap_box(&crate::boxes::TSEL, &body)
}
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,
})
}
pub fn build_stsg_box(grouping_type: &[u8; 4], indices: &[u32]) -> Option<Vec<u8>> {
let item_count: u16 = indices.len().try_into().ok()?;
let mut body = Vec::with_capacity(6 + indices.len() * 4);
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(grouping_type);
body.extend_from_slice(&item_count.to_be_bytes());
for &idx in indices {
body.extend_from_slice(&idx.to_be_bytes());
}
Some(wrap_box(&STSG, &body))
}
pub fn build_stri_box(
switch_group: i16,
alternate_group: i16,
sub_track_id: u32,
attribute_list: &[[u8; 4]],
) -> Vec<u8> {
let mut body = Vec::with_capacity(12 + attribute_list.len() * 4);
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&switch_group.to_be_bytes());
body.extend_from_slice(&alternate_group.to_be_bytes());
body.extend_from_slice(&sub_track_id.to_be_bytes());
for attr in attribute_list {
body.extend_from_slice(attr);
}
wrap_box(&STRI, &body)
}
pub fn build_strk_box(
switch_group: i16,
alternate_group: i16,
sub_track_id: u32,
attribute_list: &[[u8; 4]],
sample_groups: &[([u8; 4], Vec<u32>)],
) -> Option<Vec<u8>> {
let mut body = build_stri_box(switch_group, alternate_group, sub_track_id, attribute_list);
if !sample_groups.is_empty() {
let mut strd_body = Vec::new();
for (grouping_type, indices) in sample_groups {
strd_body.extend_from_slice(&build_stsg_box(grouping_type, indices)?);
}
body.extend_from_slice(&wrap_box(&STRD, &strd_body));
}
Some(wrap_box(&STRK, &body))
}
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(())
}
pub fn build_tref_box(refs: &[([u8; 4], Vec<u32>)]) -> Option<Vec<u8>> {
let mut body = Vec::new();
for (ref_type, ids) in refs {
let mut child = Vec::with_capacity(ids.len() * 4);
for &id in ids {
if id == 0 {
return None;
}
child.extend_from_slice(&id.to_be_bytes());
}
body.extend_from_slice(&wrap_box(ref_type, &child));
}
Some(wrap_box(&crate::boxes::TREF, &body))
}
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(())
}
pub fn build_trgr_box(groups: &[([u8; 4], u32)]) -> Vec<u8> {
let mut body = Vec::new();
for (group_type, group_id) in groups {
let mut child = Vec::with_capacity(8);
child.extend_from_slice(&[0u8; 4]); child.extend_from_slice(&group_id.to_be_bytes());
body.extend_from_slice(&wrap_box(group_type, &child));
}
wrap_box(&crate::boxes::TRGR, &body)
}
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<()> {
parse_elst_into(body, &mut t.elst)
}
fn parse_elst_into(body: &[u8], out: &mut Vec<ElstEntry>) -> 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.min(body.len() / 12));
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;
}
*out = entries;
Ok(())
}
fn elst_leading_media_time(elst: &[ElstEntry]) -> i64 {
for e in elst {
if e.media_time != -1 {
return e.media_time;
}
}
0
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct EditListEntry {
pub segment_duration: u64,
pub media_time: i64,
pub media_rate_integer: i16,
pub media_rate_fraction: i16,
}
impl EditListEntry {
pub fn is_empty_edit(&self) -> bool {
self.media_time == -1
}
pub fn is_dwell(&self) -> bool {
self.media_rate_integer == 0 && !self.is_empty_edit()
}
}
fn elst_entry_to_public(e: &ElstEntry) -> EditListEntry {
EditListEntry {
segment_duration: e.segment_duration,
media_time: e.media_time,
media_rate_integer: (e.media_rate >> 16) as i16,
media_rate_fraction: (e.media_rate & 0xFFFF) as i16,
}
}
pub fn parse_elst_box(body: &[u8]) -> Result<Vec<EditListEntry>> {
let mut t_elst: Vec<ElstEntry> = Vec::new();
parse_elst_into(body, &mut t_elst)?;
Ok(t_elst.iter().map(elst_entry_to_public).collect())
}
pub fn build_elst_box(entries: &[EditListEntry]) -> Result<Vec<u8>> {
if entries.is_empty() {
return Err(Error::invalid(
"MP4: elst with no entries (omit the box instead)",
));
}
if let Some(last) = entries.last() {
if last.is_empty_edit() {
return Err(Error::invalid(
"MP4: the last edit in a track shall never be an empty edit (ยง8.6.6.3)",
));
}
}
let mut use_v1 = false;
for e in entries {
if e.media_rate_integer != 0 && e.media_rate_integer != 1 {
return Err(Error::invalid(format!(
"MP4: elst media_rate_integer {} not in {{0, 1}} (ยง8.6.6.3)",
e.media_rate_integer
)));
}
if e.media_time < -1 {
return Err(Error::invalid(format!(
"MP4: elst media_time {} below the -1 empty-edit sentinel",
e.media_time
)));
}
if e.segment_duration > u32::MAX as u64 || e.media_time > i32::MAX as i64 {
use_v1 = true;
}
}
let mut body = Vec::with_capacity(8 + entries.len() * if use_v1 { 20 } else { 12 });
body.push(u8::from(use_v1)); body.extend_from_slice(&[0, 0, 0]); body.extend_from_slice(&(entries.len() as u32).to_be_bytes());
for e in entries {
if use_v1 {
body.extend_from_slice(&e.segment_duration.to_be_bytes());
body.extend_from_slice(&e.media_time.to_be_bytes());
} else {
body.extend_from_slice(&(e.segment_duration as u32).to_be_bytes());
body.extend_from_slice(&(e.media_time as i32).to_be_bytes());
}
body.extend_from_slice(&e.media_rate_integer.to_be_bytes());
body.extend_from_slice(&e.media_rate_fraction.to_be_bytes());
}
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"elst");
out.extend_from_slice(&body);
Ok(out)
}
fn parse_mdia(body: &[u8], t: &mut Track, file_size: u64) -> 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, file_size)?;
}
_ => 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, file_size: u64) -> 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, file_size)?;
}
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);
}
}
}
HMHD => {
let sub = read_bytes_vec(&mut cur, psz)?;
if t.hmhd.is_none() {
if let Ok(h) = parse_hmhd(&sub) {
t.hmhd = Some(h);
}
}
}
GMHD => {
let sub = read_bytes_vec(&mut cur, psz)?;
parse_gmhd(&sub, t);
}
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_hmhd(body: &[u8]) -> Result<HmhdBox> {
if body.len() < 20 {
return Err(Error::invalid("MP4: hmhd too short"));
}
let max_pdu_size = u16::from_be_bytes([body[4], body[5]]);
let avg_pdu_size = u16::from_be_bytes([body[6], body[7]]);
let max_bitrate = u32::from_be_bytes([body[8], body[9], body[10], body[11]]);
let avg_bitrate = u32::from_be_bytes([body[12], body[13], body[14], body[15]]);
Ok(HmhdBox {
max_pdu_size,
avg_pdu_size,
max_bitrate,
avg_bitrate,
})
}
fn parse_gmhd(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(Some(h)) => h,
_ => break,
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
let sub = match read_bytes_vec(&mut cur, psz) {
Ok(s) => s,
Err(_) => break,
};
match hdr.fourcc {
GMIN if t.gmin.is_none() => {
if let Some(g) = parse_gmin(&sub) {
t.gmin = Some(g);
}
}
TCMI if t.tcmi.is_none() => {
if let Some(tc) = parse_tcmi(&sub) {
t.tcmi = Some(tc);
}
}
_ => {}
}
}
}
fn parse_gmin(body: &[u8]) -> Option<GminBox> {
if body.len() < 16 {
return None;
}
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]]),
];
let balance = i16::from_be_bytes([body[12], body[13]]);
Some(GminBox {
graphicsmode,
opcolor,
balance,
})
}
pub fn parse_gmin_box(body: &[u8]) -> Result<GminBox> {
parse_gmin(body).ok_or_else(|| Error::invalid("MP4: gmin too short"))
}
pub fn build_gmin_box(r: &GminBox) -> Vec<u8> {
let mut body = Vec::with_capacity(16);
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&r.graphicsmode.to_be_bytes());
for c in r.opcolor {
body.extend_from_slice(&c.to_be_bytes());
}
body.extend_from_slice(&r.balance.to_be_bytes());
body.extend_from_slice(&[0, 0]); wrap_box(&GMIN, &body)
}
pub fn build_gmhd_box(r: &GminBox) -> Vec<u8> {
wrap_box(&GMHD, &build_gmin_box(r))
}
fn parse_tcmi(body: &[u8]) -> Option<TcmiBox> {
if body.len() < 22 {
return None;
}
let text_font = u16::from_be_bytes([body[4], body[5]]);
let text_face = u16::from_be_bytes([body[6], body[7]]);
let text_size = u16::from_be_bytes([body[8], body[9]]);
let text_color = [
u16::from_be_bytes([body[10], body[11]]),
u16::from_be_bytes([body[12], body[13]]),
u16::from_be_bytes([body[14], body[15]]),
];
let background_color = [
u16::from_be_bytes([body[16], body[17]]),
u16::from_be_bytes([body[18], body[19]]),
u16::from_be_bytes([body[20], body[21]]),
];
let font_name = if body.len() > 22 {
let n = body[22] as usize;
let avail = body.len().saturating_sub(23);
let take = n.min(avail);
String::from_utf8_lossy(&body[23..23 + take]).into_owned()
} else {
String::new()
};
Some(TcmiBox {
text_font,
text_face,
text_size,
text_color,
background_color,
font_name,
})
}
pub fn parse_tcmi_box(body: &[u8]) -> Result<TcmiBox> {
parse_tcmi(body).ok_or_else(|| Error::invalid("MP4: tcmi too short"))
}
pub fn build_tcmi_box(r: &TcmiBox) -> Vec<u8> {
let mut body = Vec::with_capacity(23 + r.font_name.len());
body.extend_from_slice(&[0, 0, 0, 0]); body.extend_from_slice(&r.text_font.to_be_bytes());
body.extend_from_slice(&r.text_face.to_be_bytes());
body.extend_from_slice(&r.text_size.to_be_bytes());
for c in r.text_color {
body.extend_from_slice(&c.to_be_bytes());
}
for c in r.background_color {
body.extend_from_slice(&c.to_be_bytes());
}
let name = r.font_name.as_bytes();
let mut n = name.len().min(255);
while n > 0 && !r.font_name.is_char_boundary(n) {
n -= 1;
}
body.push(n as u8);
body.extend_from_slice(&name[..n]);
wrap_box(&TCMI, &body)
}
fn parse_tmcd_sample_entry(entry: &[u8]) -> Option<TmcdSampleEntry> {
if entry.len() < 25 {
return None;
}
let flags = u32::from_be_bytes([entry[12], entry[13], entry[14], entry[15]]);
let timescale = u32::from_be_bytes([entry[16], entry[17], entry[18], entry[19]]);
let frame_duration = u32::from_be_bytes([entry[20], entry[21], entry[22], entry[23]]);
let number_of_frames = entry[24];
Some(TmcdSampleEntry {
flags,
timescale,
frame_duration,
number_of_frames,
})
}
pub fn parse_tmcd_sample_entry_box(entry: &[u8]) -> Result<TmcdSampleEntry> {
parse_tmcd_sample_entry(entry).ok_or_else(|| Error::invalid("MP4: tmcd sample entry too short"))
}
pub fn build_tmcd_sample_entry(r: &TmcdSampleEntry, data_reference_index: u16) -> Vec<u8> {
let mut body = Vec::with_capacity(26);
body.extend_from_slice(&[0u8; 6]); body.extend_from_slice(&data_reference_index.to_be_bytes());
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&r.flags.to_be_bytes());
body.extend_from_slice(&r.timescale.to_be_bytes());
body.extend_from_slice(&r.frame_duration.to_be_bytes());
body.push(r.number_of_frames);
body.push(0); wrap_box(&TMCD, &body)
}
fn parse_text_sample_entry(entry: &[u8]) -> Option<TextSampleEntry> {
if entry.len() < 51 {
return None;
}
let display_flags = u32::from_be_bytes([entry[8], entry[9], entry[10], entry[11]]);
let text_justification = i32::from_be_bytes([entry[12], entry[13], entry[14], entry[15]]);
let background_color = [
u16::from_be_bytes([entry[16], entry[17]]),
u16::from_be_bytes([entry[18], entry[19]]),
u16::from_be_bytes([entry[20], entry[21]]),
];
let default_text_box = [
i16::from_be_bytes([entry[22], entry[23]]),
i16::from_be_bytes([entry[24], entry[25]]),
i16::from_be_bytes([entry[26], entry[27]]),
i16::from_be_bytes([entry[28], entry[29]]),
];
let font_number = u16::from_be_bytes([entry[38], entry[39]]);
let font_face = u16::from_be_bytes([entry[40], entry[41]]);
let foreground_color = [
u16::from_be_bytes([entry[45], entry[46]]),
u16::from_be_bytes([entry[47], entry[48]]),
u16::from_be_bytes([entry[49], entry[50]]),
];
let text_name = if entry.len() > 51 {
let n = entry[51] as usize;
let avail = entry.len().saturating_sub(52);
let take = n.min(avail);
String::from_utf8_lossy(&entry[52..52 + take]).into_owned()
} else {
String::new()
};
Some(TextSampleEntry {
display_flags,
text_justification,
background_color,
default_text_box,
font_number,
font_face,
foreground_color,
text_name,
})
}
pub fn parse_text_sample_entry_box(entry: &[u8]) -> Result<TextSampleEntry> {
parse_text_sample_entry(entry).ok_or_else(|| Error::invalid("MP4: text sample entry too short"))
}
pub fn build_text_sample_entry(r: &TextSampleEntry, data_reference_index: u16) -> Vec<u8> {
let mut body = Vec::with_capacity(52 + r.text_name.len());
body.extend_from_slice(&[0u8; 6]); body.extend_from_slice(&data_reference_index.to_be_bytes());
body.extend_from_slice(&r.display_flags.to_be_bytes());
body.extend_from_slice(&r.text_justification.to_be_bytes());
for c in r.background_color {
body.extend_from_slice(&c.to_be_bytes());
}
for v in r.default_text_box {
body.extend_from_slice(&v.to_be_bytes());
}
body.extend_from_slice(&[0u8; 8]); body.extend_from_slice(&r.font_number.to_be_bytes());
body.extend_from_slice(&r.font_face.to_be_bytes());
body.push(0); body.extend_from_slice(&[0u8; 2]); for c in r.foreground_color {
body.extend_from_slice(&c.to_be_bytes());
}
let name = r.text_name.as_bytes();
let mut n = name.len().min(255);
while n > 0 && !r.text_name.is_char_boundary(n) {
n -= 1;
}
body.push(n as u8);
body.extend_from_slice(&name[..n]);
wrap_box(b"text", &body)
}
fn parse_load_settings(body: &[u8]) -> Option<LoadSettingsBox> {
if body.len() < 16 {
return None;
}
let preload_start_time = i32::from_be_bytes([body[0], body[1], body[2], body[3]]);
let preload_duration = i32::from_be_bytes([body[4], body[5], body[6], body[7]]);
let preload_flags = i32::from_be_bytes([body[8], body[9], body[10], body[11]]);
let default_hints = i32::from_be_bytes([body[12], body[13], body[14], body[15]]);
Some(LoadSettingsBox {
preload_start_time,
preload_duration,
preload_flags,
default_hints,
})
}
pub fn parse_load_settings_box(body: &[u8]) -> Result<LoadSettingsBox> {
parse_load_settings(body).ok_or_else(|| Error::invalid("MP4: load too short"))
}
pub fn build_load_settings_box(r: &LoadSettingsBox) -> Vec<u8> {
let mut body = Vec::with_capacity(16);
body.extend_from_slice(&r.preload_start_time.to_be_bytes());
body.extend_from_slice(&r.preload_duration.to_be_bytes());
body.extend_from_slice(&r.preload_flags.to_be_bytes());
body.extend_from_slice(&r.default_hints.to_be_bytes());
wrap_box(&LOAD, &body)
}
fn parse_stbl(body: &[u8], t: &mut Track, file_size: u64) -> 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, file_size)?,
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;
t.piff_tenc = unwrap.piff_tenc;
if t.stvi.is_none() {
t.stvi = unwrap.stvi;
}
}
parse_sample_entry(&entry, t)?;
Ok(())
}
struct SinfUnwrap {
original_format: Option<[u8; 4]>,
scheme_type: Option<[u8; 4]>,
tenc: Option<TencBox>,
piff_tenc: Option<PiffTencBox>,
stvi: Option<StviRecord>,
}
impl SinfUnwrap {
fn empty() -> Self {
SinfUnwrap {
original_format: None,
scheme_type: None,
tenc: None,
piff_tenc: None,
stvi: 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 => {
let children = walk_schi(&inner);
out.tenc = children.tenc;
out.piff_tenc = children.piff_tenc;
out.stvi = children.stvi;
}
_ => {}
}
}
Ok(out)
}
#[derive(Default)]
struct SchiChildren {
tenc: Option<TencBox>,
piff_tenc: Option<PiffTencBox>,
stvi: Option<StviRecord>,
}
fn walk_schi(body: &[u8]) -> SchiChildren {
let mut out = SchiChildren::default();
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(Some(h)) => h,
_ => break,
};
let psz = hdr.payload_size().unwrap_or(0) as usize;
let inner = match read_bytes_vec(&mut cur, psz) {
Ok(b) => b,
Err(_) => break,
};
match hdr.fourcc {
TENC if out.tenc.is_none() => out.tenc = parse_tenc(&inner).ok(),
UUID if out.piff_tenc.is_none()
&& inner.len() >= 16
&& inner[..16] == PIFF_TENC_USERTYPE =>
{
out.piff_tenc = parse_piff_tenc(&inner[16..]).ok();
}
STVI if out.stvi.is_none() => out.stvi = parse_stvi(&inner),
_ => {}
}
}
out
}
fn parse_sample_entry(entry: &[u8], t: &mut Track) -> Result<()> {
if entry.len() < 8 {
return Ok(());
}
if matches!(
t.codec_id_fourcc,
RTP_HINT | SRTP_HINT | RRTP_HINT | RTCP_HINT
) {
t.rtp_hint = crate::hint::parse_rtp_hint_sample_entry(t.codec_id_fourcc, entry);
return Ok(());
}
if matches!(t.codec_id_fourcc, SM2T | RM2T) {
t.mpeg2ts_hint = crate::hint::parse_mpeg2ts_hint_sample_entry(t.codec_id_fourcc, entry);
return Ok(());
}
if t.codec_id_fourcc == TMCD {
t.tmcd = parse_tmcd_sample_entry(entry);
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"text" => {
t.text_entry = parse_text_sample_entry(entry);
t.extradata = entry[8..].to_vec();
}
b"tx3g" | 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"alac" if body.len() > 4 => {
t.extradata = body[4..].to_vec();
}
b"dac3" | b"dec3" => t.extradata = body,
b"btrt" if t.btrt.is_none() => {
if let Some(rec) = parse_btrt(&body) {
t.btrt = Some(rec);
}
}
_ => {}
}
}
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"d263" => 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;
}
}
b"amve" if t.amve.is_none() => {
if let Some(rec) = parse_amve(&body) {
t.amve = Some(rec);
}
}
b"btrt" if t.btrt.is_none() => {
if let Some(rec) = parse_btrt(&body) {
t.btrt = Some(rec);
}
}
b"pasp" if t.pasp.is_none() => {
if let Some(rec) = parse_pasp(&body) {
t.pasp = Some(rec);
}
}
b"clap" if t.clap.is_none() => {
if let Some(rec) = parse_clap(&body) {
t.clap = Some(rec);
}
}
b"colr" if t.colr.is_none() => {
if let Some(rec) = parse_colr(&body) {
t.colr = Some(rec);
}
}
b"sinf" if t.stvi.is_none() => {
if let Ok(unwrap) = parse_sinf_body(&body) {
t.stvi = unwrap.stvi;
}
}
_ => {}
}
}
Ok(())
}
fn parse_amve(body: &[u8]) -> Option<AmveRecord> {
if body.len() < 8 {
return None;
}
let ambient_illuminance = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
let ambient_light_x = u16::from_be_bytes([body[4], body[5]]);
let ambient_light_y = u16::from_be_bytes([body[6], body[7]]);
Some(AmveRecord {
ambient_illuminance,
ambient_light_x,
ambient_light_y,
})
}
fn parse_btrt(body: &[u8]) -> Option<BtrtRecord> {
if body.len() < 12 {
return None;
}
let buffer_size_db = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
let max_bitrate = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
let avg_bitrate = u32::from_be_bytes([body[8], body[9], body[10], body[11]]);
Some(BtrtRecord {
buffer_size_db,
max_bitrate,
avg_bitrate,
})
}
pub fn parse_pasp(body: &[u8]) -> Option<PaspRecord> {
if body.len() < 8 {
return None;
}
Some(PaspRecord {
h_spacing: u32::from_be_bytes([body[0], body[1], body[2], body[3]]),
v_spacing: u32::from_be_bytes([body[4], body[5], body[6], body[7]]),
})
}
pub fn parse_clap(body: &[u8]) -> Option<ClapRecord> {
if body.len() < 32 {
return None;
}
let u = |i: usize| u32::from_be_bytes([body[i], body[i + 1], body[i + 2], body[i + 3]]);
Some(ClapRecord {
width_n: u(0),
width_d: u(4),
height_n: u(8),
height_d: u(12),
horiz_off_n: u(16),
horiz_off_d: u(20),
vert_off_n: u(24),
vert_off_d: u(28),
})
}
pub fn parse_colr(body: &[u8]) -> Option<ColrRecord> {
if body.len() < 4 {
return None;
}
let colour_type = [body[0], body[1], body[2], body[3]];
let rest = &body[4..];
match &colour_type {
b"nclx" => {
if rest.len() < 7 {
return None;
}
Some(ColrRecord::Nclx {
colour_primaries: u16::from_be_bytes([rest[0], rest[1]]),
transfer_characteristics: u16::from_be_bytes([rest[2], rest[3]]),
matrix_coefficients: u16::from_be_bytes([rest[4], rest[5]]),
full_range: rest[6] & 0x80 != 0,
})
}
b"rICC" => Some(ColrRecord::RestrictedIcc(rest.to_vec())),
b"prof" => Some(ColrRecord::UnrestrictedIcc(rest.to_vec())),
_ => Some(ColrRecord::Other {
colour_type,
data: rest.to_vec(),
}),
}
}
fn parse_stvi(body: &[u8]) -> Option<StviRecord> {
if body.len() < 16 {
return None;
}
let svw = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
let single_view_allowed = (svw & 0x3) as u8;
let stereo_scheme = u32::from_be_bytes([body[8], body[9], body[10], body[11]]);
let length = u32::from_be_bytes([body[12], body[13], body[14], body[15]]) as usize;
let sit_start: usize = 16;
let sit_end = sit_start.checked_add(length)?;
if sit_end > body.len() {
return None;
}
let stereo_indication_type = body[sit_start..sit_end].to_vec();
Some(StviRecord {
single_view_allowed,
stereo_scheme,
stereo_indication_type,
})
}
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.min(body.len() / 8));
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.min(body.len() / 12));
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], file_size: u64) -> 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 {
if (count as u64).saturating_mul(uniform as u64) > file_size {
return Err(Error::invalid(
"MP4: stsz constant-size sample_count exceeds what the file can back",
));
}
return Ok(vec![uniform; count]);
}
let mut out = Vec::with_capacity(count.min(body.len() / 4));
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.min(body.len().saturating_mul(2)));
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.min(body.len() / 4));
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)
}
}
pub 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;
if (pattern_size_code == 0) != (count_size_code == 0) {
return Err(Error::invalid(
"MP4: csgp pattern_size_code/count_size_code disagree on 4-bit width",
));
}
let gtpp = (flags >> 6) & 0x1 == 1;
let index_msb_indicates_fragment_local_description = (flags >> 7) & 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,
index_msb_indicates_fragment_local_description,
index_field_bits: index_w as u8,
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.min(body.len() / 8));
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,
})
}
pub fn parse_subs_box(body: &[u8]) -> Result<SubsBox> {
parse_subs(body)
}
pub fn build_subs_box(record: &SubsBox) -> Option<Vec<u8>> {
if record.version > 1 {
return None;
}
let entry_count: u32 = record.entries.len().try_into().ok()?;
let mut body = Vec::with_capacity(8 + record.entries.len() * 6);
body.push(record.version);
body.extend_from_slice(&record.flags.to_be_bytes()[1..4]); body.extend_from_slice(&entry_count.to_be_bytes());
for entry in &record.entries {
let subsample_count: u16 = entry.subsamples.len().try_into().ok()?;
body.extend_from_slice(&entry.sample_delta.to_be_bytes());
body.extend_from_slice(&subsample_count.to_be_bytes());
for sub in &entry.subsamples {
if record.version == 0 {
let size: u16 = sub.subsample_size.try_into().ok()?;
body.extend_from_slice(&size.to_be_bytes());
} else {
body.extend_from_slice(&sub.subsample_size.to_be_bytes());
}
body.push(sub.subsample_priority);
body.push(sub.discardable);
body.extend_from_slice(&sub.codec_specific_parameters.to_be_bytes());
}
}
Some(wrap_box(&SUBS, &body))
}
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,
})
}
pub fn parse_saiz_box(body: &[u8]) -> Result<SaizBox> {
parse_saiz(body)
}
pub fn parse_saio_box(body: &[u8]) -> Result<SaioBox> {
parse_saio(body)
}
pub fn build_saiz_box(record: &SaizBox) -> Option<Vec<u8>> {
if record.default_sample_info_size != 0 && !record.per_sample.is_empty() {
return None;
}
if record.default_sample_info_size == 0
&& record.sample_count as usize != record.per_sample.len()
{
return None;
}
let mut body = Vec::new();
let flags: u32 = if record.aux_info_type.is_some() { 1 } else { 0 };
body.push(0); body.extend_from_slice(&flags.to_be_bytes()[1..4]);
if let Some(t) = record.aux_info_type {
body.extend_from_slice(&t);
body.extend_from_slice(&record.aux_info_type_parameter.unwrap_or(0).to_be_bytes());
}
body.push(record.default_sample_info_size);
body.extend_from_slice(&record.sample_count.to_be_bytes());
if record.default_sample_info_size == 0 {
body.extend_from_slice(&record.per_sample);
}
Some(wrap_box(&SAIZ, &body))
}
pub fn build_saio_box(record: &SaioBox) -> Option<Vec<u8>> {
if record.version > 1 {
return None;
}
let entry_count: u32 = record.offsets.len().try_into().ok()?;
let mut body = Vec::new();
let flags: u32 = if record.aux_info_type.is_some() { 1 } else { 0 };
body.push(record.version);
body.extend_from_slice(&flags.to_be_bytes()[1..4]);
if let Some(t) = record.aux_info_type {
body.extend_from_slice(&t);
body.extend_from_slice(&record.aux_info_type_parameter.unwrap_or(0).to_be_bytes());
}
body.extend_from_slice(&entry_count.to_be_bytes());
for &offset in &record.offsets {
if record.version == 0 {
let o: u32 = offset.try_into().ok()?;
body.extend_from_slice(&o.to_be_bytes());
} else {
body.extend_from_slice(&offset.to_be_bytes());
}
}
Some(wrap_box(&SAIO, &body))
}
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.min(body.len() / 4));
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.min(body.len() / 8));
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 PiffSencRecord {
pub track_idx: u32,
pub moof_sequence: u32,
pub senc: PiffSencBox,
}
fn parse_aux_info_run(buf: &[u8], sizes: &[usize], iv_size: usize) -> Option<crate::cenc::SencBox> {
let mut cursor = 0usize;
let mut samples = Vec::with_capacity(sizes.len());
let mut any_subsamples = false;
for &size in sizes {
let cell = buf.get(cursor..cursor.checked_add(size)?)?;
cursor += size;
if size == 0 {
samples.push(crate::cenc::SencSample {
initialization_vector: Vec::new(),
subsamples: Vec::new(),
});
continue;
}
if size < iv_size {
return None;
}
let iv = cell[..iv_size].to_vec();
let mut subsamples = Vec::new();
if size > iv_size {
let rest = &cell[iv_size..];
if rest.len() < 2 {
return None;
}
let n = u16::from_be_bytes([rest[0], rest[1]]) as usize;
if rest.len() != 2 + n * 6 {
return None;
}
subsamples.reserve_exact(n);
for k in 0..n {
let e = &rest[2 + k * 6..2 + k * 6 + 6];
subsamples.push(crate::cenc::SubsampleEntry {
bytes_of_clear_data: u16::from_be_bytes([e[0], e[1]]),
bytes_of_protected_data: u32::from_be_bytes([e[2], e[3], e[4], e[5]]),
});
}
any_subsamples = true;
}
samples.push(crate::cenc::SencSample {
initialization_vector: iv,
subsamples,
});
}
Some(crate::cenc::SencBox {
flags: if any_subsamples { 0x0000_0002 } else { 0 },
samples,
})
}
#[derive(Clone, Copy, Debug)]
pub struct EmptyDurationRecord {
pub track_idx: u32,
pub moof_sequence: u32,
pub duration: u32,
}
#[derive(Clone, Debug)]
pub struct SaiRecord {
pub track_idx: u32,
pub moof_sequence: u32,
pub saiz: Vec<TrafSaiz>,
pub saio: Vec<TrafSaio>,
pub base_data_offset: u64,
}
#[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,
}
#[derive(Clone, Debug)]
pub struct TrafSampleGroupRecord {
pub track_idx: u32,
pub moof_sequence: u32,
pub sgpd: Vec<SgpdBox>,
pub sbgp: Vec<SbgpBox>,
pub csgp: Vec<CsgpBox>,
}
#[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>,
traf_sample_groups: &mut Vec<TrafSampleGroupRecord>,
piff_senc_records: &mut Vec<PiffSencRecord>,
piff_moof_psshes: &mut Vec<MoofPsshRecord>,
empty_durations: &mut Vec<EmptyDurationRecord>,
sample_budget: &mut u64,
) -> 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();
let mut pending_piff_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,
traf_sample_groups,
piff_senc_records,
empty_durations,
sample_budget,
)?;
}
PSSH => {
let body = read_bytes_vec(&mut cur, psz)?;
pending_pssh_bodies.push(body);
}
UUID => {
let body = read_bytes_vec(&mut cur, psz)?;
if body.len() >= 16 && body[..16] == PIFF_PSSH_USERTYPE {
pending_piff_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,
});
}
}
for body in pending_piff_pssh_bodies {
if let Ok(pssh) = parse_piff_pssh(&body[16..]) {
piff_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>,
sample_description_index: u32,
}
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;
const TFHD_DURATION_IS_EMPTY: u32 = 0x010000;
#[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>,
traf_sample_groups: &mut Vec<TrafSampleGroupRecord>,
piff_senc_records: &mut Vec<PiffSencRecord>,
empty_durations: &mut Vec<EmptyDurationRecord>,
sample_budget: &mut u64,
) -> Result<()> {
let mut state = TrafState::default();
let mut tfhd_seen = false;
let mut senc_body: Option<Vec<u8>> = None;
let mut piff_senc_body: Option<Vec<u8>> = None;
let mut frag_saiz: Vec<TrafSaiz> = Vec::new();
let mut frag_saio: Vec<TrafSaio> = Vec::new();
let mut frag_sgpd: Vec<SgpdBox> = Vec::new();
let mut frag_sbgp: Vec<SbgpBox> = Vec::new();
let mut frag_csgp: Vec<CsgpBox> = 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)?);
}
UUID => {
let b = read_bytes_vec(&mut cur, psz)?;
if b.len() >= 16 && b[..16] == PIFF_SENC_USERTYPE {
piff_senc_body = Some(b[16..].to_vec());
}
}
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,
});
}
SGPD => {
let b = read_bytes_vec(&mut cur, psz)?;
if let Ok(g) = parse_sgpd(&b) {
frag_sgpd.push(g);
}
}
SBGP => {
let b = read_bytes_vec(&mut cur, psz)?;
if let Ok(g) = parse_sbgp(&b) {
frag_sbgp.push(g);
}
}
CSGP => {
let b = read_bytes_vec(&mut cur, psz)?;
if let Ok(g) = parse_csgp(&b) {
frag_csgp.push(g);
}
}
TRUN => skip_cursor_bytes(&mut cur, psz),
_ => skip_cursor_bytes(&mut cur, psz),
}
}
if !tfhd_seen {
return Err(Error::invalid("MP4: traf missing tfhd"));
}
if state.tfhd_flags & TFHD_DURATION_IS_EMPTY != 0 {
let start = state
.base_media_decode_time
.unwrap_or(next_dts[state.track_idx]);
empty_durations.push(EmptyDurationRecord {
track_idx: state.track_idx as u32,
moof_sequence,
duration: state.default_sample_duration,
});
next_dts[state.track_idx] = start.saturating_add(state.default_sample_duration as i64);
return Ok(());
}
let track = &tracks[state.track_idx];
let mut standard_senc_recorded = false;
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,
});
standard_senc_recorded = true;
}
}
}
if let Some(body) = piff_senc_body {
let default_iv_size = track
.piff_tenc
.as_ref()
.map(|p| p.iv_size)
.or_else(|| track.tenc.as_ref().map(|t| t.default_per_sample_iv_size))
.unwrap_or(0);
if let Ok(piff) = parse_piff_senc(&body, default_iv_size) {
if !standard_senc_recorded {
senc_records.push(SencRecord {
track_idx: state.track_idx as u32,
moof_sequence,
senc: piff.to_senc(),
});
}
piff_senc_records.push(PiffSencRecord {
track_idx: state.track_idx as u32,
moof_sequence,
senc: piff,
});
}
}
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,
base_data_offset: state.base_data_offset,
});
}
if !frag_sgpd.is_empty() || !frag_sbgp.is_empty() || !frag_csgp.is_empty() {
traf_sample_groups.push(TrafSampleGroupRecord {
track_idx: state.track_idx as u32,
moof_sequence,
sgpd: frag_sgpd,
sbgp: frag_sbgp,
csgp: frag_csgp,
});
}
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, sample_budget)?;
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 media_cts = frag_dts.saturating_add(cts_off);
let (elst_delta, presented) = track.elst_timeline.delta_for_cts(media_cts);
let dts_v = frag_dts.saturating_add(elst_delta);
let cts_v = media_cts.saturating_add(elst_delta);
samples.push(SampleRef {
track_idx: state.track_idx as u32,
offset: sample_off,
size,
pts: cts_v,
dts: dts_v,
duration: dur,
keyframe,
discard: !presented,
sdi: state.sample_description_index,
});
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.sample_description_index = trex.default_sample_description_index;
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",
));
}
state.sample_description_index =
u32::from_be_bytes([body[off], body[off + 1], body[off + 2], body[off + 3]]);
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], sample_budget: &mut u64) -> 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;
if sample_count as u64 > *sample_budget {
return Err(Error::invalid(
"MP4: trun sample_count exceeds input-backed sample budget",
));
}
let mut off = 8usize;
let mut out = ParsedTrun::default();
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"));
}
*sample_budget -= sample_count as u64;
out.samples.reserve(sample_count);
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;
let assp = if &fourcc == b"assp" {
parse_assp(&payload[off + header_len..off + effective]).ok()
} else {
None
};
children.push(TrepChild {
fourcc,
payload_len,
assp,
});
off += effective;
if effective < box_size {
break;
}
}
Ok(TrepRecord { track_id, children })
}
fn parse_assp(body: &[u8]) -> Result<AsspRecord> {
if body.len() < 4 {
return Err(Error::invalid("MP4: assp too short for FullBox preamble"));
}
let version = body[0];
let payload = &body[4..];
match version {
0 => {
if payload.len() < 4 {
return Err(Error::invalid("MP4: assp v0 truncated offset"));
}
let off = i32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
Ok(AsspRecord {
version,
entries: vec![AsspEntry {
grouping_type_parameter: None,
min_initial_alt_startup_offset: off,
}],
})
}
1 => {
if payload.len() < 4 {
return Err(Error::invalid("MP4: assp v1 truncated num_entries"));
}
let num_entries =
u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]) as usize;
let entries_bytes = &payload[4..];
if num_entries.saturating_mul(8) > entries_bytes.len() {
return Err(Error::invalid("MP4: assp v1 num_entries overruns body"));
}
let mut entries = Vec::with_capacity(num_entries);
let mut p = 0usize;
for _ in 0..num_entries {
let gtp = u32::from_be_bytes([
entries_bytes[p],
entries_bytes[p + 1],
entries_bytes[p + 2],
entries_bytes[p + 3],
]);
let off = i32::from_be_bytes([
entries_bytes[p + 4],
entries_bytes[p + 5],
entries_bytes[p + 6],
entries_bytes[p + 7],
]);
entries.push(AsspEntry {
grouping_type_parameter: Some(gtp),
min_initial_alt_startup_offset: off,
});
p += 8;
}
Ok(AsspRecord { version, entries })
}
v => Err(Error::invalid(format!("MP4: assp unsupported version {v}"))),
}
}
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 flags = u32::from_be_bytes([0, body[1], body[2], body[3]]);
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,
flags,
}))
}
fn parse_mfra(body: &[u8], out: &mut Vec<TfraRecord>, mfro_size: &mut Option<u32>) -> 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 => {
let b = read_bytes_vec(&mut cur, psz)?;
if b.len() >= 8 && mfro_size.is_none() {
*mfro_size = Some(u32::from_be_bytes([b[4], b[5], b[6], b[7]]));
}
}
_ => 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 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"));
}
let mut entries = Vec::with_capacity(n);
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,
discard: bool,
sdi: u32,
}
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 sdi_of_sample: Vec<u32> = 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);
sdi_of_sample.push(sdi);
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);
sdi_of_sample.push(*sdi_of_sample.last().unwrap_or(&1));
}
}
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 media_cts = dts_v.saturating_add(cts_offsets[i]);
let (elst_delta, presented) = t.elst_timeline.delta_for_cts(media_cts);
let cts_v = media_cts.saturating_add(elst_delta);
let dts_v_shifted = dts_v.saturating_add(elst_delta);
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,
discard: !presented,
sdi: sdi_of_sample[i],
});
}
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(piff) = &t.piff_tenc {
let kid_hex: String = piff.kid.iter().map(|b| format!("{b:02x}")).collect();
params
.options
.insert("piff_algorithm_id", piff.algorithm_id.to_string());
params
.options
.insert("piff_iv_size", piff.iv_size.to_string());
params.options.insert("piff_kid", kid_hex);
}
if let Some(tag) = &t.elng {
params.options.insert("language", tag.clone());
}
if !t.elst.is_empty() {
params
.options
.insert("elst_entry_count", t.elst.len().to_string());
for (i, e) in t.elst.iter().enumerate() {
let p = elst_entry_to_public(e);
params.options.insert(
format!("elst_{}", i),
format!(
"dur={} media_time={} rate={}",
p.segment_duration, p.media_time, p.media_rate_integer
),
);
}
}
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(g) = &t.gmin {
params
.options
.insert("gmin_graphicsmode", g.graphicsmode.to_string());
params.options.insert(
"gmin_opcolor",
format!("{} {} {}", g.opcolor[0], g.opcolor[1], g.opcolor[2]),
);
params.options.insert("gmin_balance", g.balance.to_string());
}
if let Some(tc) = &t.tcmi {
params
.options
.insert("tcmi_text_font", tc.text_font.to_string());
params
.options
.insert("tcmi_text_face", tc.text_face.to_string());
params
.options
.insert("tcmi_text_size", tc.text_size.to_string());
params.options.insert(
"tcmi_text_color",
format!(
"{} {} {}",
tc.text_color[0], tc.text_color[1], tc.text_color[2]
),
);
params.options.insert(
"tcmi_background_color",
format!(
"{} {} {}",
tc.background_color[0], tc.background_color[1], tc.background_color[2]
),
);
if !tc.font_name.is_empty() {
params
.options
.insert("tcmi_font_name", tc.font_name.clone());
}
}
if let Some(tm) = &t.tmcd {
params.options.insert("tmcd_flags", tm.flags.to_string());
params
.options
.insert("tmcd_drop_frame", tm.drop_frame().to_string());
params
.options
.insert("tmcd_24hour_max", tm.twenty_four_hour_max().to_string());
params
.options
.insert("tmcd_negative_ok", tm.negative_times_ok().to_string());
params
.options
.insert("tmcd_counter", tm.counter().to_string());
params
.options
.insert("tmcd_timescale", tm.timescale.to_string());
params
.options
.insert("tmcd_frame_duration", tm.frame_duration.to_string());
params
.options
.insert("tmcd_number_of_frames", tm.number_of_frames.to_string());
}
if let Some(tx) = &t.text_entry {
params
.options
.insert("text_display_flags", tx.display_flags.to_string());
params
.options
.insert("text_justification", tx.text_justification.to_string());
params.options.insert(
"text_background_color",
format!(
"{} {} {}",
tx.background_color[0], tx.background_color[1], tx.background_color[2]
),
);
params.options.insert(
"text_foreground_color",
format!(
"{} {} {}",
tx.foreground_color[0], tx.foreground_color[1], tx.foreground_color[2]
),
);
params.options.insert(
"text_default_text_box",
format!(
"{} {} {} {}",
tx.default_text_box[0],
tx.default_text_box[1],
tx.default_text_box[2],
tx.default_text_box[3]
),
);
params
.options
.insert("text_font_number", tx.font_number.to_string());
params
.options
.insert("text_font_face", tx.font_face.to_string());
if !tx.text_name.is_empty() {
params
.options
.insert("text_font_name", tx.text_name.clone());
}
}
if let Some(ls) = &t.load_settings {
params
.options
.insert("load_preload_start_time", ls.preload_start_time.to_string());
params
.options
.insert("load_preload_duration", ls.preload_duration.to_string());
params
.options
.insert("load_preload_flags", ls.preload_flags.to_string());
params
.options
.insert("load_default_hints", ls.default_hints.to_string());
params
.options
.insert("load_preload_always", ls.preload_always().to_string());
params.options.insert(
"load_preload_if_enabled",
ls.preload_if_enabled().to_string(),
);
params
.options
.insert("load_double_buffer", ls.double_buffer().to_string());
params
.options
.insert("load_high_quality", ls.high_quality().to_string());
}
if let Some(a) = &t.amve {
params.options.insert(
"amve_ambient_illuminance",
a.ambient_illuminance.to_string(),
);
params
.options
.insert("amve_ambient_light_x", a.ambient_light_x.to_string());
params
.options
.insert("amve_ambient_light_y", a.ambient_light_y.to_string());
}
if let Some(s) = &t.stvi {
params.options.insert(
"stvi_single_view_allowed",
s.single_view_allowed.to_string(),
);
params
.options
.insert("stvi_stereo_scheme", s.stereo_scheme.to_string());
if !s.stereo_indication_type.is_empty() {
let mut hex = String::with_capacity(s.stereo_indication_type.len() * 2);
for b in &s.stereo_indication_type {
use std::fmt::Write as _;
let _ = write!(hex, "{b:02x}");
}
params.options.insert("stvi_indication", hex);
}
}
if let Some(b) = &t.btrt {
params
.options
.insert("btrt_buffer_size_db", b.buffer_size_db.to_string());
params
.options
.insert("btrt_max_bitrate", b.max_bitrate.to_string());
params
.options
.insert("btrt_avg_bitrate", b.avg_bitrate.to_string());
}
if let Some(p) = &t.pasp {
params
.options
.insert("pasp_h_spacing", p.h_spacing.to_string());
params
.options
.insert("pasp_v_spacing", p.v_spacing.to_string());
}
if let Some(c) = &t.clap {
params
.options
.insert("clap_width", format!("{}/{}", c.width_n, c.width_d));
params
.options
.insert("clap_height", format!("{}/{}", c.height_n, c.height_d));
params.options.insert(
"clap_horiz_off",
format!("{}/{}", c.horiz_off_n, c.horiz_off_d),
);
params.options.insert(
"clap_vert_off",
format!("{}/{}", c.vert_off_n, c.vert_off_d),
);
}
if let Some(c) = &t.colr {
match c {
ColrRecord::Nclx {
colour_primaries,
transfer_characteristics,
matrix_coefficients,
full_range,
} => {
params.options.insert("colr_type", "nclx".to_string());
params
.options
.insert("colr_primaries", colour_primaries.to_string());
params
.options
.insert("colr_transfer", transfer_characteristics.to_string());
params
.options
.insert("colr_matrix", matrix_coefficients.to_string());
params
.options
.insert("colr_full_range", (*full_range as u8).to_string());
}
ColrRecord::RestrictedIcc(d) => {
params.options.insert("colr_type", "rICC".to_string());
params.options.insert("colr_icc_len", d.len().to_string());
}
ColrRecord::UnrestrictedIcc(d) => {
params.options.insert("colr_type", "prof".to_string());
params.options.insert("colr_icc_len", d.len().to_string());
}
ColrRecord::Other { colour_type, .. } => {
params.options.insert(
"colr_type",
String::from_utf8_lossy(colour_type).to_string(),
);
}
}
}
if let Some(h) = &t.hmhd {
params
.options
.insert("hmhd_max_pdu_size", h.max_pdu_size.to_string());
params
.options
.insert("hmhd_avg_pdu_size", h.avg_pdu_size.to_string());
params
.options
.insert("hmhd_max_bitrate", h.max_bitrate.to_string());
params
.options
.insert("hmhd_avg_bitrate", h.avg_bitrate.to_string());
}
if let Some(r) = &t.rtp_hint {
params.options.insert(
"rtp_hint_format",
String::from_utf8_lossy(&r.format).to_string(),
);
params
.options
.insert("rtp_hint_max_packet_size", r.max_packet_size.to_string());
if let Some(ts) = r.timescale {
params.options.insert("rtp_hint_timescale", ts.to_string());
}
if let Some(off) = r.time_offset {
params
.options
.insert("rtp_hint_time_offset", off.to_string());
}
if let Some(off) = r.sequence_offset {
params
.options
.insert("rtp_hint_sequence_offset", off.to_string());
}
if r.srpp.is_some() {
params.options.insert("rtp_hint_srtp", "true".to_string());
}
}
if let Some(m) = &t.mpeg2ts_hint {
params.options.insert(
"m2t_hint_format",
String::from_utf8_lossy(&m.format).to_string(),
);
params.options.insert(
"m2t_hint_preceding_bytes",
m.preceding_bytes_len.to_string(),
);
params
.options
.insert("m2t_hint_trailing_bytes", m.trailing_bytes_len.to_string());
if m.precomputed_only {
params
.options
.insert("m2t_hint_precomputed", "true".to_string());
}
}
{
let h = &t.hint_stats;
if let Some(v) = h
.bytes_sent_with_rtp_64
.or(h.bytes_sent_with_rtp_32.map(u64::from))
{
params.options.insert("hinf_bytes_sent", v.to_string());
}
if let Some(v) = h.packets_sent_64.or(h.packets_sent_32.map(u64::from)) {
params.options.insert("hinf_packets_sent", v.to_string());
}
if let Some(v) = h
.bytes_sent_no_rtp_64
.or(h.bytes_sent_no_rtp_32.map(u64::from))
{
params.options.insert("hinf_payload_bytes", v.to_string());
}
if let Some(v) = h.media_bytes_sent {
params.options.insert("hinf_media_bytes", v.to_string());
}
if let Some(v) = h.immediate_bytes_sent {
params.options.insert("hinf_immediate_bytes", v.to_string());
}
if let Some(v) = h.repeated_bytes_sent {
params.options.insert("hinf_repeated_bytes", v.to_string());
}
if let Some(v) = h.largest_packet {
params.options.insert("hinf_largest_packet", v.to_string());
}
if !h.max_rates.is_empty() {
params
.options
.insert("hinf_maxr_count", h.max_rates.len().to_string());
}
if !h.payload_ids.is_empty() {
params
.options
.insert("hinf_payload_count", h.payload_ids.len().to_string());
}
}
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));
}
if cg.index_msb_indicates_fragment_local_description {
v.push_str(" fraglocal");
}
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 };
let start_time = if t.elst_timeline.mapped {
t.elst_timeline
.segments
.first()
.map(|s| s.pres_start)
.unwrap_or(0)
} else {
0
};
StreamInfo {
index,
time_base: TimeBase::new(1, timescale as i64),
duration: t.duration.map(|d| d as i64),
start_time: Some(start_time),
params,
}
}
pub struct Mp4Demuxer {
input: Box<dyn ReadSeek>,
input_len: u64,
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)]
pnot: Option<PnotRecord>,
#[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)]
empty_durations: Vec<EmptyDurationRecord>,
track_tenc_iv_sizes: Vec<Option<u8>>,
#[allow(dead_code)]
traf_sample_groups: Vec<TrafSampleGroupRecord>,
#[allow(dead_code)]
piff_psshes: Vec<PsshBox>,
#[allow(dead_code)]
piff_moof_psshes: Vec<MoofPsshRecord>,
#[allow(dead_code)]
piff_senc_records: Vec<PiffSencRecord>,
#[allow(dead_code)]
piff_tencs: Vec<Option<PiffTencBox>>,
#[allow(dead_code)]
emsgs: Vec<EmsgRecord>,
#[allow(dead_code)]
moof_earliest: Vec<Option<(i64, u32)>>,
#[allow(dead_code)]
meta_items: MetaItems,
#[allow(dead_code)]
meco: MecoBox,
#[allow(dead_code)]
movie_timescale: u32,
track_timescales: Vec<u32>,
track_ids: Vec<u32>,
edit_lists: Vec<Vec<EditListEntry>>,
last_sdi: Option<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 pnot(&self) -> Option<&PnotRecord> {
self.pnot.as_ref()
}
#[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
}
pub fn resolve_sai_aux_info(&mut self) -> Result<usize> {
use std::io::SeekFrom;
let have_senc: Vec<(u32, u32)> = self
.senc_records
.iter()
.map(|r| (r.track_idx, r.moof_sequence))
.collect();
let saved_pos = self.input.stream_position()?;
let mut new_records: Vec<SencRecord> = Vec::new();
for r in &self.sai_records {
if have_senc.contains(&(r.track_idx, r.moof_sequence)) {
continue;
}
let Some(iv_size) = self
.track_tenc_iv_sizes
.get(r.track_idx as usize)
.copied()
.flatten()
else {
continue;
};
for sz in &r.saiz {
let is_cenc_type = match &sz.aux_info_type {
None => true,
Some(t) => matches!(t, b"cenc" | b"cbc1" | b"cens" | b"cbcs"),
};
if !is_cenc_type {
continue;
}
let Some(so) = r.saio.iter().find(|so| {
so.aux_info_type == sz.aux_info_type
&& so.aux_info_type_parameter == sz.aux_info_type_parameter
}) else {
continue;
};
if so.offsets.len() != 1 {
continue;
}
let count = sz.sample_count as usize;
const MAX_AUX_TOTAL: usize = 16 * 1024 * 1024;
let sizes: Vec<usize> = if sz.default_sample_info_size != 0 {
let cell = sz.default_sample_info_size as usize;
match count.checked_mul(cell) {
Some(total) if total <= MAX_AUX_TOTAL => {}
_ => continue,
}
vec![cell; count]
} else {
if sz.per_sample.len() < count {
continue;
}
sz.per_sample[..count].iter().map(|&b| b as usize).collect()
};
let total: usize = sizes.iter().sum();
if total == 0 || total > MAX_AUX_TOTAL {
continue;
}
let Some(abs) = r.base_data_offset.checked_add(so.offsets[0]) else {
continue;
};
self.input.seek(SeekFrom::Start(abs))?;
let mut buf = vec![0u8; total];
if self.input.read_exact(&mut buf).is_err() {
continue;
}
if let Some(senc) = parse_aux_info_run(&buf, &sizes, iv_size as usize) {
new_records.push(SencRecord {
track_idx: r.track_idx,
moof_sequence: r.moof_sequence,
senc,
});
break; }
}
}
self.input.seek(SeekFrom::Start(saved_pos))?;
let added = new_records.len();
self.senc_records.extend(new_records);
Ok(added)
}
pub fn empty_duration_records(&self) -> &[EmptyDurationRecord] {
&self.empty_durations
}
#[allow(dead_code)]
pub fn traf_sample_groups(&self) -> &[TrafSampleGroupRecord] {
&self.traf_sample_groups
}
#[allow(dead_code)]
pub fn piff_psshes(&self) -> &[PsshBox] {
&self.piff_psshes
}
#[allow(dead_code)]
pub fn piff_moof_psshes(&self) -> &[MoofPsshRecord] {
&self.piff_moof_psshes
}
#[allow(dead_code)]
pub fn piff_senc_records(&self) -> &[PiffSencRecord] {
&self.piff_senc_records
}
#[allow(dead_code)]
pub fn piff_tencs(&self) -> &[Option<PiffTencBox>] {
&self.piff_tencs
}
#[allow(dead_code)]
pub fn emsgs(&self) -> &[EmsgRecord] {
&self.emsgs
}
#[allow(dead_code)]
pub fn emsg_absolute_time(&self, record: &EmsgRecord) -> Option<u64> {
match record.emsg.presentation {
crate::emsg::EmsgTime::Absolute(t) => Some(t),
crate::emsg::EmsgTime::Delta(delta) => {
let (pts, track_ts) = (*self.moof_earliest.get(record.next_moof_index)?)?;
if track_ts == 0 || record.emsg.timescale == 0 {
return None;
}
let anchor = (pts as i128) * (record.emsg.timescale as i128) / (track_ts as i128);
u64::try_from(anchor + delta as i128).ok()
}
}
}
#[allow(dead_code)]
pub fn meta_items(&self) -> &MetaItems {
&self.meta_items
}
#[allow(dead_code)]
pub fn meco(&self) -> &MecoBox {
&self.meco
}
#[allow(dead_code)]
pub fn edit_list(&self, stream_index: u32) -> &[EditListEntry] {
self.edit_lists
.get(stream_index as usize)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
#[allow(dead_code)]
pub fn sample_description_index_of_last_packet(&self) -> Option<u32> {
self.last_sdi
}
}
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.last_sdi = Some(s.sdi);
match s.offset.checked_add(s.size as u64) {
Some(end) if end <= self.input_len => {}
_ => {
return Err(Error::invalid(format!(
"MP4: sample at offset {} with size {} exceeds input length {}",
s.offset, s.size, self.input_len
)));
}
}
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;
pkt.flags.discard = s.discard;
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, &mut None)?;
Ok(out)
}
pub fn parse_prft_box(body: &[u8]) -> Result<Option<PrftRecord>> {
parse_prft(body)
}
pub fn build_prft_box(record: &PrftRecord) -> Option<Vec<u8>> {
if record.version > 1 {
return None;
}
let mut body = Vec::with_capacity(24);
body.push(record.version);
body.extend_from_slice(&record.flags.to_be_bytes()[1..4]);
body.extend_from_slice(&record.reference_track_id.to_be_bytes());
body.extend_from_slice(&record.ntp_timestamp.to_be_bytes());
if record.version == 0 {
let mt: u32 = record.media_time.try_into().ok()?;
body.extend_from_slice(&mt.to_be_bytes());
} else {
body.extend_from_slice(&record.media_time.to_be_bytes());
}
Some(wrap_box(&crate::boxes::PRFT, &body))
}
pub fn parse_pdin_box(body: &[u8]) -> Result<PdinRecord> {
parse_pdin(body)
}
pub fn build_pdin_box(record: &PdinRecord) -> Vec<u8> {
let mut body = Vec::with_capacity(4 + record.entries.len() * 8);
body.extend_from_slice(&[0u8; 4]); for e in &record.entries {
body.extend_from_slice(&e.rate.to_be_bytes());
body.extend_from_slice(&e.initial_delay.to_be_bytes());
}
wrap_box(&crate::boxes::PDIN, &body)
}
fn parse_pnot(body: &[u8]) -> Option<PnotRecord> {
if body.len() < 12 {
return None;
}
let modification_date = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
let version = u16::from_be_bytes([body[4], body[5]]);
let atom_type = [body[6], body[7], body[8], body[9]];
let atom_index = u16::from_be_bytes([body[10], body[11]]);
Some(PnotRecord {
modification_date,
version,
atom_type,
atom_index,
})
}
pub fn parse_pnot_box(body: &[u8]) -> Result<PnotRecord> {
parse_pnot(body).ok_or_else(|| Error::invalid("MP4: pnot too short"))
}
pub fn build_pnot_box(r: &PnotRecord) -> Vec<u8> {
let mut body = Vec::with_capacity(12);
body.extend_from_slice(&r.modification_date.to_be_bytes());
body.extend_from_slice(&r.version.to_be_bytes());
body.extend_from_slice(&r.atom_type);
body.extend_from_slice(&r.atom_index.to_be_bytes());
wrap_box(&crate::boxes::PNOT, &body)
}
pub fn parse_leva_box(body: &[u8]) -> Result<LevaRecord> {
parse_leva(body)
}
pub fn build_leva_box(record: &LevaRecord) -> Result<Vec<u8>> {
let mut body = Vec::with_capacity(8 + record.entries.len() * 9);
body.extend_from_slice(&[0u8; 4]); if record.entries.len() > u8::MAX as usize {
return Err(Error::invalid("MP4: leva level_count exceeds 255"));
}
body.push(record.entries.len() as u8);
for e in &record.entries {
if e.assignment_type > 0x7f {
return Err(Error::invalid(
"MP4: leva assignment_type exceeds the 7-bit field",
));
}
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()),
_ => {}
}
}
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"leva");
out.extend_from_slice(&body);
Ok(out)
}
pub fn parse_csgp_box(body: &[u8]) -> Result<CsgpBox> {
parse_csgp(body)
}
pub fn parse_trep_box(body: &[u8]) -> Result<TrepRecord> {
parse_trep(body)
}
pub fn build_trep_box(record: &TrepRecord) -> Result<Vec<u8>> {
let mut body = Vec::new();
body.extend_from_slice(&[0u8; 4]); body.extend_from_slice(&record.track_id.to_be_bytes());
for child in &record.children {
match &child.assp {
Some(a) => {
if &child.fourcc != b"assp" {
return Err(Error::invalid(
"MP4: trep child carries an AsspRecord but its fourcc is not 'assp'",
));
}
body.extend_from_slice(&build_assp_box(a)?);
}
None => {
if child.payload_len != 0 {
return Err(Error::invalid(
"MP4: trep child without a typed record must have payload_len == 0",
));
}
body.extend_from_slice(&8u32.to_be_bytes());
body.extend_from_slice(&child.fourcc);
}
}
}
let total = 8 + body.len();
let mut out = Vec::with_capacity(total);
out.extend_from_slice(&(total as u32).to_be_bytes());
out.extend_from_slice(b"trep");
out.extend_from_slice(&body);
Ok(out)
}
pub fn parse_assp_box(body: &[u8]) -> Result<AsspRecord> {
parse_assp(body)
}
pub fn build_assp_box(record: &AsspRecord) -> Result<Vec<u8>> {
let body: Vec<u8> = match record.version {
0 => {
if record.entries.len() != 1 {
return Err(Error::invalid("MP4: assp v0 must carry exactly one entry"));
}
let e = &record.entries[0];
if e.grouping_type_parameter.is_some() {
return Err(Error::invalid(
"MP4: assp v0 entry must not carry grouping_type_parameter",
));
}
let mut b = Vec::with_capacity(4 + 4);
b.extend_from_slice(&[0, 0, 0, 0]); b.extend_from_slice(&e.min_initial_alt_startup_offset.to_be_bytes());
b
}
1 => {
let num_entries = record.entries.len();
if num_entries > u32::MAX as usize {
return Err(Error::invalid("MP4: assp v1 num_entries exceeds u32"));
}
let mut b = Vec::with_capacity(4 + 4 + num_entries * 8);
b.extend_from_slice(&[1, 0, 0, 0]); b.extend_from_slice(&(num_entries as u32).to_be_bytes());
for e in &record.entries {
let gtp = e.grouping_type_parameter.unwrap_or(0);
b.extend_from_slice(>p.to_be_bytes());
b.extend_from_slice(&e.min_initial_alt_startup_offset.to_be_bytes());
}
b
}
v => {
return Err(Error::invalid(format!("MP4: assp unsupported version {v}")));
}
};
let total = 8 + body.len();
let mut out = Vec::with_capacity(total);
out.extend_from_slice(&(total as u32).to_be_bytes());
out.extend_from_slice(b"assp");
out.extend_from_slice(&body);
Ok(out)
}
pub fn parse_amve_box(body: &[u8]) -> Result<AmveRecord> {
parse_amve(body).ok_or_else(|| Error::invalid("MP4: amve too short"))
}
pub fn parse_btrt_box(body: &[u8]) -> Result<BtrtRecord> {
parse_btrt(body).ok_or_else(|| Error::invalid("MP4: btrt too short"))
}
pub fn parse_pasp_box(body: &[u8]) -> Result<PaspRecord> {
parse_pasp(body).ok_or_else(|| Error::invalid("MP4: pasp too short"))
}
pub fn parse_clap_box(body: &[u8]) -> Result<ClapRecord> {
parse_clap(body).ok_or_else(|| Error::invalid("MP4: clap too short"))
}
pub fn parse_colr_box(body: &[u8]) -> Result<ColrRecord> {
parse_colr(body).ok_or_else(|| Error::invalid("MP4: colr malformed"))
}
pub fn build_pasp_box(r: &PaspRecord) -> Vec<u8> {
let mut body = r.h_spacing.to_be_bytes().to_vec();
body.extend_from_slice(&r.v_spacing.to_be_bytes());
wrap_box(b"pasp", &body)
}
pub fn build_clap_box(r: &ClapRecord) -> Vec<u8> {
let mut body = Vec::with_capacity(32);
for v in [
r.width_n,
r.width_d,
r.height_n,
r.height_d,
r.horiz_off_n,
r.horiz_off_d,
r.vert_off_n,
r.vert_off_d,
] {
body.extend_from_slice(&v.to_be_bytes());
}
wrap_box(b"clap", &body)
}
pub fn build_colr_box(r: &ColrRecord) -> Vec<u8> {
let mut body = Vec::new();
match r {
ColrRecord::Nclx {
colour_primaries,
transfer_characteristics,
matrix_coefficients,
full_range,
} => {
body.extend_from_slice(b"nclx");
body.extend_from_slice(&colour_primaries.to_be_bytes());
body.extend_from_slice(&transfer_characteristics.to_be_bytes());
body.extend_from_slice(&matrix_coefficients.to_be_bytes());
body.push(if *full_range { 0x80 } else { 0x00 });
}
ColrRecord::RestrictedIcc(d) => {
body.extend_from_slice(b"rICC");
body.extend_from_slice(d);
}
ColrRecord::UnrestrictedIcc(d) => {
body.extend_from_slice(b"prof");
body.extend_from_slice(d);
}
ColrRecord::Other { colour_type, data } => {
body.extend_from_slice(colour_type);
body.extend_from_slice(data);
}
}
wrap_box(b"colr", &body)
}
pub fn parse_stvi_box(body: &[u8]) -> Result<StviRecord> {
parse_stvi(body).ok_or_else(|| Error::invalid("MP4: stvi malformed"))
}
pub fn build_stvi_box(record: &StviRecord) -> Result<Vec<u8>> {
let length = record.stereo_indication_type.len();
if length > u32::MAX as usize {
return Err(Error::invalid("MP4: stvi indication exceeds u32 length"));
}
let body_len = 4 + 4 + 4 + 4 + length; 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"stvi");
out.extend_from_slice(&[0u8; 4]); out.extend_from_slice(&((record.single_view_allowed as u32) & 0x3).to_be_bytes());
out.extend_from_slice(&record.stereo_scheme.to_be_bytes());
out.extend_from_slice(&(length as u32).to_be_bytes());
out.extend_from_slice(&record.stereo_indication_type);
Ok(out)
}
use std::io::Read;
fn read_bytes_vec<R: Read + ?Sized>(r: &mut R, n: usize) -> Result<Vec<u8>> {
let mut buf = Vec::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(),
elst_timeline: super::ElstTimeline::default(),
trex: super::TrexDefaults::default(),
protection_scheme: None,
tenc: None,
piff_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,
amve: None,
stvi: None,
btrt: None,
pasp: None,
clap: None,
colr: None,
hmhd: None,
gmin: None,
tcmi: None,
tmcd: None,
text_entry: None,
load_settings: None,
rtp_hint: None,
mpeg2ts_hint: None,
hint_stats: crate::hint::HintStatistics::default(),
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 mut budget = u64::MAX;
let parsed = super::parse_trun(&body, &mut budget).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 mut budget = u64::MAX;
let parsed = super::parse_trun(&body, &mut budget).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.elst), 500);
}
fn elst_entry(dur: u64, mt: i64, rate: u32) -> super::ElstEntry {
super::ElstEntry {
segment_duration: dur,
media_time: mt,
media_rate: rate,
}
}
const RATE_1: u32 = 0x0001_0000;
const RATE_0: u32 = 0;
#[test]
fn elst_timeline_identity_when_absent() {
let tl = super::build_elst_timeline(&[], 1000, 48_000);
assert_eq!(tl.delta_for_cts(0), (0, true));
assert_eq!(tl.delta_for_cts(123_456), (0, true));
}
#[test]
fn elst_timeline_initial_trim() {
let tl = super::build_elst_timeline(&[elst_entry(2000, 1024, RATE_1)], 1000, 48_000);
assert!(tl.mapped);
assert_eq!(tl.delta_for_cts(1024), (-1024, true));
assert_eq!(tl.delta_for_cts(5000), (-1024, true));
assert_eq!(tl.delta_for_cts(0), (-1024, false));
assert_eq!(tl.delta_for_cts(1023), (-1024, false));
}
#[test]
fn elst_timeline_empty_edit_delay_rescales_movie_timescale() {
let elst = [elst_entry(500, -1, RATE_1), elst_entry(3000, 100, RATE_1)];
let tl = super::build_elst_timeline(&elst, 1000, 48_000);
assert!(tl.mapped);
assert_eq!(tl.delta_for_cts(100), (23_900, true));
assert_eq!(tl.delta_for_cts(0), (23_900, false)); }
#[test]
fn elst_timeline_dwell_inserts_presentation_time() {
let elst = [
elst_entry(100, 0, RATE_1),
elst_entry(50, 100, RATE_0),
elst_entry(100, 100, RATE_1),
];
let tl = super::build_elst_timeline(&elst, 1000, 1000);
assert!(tl.mapped);
assert_eq!(tl.delta_for_cts(0), (0, true));
assert_eq!(tl.delta_for_cts(99), (0, true));
assert_eq!(tl.delta_for_cts(100), (50, true));
assert_eq!(tl.delta_for_cts(199), (50, true));
}
#[test]
fn elst_timeline_interior_gap_discards() {
let elst = [
elst_entry(100, 0, RATE_1),
elst_entry(150, -1, RATE_1),
elst_entry(100, 200, RATE_1),
];
let tl = super::build_elst_timeline(&elst, 1000, 1000);
assert!(tl.mapped);
assert_eq!(tl.delta_for_cts(50), (0, true));
assert_eq!(tl.delta_for_cts(100), (0, false));
assert_eq!(tl.delta_for_cts(199), (0, false));
assert_eq!(tl.delta_for_cts(200), (50, true));
assert_eq!(tl.delta_for_cts(305), (50, true));
}
#[test]
fn elst_timeline_non_monotonic_falls_back_to_leading_shift() {
let elst = [elst_entry(100, 0, RATE_1), elst_entry(100, 200, RATE_1)];
let tl = super::build_elst_timeline(&elst, 1000, 1000);
assert!(!tl.mapped);
assert_eq!(tl.delta_for_cts(150), (0, true));
assert_eq!(tl.delta_for_cts(250), (0, true));
}
#[test]
fn elst_timeline_zero_duration_open_ended() {
let tl = super::build_elst_timeline(&[elst_entry(0, 20, RATE_1)], 1000, 1000);
assert!(tl.mapped);
assert_eq!(tl.delta_for_cts(20), (-20, true));
assert_eq!(tl.delta_for_cts(1_000_000), (-20, true));
assert_eq!(tl.delta_for_cts(0), (-20, false)); }
#[test]
fn elst_timeline_saturates_on_giant_durations() {
let elst = [
elst_entry(u64::MAX, -1, RATE_1),
elst_entry(u64::MAX, 10, RATE_1),
];
let tl = super::build_elst_timeline(&elst, 1, 48_000);
let (d, _) = tl.delta_for_cts(10);
assert!(d >= 0, "saturated positive delta, got {d}");
let _ = tl.delta_for_cts(i64::MAX);
let _ = tl.delta_for_cts(i64::MIN);
}
#[test]
fn elst_timeline_zero_movie_timescale() {
let tl = super::build_elst_timeline(&[elst_entry(1000, 30, RATE_1)], 0, 48_000);
assert_eq!(tl.delta_for_cts(30), (-30, true));
assert_eq!(tl.delta_for_cts(29), (-30, false));
}
#[test]
fn elst_timeline_all_empty_list_falls_back() {
let tl = super::build_elst_timeline(&[elst_entry(100, -1, RATE_1)], 1000, 1000);
assert!(!tl.mapped);
assert_eq!(tl.delta_for_cts(0), (0, true));
}
#[test]
fn elst_box_roundtrip_v0() {
let entries = [
super::EditListEntry {
segment_duration: 500,
media_time: -1,
media_rate_integer: 1,
media_rate_fraction: 0,
},
super::EditListEntry {
segment_duration: 3000,
media_time: 1024,
media_rate_integer: 1,
media_rate_fraction: 0,
},
];
let built = super::build_elst_box(&entries).unwrap();
assert_eq!(&built[4..8], b"elst");
assert_eq!(built[8], 0, "v0 when values fit 32 bits");
let parsed = super::parse_elst_box(&built[8..]).unwrap();
assert_eq!(parsed, entries);
}
#[test]
fn elst_box_roundtrip_v1_promotion() {
let entries = [super::EditListEntry {
segment_duration: u32::MAX as u64 + 1,
media_time: i32::MAX as i64 + 7,
media_rate_integer: 1,
media_rate_fraction: 0,
}];
let built = super::build_elst_box(&entries).unwrap();
assert_eq!(built[8], 1, "v1 for over-32-bit values");
let parsed = super::parse_elst_box(&built[8..]).unwrap();
assert_eq!(parsed, entries);
}
#[test]
fn elst_box_roundtrip_dwell() {
let entries = [
super::EditListEntry {
segment_duration: 100,
media_time: 0,
media_rate_integer: 1,
media_rate_fraction: 0,
},
super::EditListEntry {
segment_duration: 50,
media_time: 100,
media_rate_integer: 0,
media_rate_fraction: 0,
},
super::EditListEntry {
segment_duration: 100,
media_time: 100,
media_rate_integer: 1,
media_rate_fraction: 0,
},
];
let built = super::build_elst_box(&entries).unwrap();
let parsed = super::parse_elst_box(&built[8..]).unwrap();
assert_eq!(parsed, entries);
}
#[test]
fn elst_box_build_rejections() {
let normal = super::EditListEntry {
segment_duration: 100,
media_time: 0,
media_rate_integer: 1,
media_rate_fraction: 0,
};
let mut bad_rate = normal;
bad_rate.media_rate_integer = 2;
assert!(super::build_elst_box(&[bad_rate]).is_err());
let mut neg_rate = normal;
neg_rate.media_rate_integer = -1;
assert!(super::build_elst_box(&[neg_rate]).is_err());
let empty_edit = super::EditListEntry {
segment_duration: 100,
media_time: -1,
media_rate_integer: 1,
media_rate_fraction: 0,
};
assert!(super::build_elst_box(&[normal, empty_edit]).is_err());
assert!(super::build_elst_box(&[empty_edit, normal]).is_ok());
let mut sub_sentinel = normal;
sub_sentinel.media_time = -2;
assert!(super::build_elst_box(&[sub_sentinel]).is_err());
assert!(super::build_elst_box(&[]).is_err());
}
#[test]
fn elst_box_parse_truncation() {
assert!(super::parse_elst_box(&[]).is_err());
assert!(super::parse_elst_box(&[0u8; 7]).is_err());
let mut body = vec![0u8; 4];
body.extend_from_slice(&2u32.to_be_bytes());
body.extend_from_slice(&[0u8; 12]);
assert!(super::parse_elst_box(&body).is_err());
}
#[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_tref_round_trips_multiple_types() {
let refs = vec![
(*b"subt", vec![4u32, 5]),
(*b"cdsc", vec![2u32]),
(*b"chap", vec![3u32]),
];
let boxed = super::build_tref_box(&refs).unwrap();
assert_eq!(&boxed[4..8], b"tref");
let mut t = fresh_track();
super::parse_tref(&boxed[8..], &mut t).unwrap();
assert_eq!(t.tref, refs);
}
#[test]
fn build_tref_empty_id_array() {
let refs = vec![(*b"hint", Vec::<u32>::new())];
let boxed = super::build_tref_box(&refs).unwrap();
let mut t = fresh_track();
super::parse_tref(&boxed[8..], &mut t).unwrap();
assert_eq!(t.tref.len(), 1);
assert_eq!(&t.tref[0].0, b"hint");
assert!(t.tref[0].1.is_empty());
}
#[test]
fn build_tref_rejects_zero_id() {
let refs = vec![(*b"font", vec![0u32])];
assert!(super::build_tref_box(&refs).is_none());
}
#[test]
fn build_tref_empty_set() {
let boxed = super::build_tref_box(&[]).unwrap();
assert_eq!(boxed.len(), 8);
assert_eq!(&boxed[4..8], b"tref");
}
#[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 build_trgr_round_trips() {
let groups = vec![(*b"msrc", 17u32), (*b"ster", 42u32)];
let boxed = super::build_trgr_box(&groups);
assert_eq!(&boxed[4..8], b"trgr");
let mut t = fresh_track();
super::parse_trgr(&boxed[8..], &mut t).unwrap();
assert_eq!(t.trgr, groups);
}
#[test]
fn build_trgr_empty_set() {
let boxed = super::build_trgr_box(&[]);
assert_eq!(boxed.len(), 8);
assert_eq!(&boxed[4..8], b"trgr");
let mut t = fresh_track();
super::parse_trgr(&boxed[8..], &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, 1 << 30).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, 1 << 30).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_picks_up_hinf() {
let stats = crate::hint::HintStatistics {
bytes_sent_with_rtp_64: Some(123_456),
packets_sent_64: Some(789),
..crate::hint::HintStatistics::default()
};
let hinf = crate::hint::build_hinf_box(&stats);
let mut udta = Vec::new();
udta.extend_from_slice(&hinf);
let mut t = fresh_track();
super::parse_track_udta(&udta, &mut t);
assert_eq!(t.hint_stats, stats);
assert_eq!(t.hint_stats.bytes_sent_with_rtp_64, Some(123_456));
}
#[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, 1 << 30).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 build_cprt_round_trips() {
let boxed = super::build_cprt_box(b"eng", "(c) 2026 Example").unwrap();
assert_eq!(&boxed[4..8], b"cprt");
let mut t = fresh_track();
super::parse_cprt(&boxed[8..], &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 build_cprt_empty_notice() {
let boxed = super::build_cprt_box(b"fra", "").unwrap();
let mut t = fresh_track();
super::parse_cprt(&boxed[8..], &mut t);
assert_eq!(&t.copyrights[0].language, b"fra");
assert_eq!(t.copyrights[0].notice, "");
}
#[test]
fn build_cprt_rejects_bad_inputs() {
assert!(super::build_cprt_box(b"EN1", "x").is_none());
assert!(super::build_cprt_box(b"eng", "bad\0notice").is_none());
}
#[test]
fn build_kind_round_trips() {
let boxed = super::build_kind_box("urn:mpeg:dash:role:2011", "subtitle").unwrap();
assert_eq!(&boxed[4..8], b"kind");
let mut t = fresh_track();
super::parse_kind(&boxed[8..], &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, "subtitle");
}
#[test]
fn build_kind_empty_value() {
let boxed = super::build_kind_box("urn:scheme", "").unwrap();
let mut t = fresh_track();
super::parse_kind(&boxed[8..], &mut t);
assert_eq!(t.kinds[0].0, "urn:scheme");
assert_eq!(t.kinds[0].1, "");
}
#[test]
fn build_kind_rejects_bad_inputs() {
assert!(super::build_kind_box("", "x").is_none());
assert!(super::build_kind_box("urn:a\0b", "x").is_none());
assert!(super::build_kind_box("urn:a", "x\0y").is_none());
}
#[test]
fn build_tsel_round_trips() {
let boxed = super::build_tsel_box(-5, &[*b"bitr", *b"frar"]);
assert_eq!(&boxed[4..8], b"tsel");
let mut t = fresh_track();
super::parse_tsel(&boxed[8..], &mut t);
let sel = t.tsel.unwrap();
assert_eq!(sel.switch_group, -5);
assert_eq!(sel.attribute_list, vec![*b"bitr", *b"frar"]);
}
#[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, 1 << 30).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, 1 << 30).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 build_stsg_round_trips() {
let boxed = super::build_stsg_box(b"tele", &[1, 2, 3]).unwrap();
assert_eq!(&boxed[4..8], b"stsg");
let sg = super::parse_stsg(&boxed[8..]).unwrap();
assert_eq!(&sg.grouping_type, b"tele");
assert_eq!(sg.group_description_indices, vec![1, 2, 3]);
}
#[test]
fn build_stsg_empty_indices() {
let boxed = super::build_stsg_box(b"roll", &[]).unwrap();
let sg = super::parse_stsg(&boxed[8..]).unwrap();
assert_eq!(&sg.grouping_type, b"roll");
assert!(sg.group_description_indices.is_empty());
}
#[test]
fn build_stri_round_trips() {
let boxed = super::build_stri_box(-2, 7, 9, &[*b"tesc", *b"bitr"]);
assert_eq!(&boxed[4..8], b"stri");
let st = super::parse_stri(&boxed[8..]).unwrap();
assert_eq!(st.switch_group, -2);
assert_eq!(st.alternate_group, 7);
assert_eq!(st.sub_track_id, 9);
assert_eq!(st.attribute_list, vec![*b"tesc", *b"bitr"]);
}
#[test]
fn build_strk_full_round_trips() {
let groups = vec![(*b"tele", vec![1u32, 2]), (*b"roll", vec![4u32])];
let boxed = super::build_strk_box(2, 2, 5, &[*b"tesc"], &groups).unwrap();
assert_eq!(&boxed[4..8], b"strk");
let mut t = fresh_track();
super::parse_strk(&boxed[8..], &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 build_strk_stri_only_omits_strd() {
let boxed = super::build_strk_box(1, 0, 3, &[], &[]).unwrap();
let mut t = fresh_track();
super::parse_strk(&boxed[8..], &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());
assert!(!boxed.windows(4).any(|w| w == b"strd"));
}
#[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, 1 << 30).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, 1 << 30).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_hmhd_pdu_sizes_and_bitrates() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0]); body.extend_from_slice(&1500u16.to_be_bytes()); body.extend_from_slice(&820u16.to_be_bytes()); body.extend_from_slice(&3_000_000u32.to_be_bytes()); body.extend_from_slice(&1_200_000u32.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); let h = super::parse_hmhd(&body).unwrap();
assert_eq!(h.max_pdu_size, 1500);
assert_eq!(h.avg_pdu_size, 820);
assert_eq!(h.max_bitrate, 3_000_000);
assert_eq!(h.avg_bitrate, 1_200_000);
}
#[test]
fn parse_hmhd_tolerates_nonzero_version() {
let mut body = Vec::new();
body.extend_from_slice(&[2u8, 0, 0, 0]); body.extend_from_slice(&64u16.to_be_bytes());
body.extend_from_slice(&64u16.to_be_bytes());
body.extend_from_slice(&500u32.to_be_bytes());
body.extend_from_slice(&500u32.to_be_bytes());
body.extend_from_slice(&0u32.to_be_bytes());
let h = super::parse_hmhd(&body).unwrap();
assert_eq!(h.max_pdu_size, 64);
assert_eq!(h.max_bitrate, 500);
}
#[test]
fn parse_hmhd_nonzero_reserved_tolerated() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0]);
body.extend_from_slice(&10u16.to_be_bytes());
body.extend_from_slice(&20u16.to_be_bytes());
body.extend_from_slice(&30u32.to_be_bytes());
body.extend_from_slice(&40u32.to_be_bytes());
body.extend_from_slice(&0xDEAD_BEEFu32.to_be_bytes()); let h = super::parse_hmhd(&body).unwrap();
assert_eq!(h.avg_bitrate, 40);
}
#[test]
fn parse_hmhd_too_short_is_rejected() {
let body = vec![0u8; 19];
assert!(super::parse_hmhd(&body).is_err());
}
#[test]
fn parse_minf_picks_up_hmhd() {
let mut hmhd = Vec::new();
hmhd.extend_from_slice(&[0u8, 0, 0, 0]);
hmhd.extend_from_slice(&1400u16.to_be_bytes());
hmhd.extend_from_slice(&700u16.to_be_bytes());
hmhd.extend_from_slice(&2_500_000u32.to_be_bytes());
hmhd.extend_from_slice(&900_000u32.to_be_bytes());
hmhd.extend_from_slice(&0u32.to_be_bytes());
let mut minf = Vec::new();
minf.extend(wrap_box_full_size(b"hmhd", &hmhd));
let mut t = fresh_track();
super::parse_minf(&minf, &mut t, 1 << 30).unwrap();
let h = t.hmhd.expect("hmhd should be parsed");
assert_eq!(h.max_pdu_size, 1400);
assert_eq!(h.avg_pdu_size, 700);
assert_eq!(h.max_bitrate, 2_500_000);
assert_eq!(h.avg_bitrate, 900_000);
}
#[test]
fn parse_minf_keeps_first_hmhd_ignores_duplicate() {
let mut first = Vec::new();
first.extend_from_slice(&[0u8, 0, 0, 0]);
first.extend_from_slice(&111u16.to_be_bytes());
first.extend_from_slice(&[0u8; 14]);
let mut second = Vec::new();
second.extend_from_slice(&[0u8, 0, 0, 0]);
second.extend_from_slice(&222u16.to_be_bytes());
second.extend_from_slice(&[0u8; 14]);
let mut minf = Vec::new();
minf.extend(wrap_box_full_size(b"hmhd", &first));
minf.extend(wrap_box_full_size(b"hmhd", &second));
let mut t = fresh_track();
super::parse_minf(&minf, &mut t, 1 << 30).unwrap();
let h = t.hmhd.expect("hmhd should be parsed");
assert_eq!(h.max_pdu_size, 111);
}
#[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, 1 << 30).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, 1 << 30).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, 1 << 30).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);
}
#[test]
fn parse_gmin_fields() {
let mut body = vec![0u8, 0, 0, 0]; body.extend_from_slice(&0x0040u16.to_be_bytes()); body.extend_from_slice(&0x8000u16.to_be_bytes()); body.extend_from_slice(&0x8001u16.to_be_bytes()); body.extend_from_slice(&0x8002u16.to_be_bytes()); body.extend_from_slice(&(-256i16).to_be_bytes()); body.extend_from_slice(&[0u8, 0]); let g = super::parse_gmin(&body).expect("gmin should parse");
assert_eq!(g.graphicsmode, 0x0040);
assert_eq!(g.opcolor, [0x8000, 0x8001, 0x8002]);
assert_eq!(g.balance, -256);
}
#[test]
fn parse_gmin_too_short_is_rejected() {
assert!(super::parse_gmin(&[0u8; 15]).is_none());
assert!(super::parse_gmin_box(&[0u8; 15]).is_err());
}
#[test]
fn build_gmin_box_round_trips() {
let g = super::GminBox {
graphicsmode: 0x0100,
opcolor: [0x1234, 0x5678, 0x9abc],
balance: 128,
};
let boxed = super::build_gmin_box(&g);
assert_eq!(boxed.len(), 24);
assert_eq!(&boxed[4..8], b"gmin");
assert_eq!(
u32::from_be_bytes([boxed[0], boxed[1], boxed[2], boxed[3]]),
24
);
let back = super::parse_gmin_box(&boxed[8..]).expect("round-trip parse");
assert_eq!(back, g);
}
#[test]
fn build_gmhd_box_wraps_gmin() {
let g = super::GminBox {
graphicsmode: 0,
opcolor: [0, 0, 0],
balance: 0,
};
let gmhd = super::build_gmhd_box(&g);
assert_eq!(&gmhd[4..8], b"gmhd");
assert_eq!(gmhd.len(), 32);
let mut t = fresh_track();
super::parse_gmhd(&gmhd[8..], &mut t);
assert_eq!(t.gmin, Some(g));
assert_eq!(t.tcmi, None);
}
#[test]
fn parse_gmhd_skips_foreign_children() {
let g = super::GminBox {
graphicsmode: 0x0040,
opcolor: [1, 2, 3],
balance: -64,
};
let mut inner = super::wrap_box(b"text", &[0xAAu8; 20]);
inner.extend_from_slice(&super::build_gmin_box(&g));
let mut t = fresh_track();
super::parse_gmhd(&inner, &mut t);
assert_eq!(t.gmin, Some(g));
}
#[test]
fn parse_gmhd_captures_gmin_and_tcmi() {
let g = super::GminBox {
graphicsmode: 0,
opcolor: [0, 0, 0],
balance: 0,
};
let tc = super::TcmiBox {
text_font: 0,
text_face: 0,
text_size: 12,
text_color: [0xFFFF, 0xFFFF, 0xFFFF],
background_color: [0, 0, 0],
font_name: "Chicago".to_string(),
};
let mut inner = super::build_gmin_box(&g);
inner.extend_from_slice(&super::build_tcmi_box(&tc));
let mut t = fresh_track();
super::parse_gmhd(&inner, &mut t);
assert_eq!(t.gmin, Some(g));
assert_eq!(t.tcmi, Some(tc));
}
#[test]
fn build_tcmi_box_round_trips() {
let tc = super::TcmiBox {
text_font: 3,
text_face: 0x01, text_size: 18,
text_color: [0x1234, 0x5678, 0x9abc],
background_color: [0xdead, 0xbeef, 0xcafe],
font_name: "Monaco".to_string(),
};
let boxed = super::build_tcmi_box(&tc);
assert_eq!(&boxed[4..8], b"tcmi");
let back = super::parse_tcmi_box(&boxed[8..]).expect("round-trip parse");
assert_eq!(back, tc);
}
#[test]
fn build_tcmi_box_empty_name_round_trips() {
let tc = super::TcmiBox {
text_font: 0,
text_face: 0,
text_size: 10,
text_color: [0, 0, 0],
background_color: [0xFFFF, 0xFFFF, 0xFFFF],
font_name: String::new(),
};
let boxed = super::build_tcmi_box(&tc);
let back = super::parse_tcmi_box(&boxed[8..]).expect("round-trip parse");
assert_eq!(back, tc);
}
#[test]
fn build_tcmi_box_truncates_overlong_name_on_char_boundary() {
let name: String = "\u{FFFD}".repeat(90);
let tc = super::TcmiBox {
text_font: 1,
text_face: 2,
text_size: 3,
text_color: [1, 2, 3],
background_color: [4, 5, 6],
font_name: name.clone(),
};
let boxed = super::build_tcmi_box(&tc);
let back = super::parse_tcmi_box(&boxed[8..]).expect("reparse after truncating build");
assert!(back.font_name.len() <= 255);
assert!(name.starts_with(&back.font_name));
assert_eq!(back.font_name.len() % 3, 0, "cut on a U+FFFD boundary");
let boxed2 = super::build_tcmi_box(&back);
let back2 = super::parse_tcmi_box(&boxed2[8..]).unwrap();
assert_eq!(back2, back);
}
#[test]
fn parse_tcmi_short_and_overrun() {
assert!(super::parse_tcmi(&[0u8; 21]).is_none());
assert!(super::parse_tcmi_box(&[0u8; 21]).is_err());
let mut body = vec![0u8; 22];
body.push(200);
body.extend_from_slice(b"abc");
let tc = super::parse_tcmi(&body).expect("clamped parse");
assert_eq!(tc.font_name, "abc");
}
#[test]
fn build_stream_info_surfaces_tcmi_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Data;
t.codec_id_fourcc = *b"tmcd";
t.timescale = 1000;
t.tcmi = Some(super::TcmiBox {
text_font: 0,
text_face: 1,
text_size: 24,
text_color: [0xFFFF, 0xFFFF, 0xFFFF],
background_color: [0, 0, 0],
font_name: "Helvetica".to_string(),
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("tcmi_text_font"), Some("0"));
assert_eq!(info.params.options.get("tcmi_text_face"), Some("1"));
assert_eq!(info.params.options.get("tcmi_text_size"), Some("24"));
assert_eq!(
info.params.options.get("tcmi_text_color"),
Some("65535 65535 65535")
);
assert_eq!(
info.params.options.get("tcmi_background_color"),
Some("0 0 0")
);
assert_eq!(info.params.options.get("tcmi_font_name"), Some("Helvetica"));
}
#[test]
fn tmcd_sample_entry_round_trips_ntsc_drop_frame() {
let e = super::TmcdSampleEntry {
flags: 0x0001 | 0x0002, timescale: 30_000,
frame_duration: 1001,
number_of_frames: 30,
};
let boxed = super::build_tmcd_sample_entry(&e, 1);
assert_eq!(&boxed[4..8], b"tmcd");
assert_eq!(u16::from_be_bytes([boxed[8 + 6], boxed[8 + 7]]), 1);
let back = super::parse_tmcd_sample_entry_box(&boxed[8..]).expect("round-trip");
assert_eq!(back, e);
assert!(back.drop_frame());
assert!(back.twenty_four_hour_max());
assert!(!back.negative_times_ok());
assert!(!back.counter());
}
#[test]
fn tmcd_sample_entry_too_short_is_rejected() {
assert!(super::parse_tmcd_sample_entry(&[0u8; 24]).is_none());
assert!(super::parse_tmcd_sample_entry_box(&[0u8; 24]).is_err());
}
#[test]
fn parse_stsd_captures_tmcd_entry() {
let e = super::TmcdSampleEntry {
flags: 0x0008, timescale: 25,
frame_duration: 1,
number_of_frames: 25,
};
let entry = super::build_tmcd_sample_entry(&e, 1);
let mut stsd = vec![0u8, 0, 0, 0];
stsd.extend_from_slice(&1u32.to_be_bytes());
stsd.extend_from_slice(&entry);
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Data;
super::parse_stsd(&stsd, &mut t).expect("stsd parse");
assert_eq!(t.codec_id_fourcc, *b"tmcd");
assert_eq!(t.tmcd, Some(e));
assert!(t.tmcd.unwrap().counter());
}
#[test]
fn build_stream_info_surfaces_tmcd_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Data;
t.codec_id_fourcc = *b"tmcd";
t.timescale = 30_000;
t.tmcd = Some(super::TmcdSampleEntry {
flags: 0x0001,
timescale: 30_000,
frame_duration: 1001,
number_of_frames: 30,
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("tmcd_flags"), Some("1"));
assert_eq!(info.params.options.get("tmcd_drop_frame"), Some("true"));
assert_eq!(info.params.options.get("tmcd_24hour_max"), Some("false"));
assert_eq!(info.params.options.get("tmcd_timescale"), Some("30000"));
assert_eq!(info.params.options.get("tmcd_frame_duration"), Some("1001"));
assert_eq!(info.params.options.get("tmcd_number_of_frames"), Some("30"));
}
#[test]
fn text_sample_entry_round_trips() {
let e = super::TextSampleEntry {
display_flags: 0x2000 | 0x1000, text_justification: -1, background_color: [0x1111, 0x2222, 0x3333],
default_text_box: [0, 0, 60, 320],
font_number: 0,
font_face: 0x02, foreground_color: [0xFFFF, 0xFFFF, 0xFFFF],
text_name: "Geneva".to_string(),
};
let boxed = super::build_text_sample_entry(&e, 1);
assert_eq!(&boxed[4..8], b"text");
assert_eq!(u16::from_be_bytes([boxed[8 + 6], boxed[8 + 7]]), 1);
let back = super::parse_text_sample_entry_box(&boxed[8..]).expect("round-trip");
assert_eq!(back, e);
}
#[test]
fn text_sample_entry_too_short_is_rejected() {
assert!(super::parse_text_sample_entry(&[0u8; 50]).is_none());
assert!(super::parse_text_sample_entry_box(&[0u8; 50]).is_err());
}
#[test]
fn text_sample_entry_name_overrun_clamped() {
let mut e = vec![0u8; 51];
e.push(200); e.extend_from_slice(b"xy");
let tx = super::parse_text_sample_entry(&e).expect("clamped parse");
assert_eq!(tx.text_name, "xy");
}
#[test]
fn parse_stsd_captures_text_entry() {
let e = super::TextSampleEntry {
display_flags: 0,
text_justification: 1, background_color: [0, 0, 0],
default_text_box: [0, 0, 0, 0],
font_number: 0,
font_face: 0,
foreground_color: [0xFFFF, 0xFFFF, 0xFFFF],
text_name: String::new(),
};
let entry = super::build_text_sample_entry(&e, 1);
let mut stsd = vec![0u8, 0, 0, 0];
stsd.extend_from_slice(&1u32.to_be_bytes());
stsd.extend_from_slice(&entry);
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Subtitle;
super::parse_stsd(&stsd, &mut t).expect("stsd parse");
assert_eq!(t.codec_id_fourcc, *b"text");
assert_eq!(t.text_entry, Some(e));
assert!(!t.extradata.is_empty());
}
#[test]
fn build_stream_info_surfaces_text_entry_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Subtitle;
t.codec_id_fourcc = *b"text";
t.timescale = 1000;
t.text_entry = Some(super::TextSampleEntry {
display_flags: 0x2000,
text_justification: -1,
background_color: [0, 0, 0],
default_text_box: [1, 2, 3, 4],
font_number: 0,
font_face: 1,
foreground_color: [0xFFFF, 0xFFFF, 0xFFFF],
text_name: "Courier".to_string(),
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("text_display_flags"), Some("8192"));
assert_eq!(info.params.options.get("text_justification"), Some("-1"));
assert_eq!(
info.params.options.get("text_default_text_box"),
Some("1 2 3 4")
);
assert_eq!(
info.params.options.get("text_foreground_color"),
Some("65535 65535 65535")
);
assert_eq!(info.params.options.get("text_font_face"), Some("1"));
assert_eq!(info.params.options.get("text_font_name"), Some("Courier"));
}
#[test]
fn load_settings_round_trips_and_decodes_flags() {
let ls = super::LoadSettingsBox {
preload_start_time: 0,
preload_duration: -1, preload_flags: 2, default_hints: 0x0020 | 0x0100,
};
let boxed = super::build_load_settings_box(&ls);
assert_eq!(&boxed[4..8], b"load");
assert_eq!(boxed.len(), 24);
let back = super::parse_load_settings_box(&boxed[8..]).expect("round-trip");
assert_eq!(back, ls);
assert!(!back.preload_always());
assert!(back.preload_if_enabled());
assert!(back.double_buffer());
assert!(back.high_quality());
}
#[test]
fn load_settings_too_short_is_rejected() {
assert!(super::parse_load_settings(&[0u8; 15]).is_none());
assert!(super::parse_load_settings_box(&[0u8; 15]).is_err());
}
#[test]
fn parse_trak_picks_up_nested_load() {
let ls = super::LoadSettingsBox {
preload_start_time: 100,
preload_duration: 5000,
preload_flags: 1,
default_hints: 0,
};
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 trak = Vec::new();
trak.extend(wrap_box_full_size(b"tkhd", &tkhd));
trak.extend(wrap_box_full_size(b"mdia", &mdia));
trak.extend_from_slice(&super::build_load_settings_box(&ls));
let t = super::parse_trak(&trak, 1 << 30).unwrap().unwrap();
assert_eq!(t.load_settings, Some(ls));
}
#[test]
fn build_stream_info_surfaces_load_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 1000;
t.load_settings = Some(super::LoadSettingsBox {
preload_start_time: 0,
preload_duration: -1,
preload_flags: 1,
default_hints: 0x0100,
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("load_preload_duration"), Some("-1"));
assert_eq!(info.params.options.get("load_preload_flags"), Some("1"));
assert_eq!(info.params.options.get("load_preload_always"), Some("true"));
assert_eq!(
info.params.options.get("load_preload_if_enabled"),
Some("false")
);
assert_eq!(info.params.options.get("load_high_quality"), Some("true"));
assert_eq!(info.params.options.get("load_double_buffer"), Some("false"));
}
#[test]
fn build_stream_info_surfaces_gmin_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Subtitle;
t.codec_id_fourcc = *b"text";
t.timescale = 1000;
t.gmin = Some(super::GminBox {
graphicsmode: 64,
opcolor: [1, 2, 3],
balance: -256,
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("gmin_graphicsmode"), Some("64"));
assert_eq!(info.params.options.get("gmin_opcolor"), Some("1 2 3"));
assert_eq!(info.params.options.get("gmin_balance"), Some("-256"));
}
#[test]
fn build_stream_info_no_gmin_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("gmin_graphicsmode"), None);
assert_eq!(info.params.options.get("gmin_opcolor"), None);
assert_eq!(info.params.options.get("gmin_balance"), None);
}
fn amve_body(illum: u32, x: u16, y: u16) -> Vec<u8> {
let mut out = Vec::with_capacity(8);
out.extend_from_slice(&illum.to_be_bytes());
out.extend_from_slice(&x.to_be_bytes());
out.extend_from_slice(&y.to_be_bytes());
out
}
fn video_sample_entry_body(width: u16, height: u16, children: &[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; 16]); out.extend_from_slice(&width.to_be_bytes());
out.extend_from_slice(&height.to_be_bytes());
out.extend_from_slice(&0x0048_0000u32.to_be_bytes()); out.extend_from_slice(&0x0048_0000u32.to_be_bytes()); out.extend_from_slice(&[0u8; 4]); out.extend_from_slice(&1u16.to_be_bytes()); out.extend_from_slice(&[0u8; 32]); out.extend_from_slice(&0x0018u16.to_be_bytes()); out.extend_from_slice(&0xFFFFu16.to_be_bytes()); debug_assert_eq!(out.len(), 78);
out.extend_from_slice(children);
out
}
#[test]
fn parse_amve_bt2035_example() {
let body = amve_body(100_000, 15_635, 16_450);
let rec = super::parse_amve(&body).expect("amve should parse");
assert_eq!(rec.ambient_illuminance, 100_000);
assert_eq!(rec.ambient_light_x, 15_635);
assert_eq!(rec.ambient_light_y, 16_450);
}
#[test]
fn parse_amve_tolerates_trailing_bytes() {
let mut body = amve_body(7, 1, 2);
body.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
let rec = super::parse_amve(&body).expect("amve should parse");
assert_eq!(rec.ambient_illuminance, 7);
assert_eq!(rec.ambient_light_x, 1);
assert_eq!(rec.ambient_light_y, 2);
}
#[test]
fn parse_amve_rejects_short_body() {
assert!(super::parse_amve(&[0u8; 7]).is_none());
assert!(super::parse_amve(&[]).is_none());
assert!(super::parse_amve_box(&[0u8; 7]).is_err());
}
#[test]
fn parse_amve_box_public_entry() {
let body = amve_body(50_000, 25_000, 30_000);
let rec = super::parse_amve_box(&body).expect("amve should parse");
assert_eq!(rec.ambient_illuminance, 50_000);
assert_eq!(rec.ambient_light_x, 25_000);
assert_eq!(rec.ambient_light_y, 30_000);
}
#[test]
fn parse_video_sample_entry_picks_up_amve() {
let amve = wrap_box_full_size(b"amve", &amve_body(100_000, 15_635, 16_450));
let entry = video_sample_entry_body(1920, 1080, &amve);
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
super::parse_video_sample_entry(&entry, &mut t).unwrap();
assert_eq!(t.width, Some(1920));
assert_eq!(t.height, Some(1080));
let a = t.amve.expect("amve should be parsed");
assert_eq!(a.ambient_illuminance, 100_000);
assert_eq!(a.ambient_light_x, 15_635);
assert_eq!(a.ambient_light_y, 16_450);
}
#[test]
fn parse_video_sample_entry_amve_first_wins() {
let mut children = wrap_box_full_size(b"amve", &amve_body(1, 2, 3));
children.extend(wrap_box_full_size(b"amve", &amve_body(9, 9, 9)));
let entry = video_sample_entry_body(640, 480, &children);
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
super::parse_video_sample_entry(&entry, &mut t).unwrap();
let a = t.amve.expect("amve should be parsed");
assert_eq!(a.ambient_illuminance, 1);
assert_eq!(a.ambient_light_x, 2);
assert_eq!(a.ambient_light_y, 3);
}
#[test]
fn parse_video_sample_entry_drops_malformed_amve() {
let amve = wrap_box_full_size(b"amve", &[0u8; 4]); let entry = video_sample_entry_body(1280, 720, &amve);
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
super::parse_video_sample_entry(&entry, &mut t).unwrap();
assert_eq!(t.width, Some(1280));
assert!(t.amve.is_none());
}
#[test]
fn build_stream_info_surfaces_amve_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"hvc1";
t.timescale = 1000;
t.amve = Some(super::AmveRecord {
ambient_illuminance: 100_000,
ambient_light_x: 15_635,
ambient_light_y: 16_450,
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(
info.params.options.get("amve_ambient_illuminance"),
Some("100000")
);
assert_eq!(
info.params.options.get("amve_ambient_light_x"),
Some("15635")
);
assert_eq!(
info.params.options.get("amve_ambient_light_y"),
Some("16450")
);
}
#[test]
fn build_stream_info_no_amve_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("amve_ambient_illuminance"), None);
assert_eq!(info.params.options.get("amve_ambient_light_x"), None);
assert_eq!(info.params.options.get("amve_ambient_light_y"), None);
}
fn btrt_body(buf: u32, max: u32, avg: u32) -> Vec<u8> {
let mut out = Vec::with_capacity(12);
out.extend_from_slice(&buf.to_be_bytes());
out.extend_from_slice(&max.to_be_bytes());
out.extend_from_slice(&avg.to_be_bytes());
out
}
#[test]
fn parse_btrt_three_u32_fields() {
let body = btrt_body(65_536, 5_000_000, 3_200_000);
let rec = super::parse_btrt(&body).expect("btrt should parse");
assert_eq!(rec.buffer_size_db, 65_536);
assert_eq!(rec.max_bitrate, 5_000_000);
assert_eq!(rec.avg_bitrate, 3_200_000);
}
#[test]
fn parse_btrt_tolerates_trailing_bytes() {
let mut body = btrt_body(1, 2, 3);
body.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
let rec = super::parse_btrt(&body).expect("btrt should parse");
assert_eq!(rec.buffer_size_db, 1);
assert_eq!(rec.max_bitrate, 2);
assert_eq!(rec.avg_bitrate, 3);
}
#[test]
fn parse_btrt_rejects_short_body() {
assert!(super::parse_btrt(&[0u8; 11]).is_none());
assert!(super::parse_btrt(&[]).is_none());
assert!(super::parse_btrt_box(&[0u8; 11]).is_err());
}
#[test]
fn parse_btrt_box_public_entry() {
let body = btrt_body(8_192, 1_500_000, 900_000);
let rec = super::parse_btrt_box(&body).expect("btrt should parse");
assert_eq!(rec.buffer_size_db, 8_192);
assert_eq!(rec.max_bitrate, 1_500_000);
assert_eq!(rec.avg_bitrate, 900_000);
}
#[test]
fn parse_video_sample_entry_picks_up_btrt() {
let btrt = wrap_box_full_size(b"btrt", &btrt_body(131_072, 8_000_000, 6_000_000));
let entry = video_sample_entry_body(3840, 2160, &btrt);
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
super::parse_video_sample_entry(&entry, &mut t).unwrap();
assert_eq!(t.width, Some(3840));
let b = t.btrt.expect("btrt should be parsed");
assert_eq!(b.buffer_size_db, 131_072);
assert_eq!(b.max_bitrate, 8_000_000);
assert_eq!(b.avg_bitrate, 6_000_000);
}
fn pasp_body(h: u32, v: u32) -> Vec<u8> {
let mut b = h.to_be_bytes().to_vec();
b.extend_from_slice(&v.to_be_bytes());
b
}
fn clap_body(vals: [u32; 8]) -> Vec<u8> {
let mut b = Vec::new();
for v in vals {
b.extend_from_slice(&v.to_be_bytes());
}
b
}
#[test]
fn parse_pasp_clap_colr_bodies() {
let p = super::parse_pasp(&pasp_body(16, 15)).unwrap();
assert_eq!(p.h_spacing, 16);
assert_eq!(p.v_spacing, 15);
assert!(super::parse_pasp(&[0u8; 4]).is_none());
let c = super::parse_clap(&clap_body([1920, 1, 1080, 1, 0, 1, 0, 1])).unwrap();
assert_eq!(c.width_n, 1920);
assert_eq!(c.height_n, 1080);
assert_eq!(c.horiz_off_d, 1);
assert!(super::parse_clap(&[0u8; 16]).is_none());
let mut nclx = b"nclx".to_vec();
nclx.extend_from_slice(&1u16.to_be_bytes());
nclx.extend_from_slice(&1u16.to_be_bytes());
nclx.extend_from_slice(&1u16.to_be_bytes());
nclx.push(0x80); match super::parse_colr(&nclx).unwrap() {
super::ColrRecord::Nclx {
colour_primaries,
transfer_characteristics,
matrix_coefficients,
full_range,
} => {
assert_eq!(colour_primaries, 1);
assert_eq!(transfer_characteristics, 1);
assert_eq!(matrix_coefficients, 1);
assert!(full_range);
}
other => panic!("expected nclx, got {other:?}"),
}
let mut prof = b"prof".to_vec();
prof.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
match super::parse_colr(&prof).unwrap() {
super::ColrRecord::UnrestrictedIcc(d) => {
assert_eq!(d, vec![0xDE, 0xAD, 0xBE, 0xEF])
}
other => panic!("expected prof, got {other:?}"),
}
let mut other = b"xxxx".to_vec();
other.push(9);
match super::parse_colr(&other).unwrap() {
super::ColrRecord::Other { colour_type, data } => {
assert_eq!(&colour_type, b"xxxx");
assert_eq!(data, vec![9]);
}
o => panic!("expected Other, got {o:?}"),
}
assert!(super::parse_colr(&[1, 2, 3]).is_none());
}
#[test]
fn build_pasp_clap_colr_round_trip() {
let p = super::PaspRecord {
h_spacing: 64,
v_spacing: 45,
};
let built = super::build_pasp_box(&p);
assert_eq!(super::parse_pasp_box(&built[8..]).unwrap(), p);
let c = super::ClapRecord {
width_n: 1920,
width_d: 1,
height_n: 1080,
height_d: 1,
horiz_off_n: 3,
horiz_off_d: 2,
vert_off_n: 5,
vert_off_d: 4,
};
let built = super::build_clap_box(&c);
assert_eq!(super::parse_clap_box(&built[8..]).unwrap(), c);
for colr in [
super::ColrRecord::Nclx {
colour_primaries: 9,
transfer_characteristics: 16,
matrix_coefficients: 9,
full_range: true,
},
super::ColrRecord::UnrestrictedIcc(vec![1, 2, 3, 4]),
super::ColrRecord::RestrictedIcc(vec![9, 8, 7]),
super::ColrRecord::Other {
colour_type: *b"zzzz",
data: vec![0, 1, 2],
},
] {
let built = super::build_colr_box(&colr);
assert_eq!(super::parse_colr_box(&built[8..]).unwrap(), colr);
}
}
#[test]
fn parse_video_sample_entry_picks_up_pasp_clap_colr() {
let mut children = wrap_box_full_size(b"pasp", &pasp_body(40, 33));
children.extend(wrap_box_full_size(
b"clap",
&clap_body([1280, 1, 720, 1, 0, 1, 0, 1]),
));
let mut nclx = b"nclx".to_vec();
nclx.extend_from_slice(&9u16.to_be_bytes()); nclx.extend_from_slice(&16u16.to_be_bytes()); nclx.extend_from_slice(&9u16.to_be_bytes()); nclx.push(0x00); children.extend(wrap_box_full_size(b"colr", &nclx));
let entry = video_sample_entry_body(1280, 720, &children);
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
super::parse_video_sample_entry(&entry, &mut t).unwrap();
assert_eq!(t.pasp.unwrap().h_spacing, 40);
assert_eq!(t.clap.unwrap().width_n, 1280);
match t.colr.unwrap() {
super::ColrRecord::Nclx {
colour_primaries,
full_range,
..
} => {
assert_eq!(colour_primaries, 9);
assert!(!full_range);
}
o => panic!("expected nclx, got {o:?}"),
}
}
#[test]
fn build_stream_info_surfaces_pasp_clap_colr_options() {
let mut children = wrap_box_full_size(b"pasp", &pasp_body(64, 45));
children.extend(wrap_box_full_size(
b"clap",
&clap_body([1920, 1, 1080, 1, 0, 1, 0, 1]),
));
let mut nclx = b"nclx".to_vec();
nclx.extend_from_slice(&1u16.to_be_bytes());
nclx.extend_from_slice(&1u16.to_be_bytes());
nclx.extend_from_slice(&1u16.to_be_bytes());
nclx.push(0x80);
children.extend(wrap_box_full_size(b"colr", &nclx));
let entry = video_sample_entry_body(1920, 1080, &children);
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
super::parse_video_sample_entry(&entry, &mut t).unwrap();
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
let o = &info.params.options;
assert_eq!(o.get("pasp_h_spacing"), Some("64"));
assert_eq!(o.get("pasp_v_spacing"), Some("45"));
assert_eq!(o.get("clap_width"), Some("1920/1"));
assert_eq!(o.get("clap_horiz_off"), Some("0/1"));
assert_eq!(o.get("colr_type"), Some("nclx"));
assert_eq!(o.get("colr_primaries"), Some("1"));
assert_eq!(o.get("colr_full_range"), Some("1"));
}
#[test]
fn parse_video_sample_entry_btrt_first_wins() {
let mut children = wrap_box_full_size(b"btrt", &btrt_body(1, 2, 3));
children.extend(wrap_box_full_size(b"btrt", &btrt_body(9, 9, 9)));
let entry = video_sample_entry_body(640, 480, &children);
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
super::parse_video_sample_entry(&entry, &mut t).unwrap();
let b = t.btrt.expect("btrt should be parsed");
assert_eq!(b.buffer_size_db, 1);
assert_eq!(b.max_bitrate, 2);
assert_eq!(b.avg_bitrate, 3);
}
#[test]
fn parse_video_sample_entry_drops_malformed_btrt() {
let btrt = wrap_box_full_size(b"btrt", &[0u8; 8]); let entry = video_sample_entry_body(1280, 720, &btrt);
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
super::parse_video_sample_entry(&entry, &mut t).unwrap();
assert_eq!(t.width, Some(1280));
assert!(t.btrt.is_none());
}
#[test]
fn parse_audio_sample_entry_picks_up_btrt() {
let btrt = wrap_box_full_size(b"btrt", &btrt_body(4_096, 256_000, 192_000));
let mut entry = audio_sample_entry_body(2, 1);
entry.extend_from_slice(&btrt);
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Audio;
super::parse_audio_sample_entry(&entry, &mut t).unwrap();
assert_eq!(t.channels, Some(2));
let b = t.btrt.expect("btrt should be parsed");
assert_eq!(b.buffer_size_db, 4_096);
assert_eq!(b.max_bitrate, 256_000);
assert_eq!(b.avg_bitrate, 192_000);
}
#[test]
fn build_stream_info_surfaces_btrt_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 1000;
t.btrt = Some(super::BtrtRecord {
buffer_size_db: 131_072,
max_bitrate: 8_000_000,
avg_bitrate: 6_000_000,
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(
info.params.options.get("btrt_buffer_size_db"),
Some("131072")
);
assert_eq!(info.params.options.get("btrt_max_bitrate"), Some("8000000"));
assert_eq!(info.params.options.get("btrt_avg_bitrate"), Some("6000000"));
}
#[test]
fn build_stream_info_no_btrt_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("btrt_buffer_size_db"), None);
assert_eq!(info.params.options.get("btrt_max_bitrate"), None);
assert_eq!(info.params.options.get("btrt_avg_bitrate"), None);
}
fn stvi_body(single_view_allowed: u8, stereo_scheme: u32, indication: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&[0u8; 4]);
let word = (single_view_allowed as u32) & 0x3;
out.extend_from_slice(&word.to_be_bytes());
out.extend_from_slice(&stereo_scheme.to_be_bytes());
out.extend_from_slice(&(indication.len() as u32).to_be_bytes());
out.extend_from_slice(indication);
out
}
#[test]
fn parse_stvi_scheme1_frame_packing() {
let body = stvi_body(3, 1, &3u32.to_be_bytes());
let rec = super::parse_stvi(&body).expect("stvi should parse");
assert_eq!(rec.single_view_allowed, 3);
assert_eq!(rec.stereo_scheme, 1);
assert_eq!(rec.stereo_indication_type, vec![0, 0, 0, 3]);
assert!(rec.right_view_monoscopic_allowed());
assert!(rec.left_view_monoscopic_allowed());
}
#[test]
fn parse_stvi_masks_reserved_bits() {
let mut body = stvi_body(0, 3, &[0x01, 0x00]);
body[4] = 0xFF;
body[5] = 0xFF;
body[6] = 0xFF;
body[7] = 0xFE; let rec = super::parse_stvi(&body).expect("stvi should parse");
assert_eq!(rec.single_view_allowed, 2);
assert!(!rec.right_view_monoscopic_allowed());
assert!(rec.left_view_monoscopic_allowed());
assert_eq!(rec.stereo_scheme, 3);
assert_eq!(rec.stereo_indication_type, vec![0x01, 0x00]);
}
#[test]
fn parse_stvi_ignores_trailing_any_box() {
let mut body = stvi_body(1, 1, &4u32.to_be_bytes());
body.extend_from_slice(&8u32.to_be_bytes());
body.extend_from_slice(b"free");
let rec = super::parse_stvi(&body).expect("stvi should parse");
assert_eq!(rec.single_view_allowed, 1);
assert_eq!(rec.stereo_indication_type, vec![0, 0, 0, 4]);
}
#[test]
fn parse_stvi_rejects_overrunning_length() {
let mut body = stvi_body(0, 1, &[]);
body[12..16].copy_from_slice(&16u32.to_be_bytes());
assert!(super::parse_stvi(&body).is_none());
}
#[test]
fn parse_stvi_rejects_short_body() {
assert!(super::parse_stvi(&[0u8; 15]).is_none());
assert!(super::parse_stvi_box(&[0u8; 15]).is_err());
}
#[test]
fn parse_stvi_box_public_entry() {
let body = stvi_body(2, 2, &7u32.to_be_bytes());
let rec = super::parse_stvi_box(&body).expect("stvi should parse");
assert_eq!(rec.single_view_allowed, 2);
assert_eq!(rec.stereo_scheme, 2);
assert_eq!(rec.stereo_indication_type, vec![0, 0, 0, 7]);
}
#[test]
fn parse_stvi_empty_indication() {
let body = stvi_body(0, 99, &[]);
let rec = super::parse_stvi(&body).expect("stvi should parse");
assert_eq!(rec.stereo_scheme, 99);
assert!(rec.stereo_indication_type.is_empty());
}
#[test]
fn walk_schi_picks_up_stvi() {
let stvi = wrap_box_full_size(b"stvi", &stvi_body(1, 1, &3u32.to_be_bytes()));
let children = super::walk_schi(&stvi);
let s = children.stvi.expect("stvi should be parsed");
assert_eq!(s.single_view_allowed, 1);
assert_eq!(s.stereo_scheme, 1);
assert!(children.tenc.is_none());
}
#[test]
fn parse_video_sample_entry_picks_up_stvi_via_sinf() {
let stvi = wrap_box_full_size(b"stvi", &stvi_body(3, 1, &3u32.to_be_bytes()));
let schi = wrap_box_full_size(b"schi", &stvi);
let sinf = wrap_box_full_size(b"sinf", &schi);
let entry = video_sample_entry_body(1920, 1080, &sinf);
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
super::parse_video_sample_entry(&entry, &mut t).unwrap();
assert_eq!(t.width, Some(1920));
let s = t.stvi.expect("stvi should be parsed");
assert_eq!(s.single_view_allowed, 3);
assert_eq!(s.stereo_scheme, 1);
assert_eq!(s.stereo_indication_type, vec![0, 0, 0, 3]);
}
#[test]
fn build_stream_info_surfaces_stvi_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 1000;
t.stvi = Some(super::StviRecord {
single_view_allowed: 3,
stereo_scheme: 1,
stereo_indication_type: vec![0x00, 0x00, 0x00, 0x03],
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(
info.params.options.get("stvi_single_view_allowed"),
Some("3")
);
assert_eq!(info.params.options.get("stvi_stereo_scheme"), Some("1"));
assert_eq!(info.params.options.get("stvi_indication"), Some("00000003"));
}
#[test]
fn build_stream_info_stvi_empty_indication_omits_hex() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Video;
t.codec_id_fourcc = *b"avc1";
t.timescale = 1000;
t.stvi = Some(super::StviRecord {
single_view_allowed: 0,
stereo_scheme: 2,
stereo_indication_type: Vec::new(),
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(
info.params.options.get("stvi_single_view_allowed"),
Some("0")
);
assert_eq!(info.params.options.get("stvi_stereo_scheme"), Some("2"));
assert_eq!(info.params.options.get("stvi_indication"), None);
}
#[test]
fn build_stream_info_surfaces_rtp_hint_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Data;
t.codec_id_fourcc = *b"rtp ";
t.timescale = 90_000;
t.rtp_hint = Some(crate::hint::RtpHintSampleEntry {
format: *b"rtp ",
data_reference_index: 1,
hint_track_version: 1,
highest_compatible_version: 1,
max_packet_size: 1450,
timescale: Some(90_000),
time_offset: Some(-100),
sequence_offset: Some(42),
srpp: None,
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("rtp_hint_format"), Some("rtp "));
assert_eq!(
info.params.options.get("rtp_hint_max_packet_size"),
Some("1450")
);
assert_eq!(info.params.options.get("rtp_hint_timescale"), Some("90000"));
assert_eq!(
info.params.options.get("rtp_hint_time_offset"),
Some("-100")
);
assert_eq!(
info.params.options.get("rtp_hint_sequence_offset"),
Some("42")
);
assert_eq!(info.params.options.get("rtp_hint_srtp"), None);
}
#[test]
fn build_stream_info_srtp_hint_marks_srtp() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Data;
t.codec_id_fourcc = *b"srtp";
t.timescale = 90_000;
t.rtp_hint = Some(crate::hint::RtpHintSampleEntry {
format: *b"srtp",
data_reference_index: 1,
hint_track_version: 1,
highest_compatible_version: 1,
max_packet_size: 1400,
timescale: Some(90_000),
time_offset: None,
sequence_offset: None,
srpp: Some(crate::hint::SrppBox {
version: 0,
encryption_algorithm_rtp: 0x2020_2020,
encryption_algorithm_rtcp: 0x2020_2020,
integrity_algorithm_rtp: 0x2020_2020,
integrity_algorithm_rtcp: 0x2020_2020,
scheme_bytes: vec![],
}),
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("rtp_hint_format"), Some("srtp"));
assert_eq!(info.params.options.get("rtp_hint_srtp"), Some("true"));
assert_eq!(info.params.options.get("rtp_hint_time_offset"), None);
assert_eq!(info.params.options.get("rtp_hint_sequence_offset"), None);
}
#[test]
fn build_stream_info_no_rtp_hint_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("rtp_hint_format"), None);
assert_eq!(info.params.options.get("rtp_hint_max_packet_size"), None);
}
#[test]
fn build_stream_info_surfaces_mpeg2ts_hint_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Data;
t.codec_id_fourcc = *b"sm2t";
t.timescale = 90_000;
t.mpeg2ts_hint = Some(crate::hint::Mpeg2TsHintSampleEntry {
format: *b"sm2t",
data_reference_index: 1,
hint_track_version: 1,
highest_compatible_version: 1,
preceding_bytes_len: 4,
trailing_bytes_len: 16,
precomputed_only: true,
additional_data: vec![],
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("m2t_hint_format"), Some("sm2t"));
assert_eq!(
info.params.options.get("m2t_hint_preceding_bytes"),
Some("4")
);
assert_eq!(
info.params.options.get("m2t_hint_trailing_bytes"),
Some("16")
);
assert_eq!(
info.params.options.get("m2t_hint_precomputed"),
Some("true")
);
}
#[test]
fn build_stream_info_surfaces_hinf_on_options() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Data;
t.codec_id_fourcc = *b"rtp ";
t.timescale = 90_000;
t.hint_stats = crate::hint::HintStatistics {
bytes_sent_with_rtp_64: Some(1_000_000),
packets_sent_64: Some(5000),
bytes_sent_no_rtp_64: Some(940_000),
media_bytes_sent: Some(800_000),
largest_packet: Some(1452),
max_rates: vec![
crate::hint::MaxRate {
period: 1000,
bytes: 64_000,
},
crate::hint::MaxRate {
period: 2000,
bytes: 120_000,
},
],
payload_ids: vec![crate::hint::PayloadId {
payload_id: 96,
rtpmap: "H264/90000".to_string(),
}],
..crate::hint::HintStatistics::default()
};
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("hinf_bytes_sent"), Some("1000000"));
assert_eq!(info.params.options.get("hinf_packets_sent"), Some("5000"));
assert_eq!(
info.params.options.get("hinf_payload_bytes"),
Some("940000")
);
assert_eq!(info.params.options.get("hinf_media_bytes"), Some("800000"));
assert_eq!(info.params.options.get("hinf_largest_packet"), Some("1452"));
assert_eq!(info.params.options.get("hinf_maxr_count"), Some("2"));
assert_eq!(info.params.options.get("hinf_payload_count"), Some("1"));
assert_eq!(info.params.options.get("hinf_immediate_bytes"), None);
}
#[test]
fn build_stream_info_no_hinf_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("hinf_bytes_sent"), None);
assert_eq!(info.params.options.get("hinf_maxr_count"), None);
}
#[test]
fn build_stream_info_mpeg2ts_hint_no_precomputed_omits_key() {
let mut t = fresh_track();
t.media_type = oxideav_core::MediaType::Data;
t.codec_id_fourcc = *b"rm2t";
t.timescale = 90_000;
t.mpeg2ts_hint = Some(crate::hint::Mpeg2TsHintSampleEntry {
format: *b"rm2t",
data_reference_index: 1,
hint_track_version: 1,
highest_compatible_version: 1,
preceding_bytes_len: 0,
trailing_bytes_len: 0,
precomputed_only: false,
additional_data: vec![],
});
let info = super::build_stream_info(0, &t, &oxideav_core::NullCodecResolver);
assert_eq!(info.params.options.get("m2t_hint_format"), Some("rm2t"));
assert_eq!(info.params.options.get("m2t_hint_precomputed"), None);
}
#[test]
fn build_stream_info_no_stvi_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("stvi_single_view_allowed"), None);
assert_eq!(info.params.options.get("stvi_stereo_scheme"), None);
assert_eq!(info.params.options.get("stvi_indication"), 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, 1 << 30).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, 1 << 30).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_captures_fragment_local_flag_and_resolves_msb() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0x81]);
body.extend_from_slice(b"seig");
body.extend_from_slice(&1u32.to_be_bytes()); body.push(0x32); body.extend_from_slice(&[0x80, 0x01, 0x83]); let cg = super::parse_csgp(&body).unwrap();
assert!(cg.index_msb_indicates_fragment_local_description);
assert_eq!(cg.index_field_bits, 8);
assert_eq!(cg.patterns[0].indices, vec![0x80, 0x01, 0x83]);
let pat = &cg.patterns[0];
let r0 = pat
.resolve_index(
0,
cg.index_msb_indicates_fragment_local_description,
cg.index_field_bits,
)
.unwrap();
assert!(r0.fragment_local);
assert_eq!(r0.value, 0);
let r1 = pat
.resolve_index(
1,
cg.index_msb_indicates_fragment_local_description,
cg.index_field_bits,
)
.unwrap();
assert!(!r1.fragment_local);
assert_eq!(r1.value, 1);
let r2 = pat
.resolve_index(
2,
cg.index_msb_indicates_fragment_local_description,
cg.index_field_bits,
)
.unwrap();
assert!(r2.fragment_local);
assert_eq!(r2.value, 3);
}
#[test]
fn resolve_csgp_index_width_aware() {
let r = super::resolve_csgp_index(0x8000_0001, false, 32);
assert!(!r.fragment_local);
assert_eq!(r.value, 0x8000_0001);
let r = super::resolve_csgp_index(0x9, true, 4);
assert!(r.fragment_local);
assert_eq!(r.value, 0x1);
let r = super::resolve_csgp_index(0x8001, true, 16);
assert!(r.fragment_local);
assert_eq!(r.value, 0x1);
let r = super::resolve_csgp_index(0x8000_0005, true, 32);
assert!(r.fragment_local);
assert_eq!(r.value, 5);
let r = super::resolve_csgp_index(0xFF, true, 0);
assert!(!r.fragment_local);
assert_eq!(r.value, 0xFF);
}
#[test]
fn csgp_resolve_samples_cycles_pattern() {
let cg = super::CsgpBox {
grouping_type: *b"roll",
grouping_type_parameter: None,
index_msb_indicates_fragment_local_description: false,
index_field_bits: 16,
patterns: vec![super::CsgpPattern {
sample_count: 7,
indices: vec![10, 20, 30],
}],
};
let resolved = cg.resolve_samples(7);
let values: Vec<u32> = resolved.iter().map(|r| r.value).collect();
assert_eq!(values, vec![10, 20, 30, 10, 20, 30, 10]);
assert!(resolved.iter().all(|r| !r.fragment_local));
}
#[test]
fn csgp_resolve_samples_multi_pattern() {
let cg = super::CsgpBox {
grouping_type: *b"roll",
grouping_type_parameter: None,
index_msb_indicates_fragment_local_description: false,
index_field_bits: 16,
patterns: vec![
super::CsgpPattern {
sample_count: 2,
indices: vec![1, 2],
},
super::CsgpPattern {
sample_count: 3,
indices: vec![9],
},
],
};
let values: Vec<u32> = cg.resolve_samples(5).iter().map(|r| r.value).collect();
assert_eq!(values, vec![1, 2, 9, 9, 9]);
}
#[test]
fn csgp_resolve_samples_pads_trailing_default() {
let cg = super::CsgpBox {
grouping_type: *b"roll",
grouping_type_parameter: None,
index_msb_indicates_fragment_local_description: false,
index_field_bits: 16,
patterns: vec![super::CsgpPattern {
sample_count: 2,
indices: vec![5],
}],
};
let values: Vec<u32> = cg.resolve_samples(5).iter().map(|r| r.value).collect();
assert_eq!(values, vec![5, 5, 0, 0, 0]);
}
#[test]
fn csgp_resolve_samples_clamps_overflow() {
let cg = super::CsgpBox {
grouping_type: *b"roll",
grouping_type_parameter: None,
index_msb_indicates_fragment_local_description: false,
index_field_bits: 16,
patterns: vec![super::CsgpPattern {
sample_count: 100,
indices: vec![7],
}],
};
let resolved = cg.resolve_samples(3);
assert_eq!(resolved.len(), 3);
assert!(resolved.iter().all(|r| r.value == 7));
}
#[test]
fn csgp_resolve_samples_resolves_fragment_local_msb() {
let cg = super::CsgpBox {
grouping_type: *b"seig",
grouping_type_parameter: None,
index_msb_indicates_fragment_local_description: true,
index_field_bits: 8,
patterns: vec![super::CsgpPattern {
sample_count: 4,
indices: vec![0x81, 0x02],
}],
};
let resolved = cg.resolve_samples(4);
assert_eq!(resolved.len(), 4);
assert!(resolved[0].fragment_local);
assert_eq!(resolved[0].value, 1);
assert!(!resolved[1].fragment_local);
assert_eq!(resolved[1].value, 2);
assert!(resolved[2].fragment_local);
assert_eq!(resolved[2].value, 1);
assert!(!resolved[3].fragment_local);
assert_eq!(resolved[3].value, 2);
}
#[test]
fn csgp_resolve_samples_empty_pattern_skipped() {
let cg = super::CsgpBox {
grouping_type: *b"roll",
grouping_type_parameter: None,
index_msb_indicates_fragment_local_description: false,
index_field_bits: 16,
patterns: vec![
super::CsgpPattern {
sample_count: 5,
indices: vec![],
},
super::CsgpPattern {
sample_count: 2,
indices: vec![3],
},
],
};
let values: Vec<u32> = cg.resolve_samples(4).iter().map(|r| r.value).collect();
assert_eq!(values, vec![3, 3, 0, 0]);
}
#[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_csgp_mixed_4bit_width_rejected() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0x04]);
body.extend_from_slice(b"roll");
body.extend_from_slice(&0u32.to_be_bytes()); let err = super::parse_csgp(&body);
assert!(err.is_err(), "mixed 4-bit/non-4-bit width must be rejected");
}
#[test]
fn parse_csgp_mixed_4bit_width_rejected_other_way() {
let mut body = Vec::new();
body.extend_from_slice(&[0u8, 0, 0, 0x10]);
body.extend_from_slice(b"roll");
body.extend_from_slice(&0u32.to_be_bytes()); assert!(super::parse_csgp(&body).is_err());
}
#[test]
fn parse_csgp_agreeing_widths_accepted() {
let mut both_4 = Vec::new();
both_4.extend_from_slice(&[0u8, 0, 0, 0]);
both_4.extend_from_slice(b"roll");
both_4.extend_from_slice(&0u32.to_be_bytes());
assert!(super::parse_csgp(&both_4).is_ok());
let mut both_8 = Vec::new();
both_8.extend_from_slice(&[0u8, 0, 0, 0x14]);
both_8.extend_from_slice(b"roll");
both_8.extend_from_slice(&0u32.to_be_bytes());
assert!(super::parse_csgp(&both_8).is_ok());
}
#[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, 1 << 30).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, 1 << 30).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, 1 << 30).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, 1 << 30).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 build_pdin_box_round_trips() {
let record = super::PdinRecord {
entries: vec![
super::PdinEntry {
rate: 125_000,
initial_delay: 2_500,
},
super::PdinEntry {
rate: 250_000,
initial_delay: 1_200,
},
],
};
let boxed = super::build_pdin_box(&record);
assert_eq!(&boxed[4..8], b"pdin");
let r = super::parse_pdin_box(&boxed[8..]).unwrap();
assert_eq!(r.entries, record.entries);
}
#[test]
fn build_pdin_box_empty() {
let boxed = super::build_pdin_box(&super::PdinRecord { entries: vec![] });
assert_eq!(boxed.len(), 12);
let r = super::parse_pdin_box(&boxed[8..]).unwrap();
assert!(r.entries.is_empty());
}
#[test]
fn pnot_box_round_trips() {
let r = super::PnotRecord {
modification_date: 0xC0FF_EE00,
version: 0,
atom_type: *b"PICT",
atom_index: 1,
};
let boxed = super::build_pnot_box(&r);
assert_eq!(&boxed[4..8], b"pnot");
assert_eq!(boxed.len(), 20); let back = super::parse_pnot_box(&boxed[8..]).expect("round-trip");
assert_eq!(back, r);
}
#[test]
fn pnot_box_too_short_is_rejected() {
assert!(super::parse_pnot(&[0u8; 11]).is_none());
assert!(super::parse_pnot_box(&[0u8; 11]).is_err());
}
#[test]
fn build_prft_box_v0_round_trips() {
let record = super::PrftRecord {
reference_track_id: 1,
ntp_timestamp: 0x1234_5678_9ABC_DEF0,
media_time: 90_000,
version: 0,
flags: 0x00_0001, };
let boxed = super::build_prft_box(&record).unwrap();
assert_eq!(&boxed[4..8], b"prft");
let r = super::parse_prft_box(&boxed[8..]).unwrap().unwrap();
assert_eq!(r.reference_track_id, 1);
assert_eq!(r.ntp_timestamp, 0x1234_5678_9ABC_DEF0);
assert_eq!(r.media_time, 90_000);
assert_eq!(r.version, 0);
assert_eq!(r.flags, 0x00_0001);
assert!(r.is_encoder_input_output());
}
#[test]
fn build_prft_box_v1_round_trips() {
let record = super::PrftRecord {
reference_track_id: 3,
ntp_timestamp: 0xDEAD_BEEF_CAFE_F00D,
media_time: 0x1_0000_0001,
version: 1,
flags: 0,
};
let boxed = super::build_prft_box(&record).unwrap();
let r = super::parse_prft_box(&boxed[8..]).unwrap().unwrap();
assert_eq!(r.version, 1);
assert_eq!(r.media_time, 0x1_0000_0001);
assert_eq!(r.reference_track_id, 3);
}
#[test]
fn build_prft_box_rejects_inconsistent() {
let overflow = super::PrftRecord {
reference_track_id: 1,
ntp_timestamp: 0,
media_time: 0x1_0000_0000,
version: 0,
flags: 0,
};
assert!(super::build_prft_box(&overflow).is_none());
let bad_version = super::PrftRecord {
version: 2,
..overflow
};
assert!(super::build_prft_box(&bad_version).is_none());
}
#[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 parse_assp_v0_single_offset() {
let mut body = vec![0u8; 4]; body.extend_from_slice(&(-5i32).to_be_bytes());
let r = super::parse_assp(&body).unwrap();
assert_eq!(r.version, 0);
assert_eq!(r.entries.len(), 1);
assert_eq!(r.entries[0].grouping_type_parameter, None);
assert_eq!(r.entries[0].min_initial_alt_startup_offset, -5);
}
#[test]
fn parse_assp_v1_keyed_entries() {
let mut body = vec![1u8, 0, 0, 0]; body.extend_from_slice(&2u32.to_be_bytes()); body.extend_from_slice(&7u32.to_be_bytes());
body.extend_from_slice(&(-3i32).to_be_bytes());
body.extend_from_slice(&9u32.to_be_bytes());
body.extend_from_slice(&100i32.to_be_bytes());
let r = super::parse_assp(&body).unwrap();
assert_eq!(r.version, 1);
assert_eq!(r.entries.len(), 2);
assert_eq!(r.entries[0].grouping_type_parameter, Some(7));
assert_eq!(r.entries[0].min_initial_alt_startup_offset, -3);
assert_eq!(r.entries[1].grouping_type_parameter, Some(9));
assert_eq!(r.entries[1].min_initial_alt_startup_offset, 100);
}
#[test]
fn parse_assp_rejects_malformed() {
assert!(super::parse_assp(&[0u8; 3]).is_err());
assert!(super::parse_assp(&[0u8; 4]).is_err());
assert!(super::parse_assp(&[1u8, 0, 0, 0]).is_err());
let mut overrun = vec![1u8, 0, 0, 0];
overrun.extend_from_slice(&4u32.to_be_bytes());
overrun.extend_from_slice(&[0u8; 8]);
assert!(super::parse_assp(&overrun).is_err());
assert!(super::parse_assp(&[2u8, 0, 0, 0, 0, 0, 0, 0]).is_err());
}
#[test]
fn assp_build_parse_round_trip() {
for record in [
super::AsspRecord {
version: 0,
entries: vec![super::AsspEntry {
grouping_type_parameter: None,
min_initial_alt_startup_offset: -42,
}],
},
super::AsspRecord {
version: 1,
entries: vec![
super::AsspEntry {
grouping_type_parameter: Some(0),
min_initial_alt_startup_offset: 0,
},
super::AsspEntry {
grouping_type_parameter: Some(0xDEAD_BEEF),
min_initial_alt_startup_offset: i32::MIN,
},
],
},
] {
let bytes = super::build_assp_box(&record).unwrap();
assert_eq!(&bytes[4..8], b"assp");
assert_eq!(
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize,
bytes.len()
);
let reparsed = super::parse_assp_box(&bytes[8..]).unwrap();
assert_eq!(reparsed, record);
}
}
#[test]
fn assp_build_rejects_invalid_records() {
assert!(super::build_assp_box(&super::AsspRecord {
version: 0,
entries: vec![super::AsspEntry {
grouping_type_parameter: Some(1),
min_initial_alt_startup_offset: 0,
}],
})
.is_err());
assert!(super::build_assp_box(&super::AsspRecord {
version: 0,
entries: vec![
super::AsspEntry {
grouping_type_parameter: None,
min_initial_alt_startup_offset: 0,
};
2
],
})
.is_err());
assert!(super::build_assp_box(&super::AsspRecord {
version: 9,
entries: vec![],
})
.is_err());
}
#[test]
fn parse_trep_decodes_assp_child() {
let mut assp_body = vec![0u8; 4];
assp_body.extend_from_slice(&(-7i32).to_be_bytes());
let assp = build_child_box(b"assp", &assp_body);
let other = build_child_box(b"xtra", &[0; 4]);
let body = build_trep(11, &[assp, other]);
let r = super::parse_trep(&body).unwrap();
assert_eq!(r.children.len(), 2);
let a = r.children[0].assp.as_ref().expect("assp should decode");
assert_eq!(a.version, 0);
assert_eq!(a.entries[0].min_initial_alt_startup_offset, -7);
assert!(r.children[1].assp.is_none());
}
#[test]
fn build_trep_box_round_trips_assp_child() {
let record = super::TrepRecord {
track_id: 5,
children: vec![
super::TrepChild {
fourcc: *b"assp",
payload_len: 0,
assp: Some(super::AsspRecord {
version: 1,
entries: vec![super::AsspEntry {
grouping_type_parameter: Some(2),
min_initial_alt_startup_offset: -8,
}],
}),
},
super::TrepChild {
fourcc: *b"zzzz",
payload_len: 0,
assp: None,
},
],
};
let bytes = super::build_trep_box(&record).unwrap();
assert_eq!(&bytes[4..8], b"trep");
let reparsed = super::parse_trep_box(&bytes[8..]).unwrap();
assert_eq!(reparsed.track_id, 5);
assert_eq!(reparsed.children.len(), 2);
assert_eq!(&reparsed.children[0].fourcc, b"assp");
assert_eq!(reparsed.children[0].assp, record.children[0].assp);
assert_eq!(&reparsed.children[1].fourcc, b"zzzz");
assert_eq!(reparsed.children[1].payload_len, 0);
assert!(reparsed.children[1].assp.is_none());
}
#[test]
fn build_trep_box_rejects_inconsistent_children() {
assert!(super::build_trep_box(&super::TrepRecord {
track_id: 1,
children: vec![super::TrepChild {
fourcc: *b"abcd",
payload_len: 0,
assp: Some(super::AsspRecord {
version: 0,
entries: vec![super::AsspEntry {
grouping_type_parameter: None,
min_initial_alt_startup_offset: 0,
}],
}),
}],
})
.is_err());
assert!(super::build_trep_box(&super::TrepRecord {
track_id: 1,
children: vec![super::TrepChild {
fourcc: *b"abcd",
payload_len: 4,
assp: None,
}],
})
.is_err());
}
#[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);
}
#[test]
fn leva_build_round_trip_all_assignment_types() {
let record = super::LevaRecord {
entries: vec![
super::LevaEntry {
track_id: 1,
padding_flag: true,
assignment_type: 2,
grouping_type: 0,
grouping_type_parameter: 0,
sub_track_id: 0,
},
super::LevaEntry {
track_id: 2,
padding_flag: false,
assignment_type: 0,
grouping_type: u32::from_be_bytes(*b"rash"),
grouping_type_parameter: 0,
sub_track_id: 0,
},
super::LevaEntry {
track_id: 3,
padding_flag: true,
assignment_type: 1,
grouping_type: u32::from_be_bytes(*b"roll"),
grouping_type_parameter: 9,
sub_track_id: 0,
},
super::LevaEntry {
track_id: 4,
padding_flag: false,
assignment_type: 4,
grouping_type: 0,
grouping_type_parameter: 0,
sub_track_id: 77,
},
],
};
let boxed = super::build_leva_box(&record).unwrap();
assert_eq!(&boxed[4..8], b"leva");
assert_eq!(
boxed.len(),
u32::from_be_bytes(boxed[..4].try_into().unwrap()) as usize
);
let parsed = super::parse_leva_box(&boxed[8..]).unwrap();
assert_eq!(parsed.entries, record.entries);
}
#[test]
fn leva_build_rejects_oversized_assignment_type() {
let record = super::LevaRecord {
entries: vec![super::LevaEntry {
track_id: 1,
padding_flag: false,
assignment_type: 200,
grouping_type: 0,
grouping_type_parameter: 0,
sub_track_id: 0,
}],
};
assert!(super::build_leva_box(&record).is_err());
}
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 build_subs_box_v0_round_trips() {
let record = super::SubsBox {
version: 0,
flags: 0x00_aa_55,
entries: vec![
super::SubsEntry {
sample_delta: 1,
subsamples: vec![
super::SubSampleEntry {
subsample_size: 100,
subsample_priority: 5,
discardable: 0,
codec_specific_parameters: 1,
},
super::SubSampleEntry {
subsample_size: 50,
subsample_priority: 4,
discardable: 1,
codec_specific_parameters: 2,
},
],
},
super::SubsEntry {
sample_delta: 2,
subsamples: vec![super::SubSampleEntry {
subsample_size: 80,
subsample_priority: 6,
discardable: 0,
codec_specific_parameters: 0xdead_beef,
}],
},
],
};
let boxed = super::build_subs_box(&record).unwrap();
assert_eq!(&boxed[4..8], b"subs");
let s = super::parse_subs_box(&boxed[8..]).unwrap();
assert_eq!(s.version, 0);
assert_eq!(s.flags, 0x00_aa_55);
assert_eq!(s.entries.len(), 2);
assert_eq!(s.entries[0].subsamples[1].subsample_size, 50);
assert_eq!(s.entries[0].subsamples[1].discardable, 1);
assert_eq!(
s.entries[1].subsamples[0].codec_specific_parameters,
0xdead_beef
);
}
#[test]
fn build_subs_box_v1_wide_size() {
let record = super::SubsBox {
version: 1,
flags: 0,
entries: vec![super::SubsEntry {
sample_delta: 1,
subsamples: vec![super::SubSampleEntry {
subsample_size: 0x0001_2345,
subsample_priority: 0,
discardable: 0,
codec_specific_parameters: 0,
}],
}],
};
let boxed = super::build_subs_box(&record).unwrap();
let s = super::parse_subs_box(&boxed[8..]).unwrap();
assert_eq!(s.version, 1);
assert_eq!(s.entries[0].subsamples[0].subsample_size, 0x0001_2345);
}
#[test]
fn build_subs_box_empty_and_degenerate() {
let empty = super::build_subs_box(&super::SubsBox::default()).unwrap();
assert!(super::parse_subs_box(&empty[8..])
.unwrap()
.entries
.is_empty());
let degenerate = super::SubsBox {
version: 0,
flags: 0,
entries: vec![super::SubsEntry {
sample_delta: 5,
subsamples: vec![],
}],
};
let boxed = super::build_subs_box(°enerate).unwrap();
let s = super::parse_subs_box(&boxed[8..]).unwrap();
assert_eq!(s.entries[0].sample_delta, 5);
assert!(s.entries[0].subsamples.is_empty());
}
#[test]
fn build_subs_box_rejects_inconsistent() {
let overflow = super::SubsBox {
version: 0,
flags: 0,
entries: vec![super::SubsEntry {
sample_delta: 1,
subsamples: vec![super::SubSampleEntry {
subsample_size: 0x0001_0000,
subsample_priority: 0,
discardable: 0,
codec_specific_parameters: 0,
}],
}],
};
assert!(super::build_subs_box(&overflow).is_none());
let bad_version = super::SubsBox {
version: 2,
..super::SubsBox::default()
};
assert!(super::build_subs_box(&bad_version).is_none());
}
#[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, 1 << 30).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, 1 << 30).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 build_saiz_box_constant_size_round_trips() {
let record = super::SaizBox {
aux_info_type: None,
aux_info_type_parameter: None,
default_sample_info_size: 8,
sample_count: 4,
per_sample: vec![],
};
let boxed = super::build_saiz_box(&record).unwrap();
assert_eq!(&boxed[4..8], b"saiz");
let s = super::parse_saiz_box(&boxed[8..]).unwrap();
assert_eq!(s.default_sample_info_size, 8);
assert_eq!(s.sample_count, 4);
assert!(s.aux_info_type.is_none());
assert!(s.per_sample.is_empty());
}
#[test]
fn build_saiz_box_variable_with_aux_type() {
let record = super::SaizBox {
aux_info_type: Some(*b"cenc"),
aux_info_type_parameter: Some(7),
default_sample_info_size: 0,
sample_count: 3,
per_sample: vec![10, 20, 30],
};
let boxed = super::build_saiz_box(&record).unwrap();
let s = super::parse_saiz_box(&boxed[8..]).unwrap();
assert_eq!(s.aux_info_type, Some(*b"cenc"));
assert_eq!(s.aux_info_type_parameter, Some(7));
assert_eq!(s.sample_count, 3);
assert_eq!(s.per_sample, vec![10, 20, 30]);
}
#[test]
fn build_saiz_box_rejects_inconsistent() {
let both = super::SaizBox {
default_sample_info_size: 8,
per_sample: vec![1],
..Default::default()
};
assert!(super::build_saiz_box(&both).is_none());
let count_mismatch = super::SaizBox {
default_sample_info_size: 0,
sample_count: 5,
per_sample: vec![1, 2],
..Default::default()
};
assert!(super::build_saiz_box(&count_mismatch).is_none());
}
#[test]
fn build_saio_box_v0_and_v1_round_trip() {
let v0 = super::SaioBox {
version: 0,
aux_info_type: None,
aux_info_type_parameter: None,
offsets: vec![0x1000, 0x2000],
};
let boxed = super::build_saio_box(&v0).unwrap();
assert_eq!(&boxed[4..8], b"saio");
let s = super::parse_saio_box(&boxed[8..]).unwrap();
assert_eq!(s.version, 0);
assert_eq!(s.offsets, vec![0x1000, 0x2000]);
let v1 = super::SaioBox {
version: 1,
aux_info_type: Some(*b"cenc"),
aux_info_type_parameter: Some(7),
offsets: vec![0x1_0000_0000, 0xffff_ffff_ffff_0000],
};
let boxed = super::build_saio_box(&v1).unwrap();
let s = super::parse_saio_box(&boxed[8..]).unwrap();
assert_eq!(s.version, 1);
assert_eq!(s.aux_info_type, Some(*b"cenc"));
assert_eq!(s.aux_info_type_parameter, Some(7));
assert_eq!(s.offsets, vec![0x1_0000_0000, 0xffff_ffff_ffff_0000]);
}
#[test]
fn build_saio_box_rejects_inconsistent() {
let overflow = super::SaioBox {
version: 0,
aux_info_type: None,
aux_info_type_parameter: None,
offsets: vec![0x1_0000_0000],
};
assert!(super::build_saio_box(&overflow).is_none());
let bad_version = super::SaioBox {
version: 2,
..Default::default()
};
assert!(super::build_saio_box(&bad_version).is_none());
}
#[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, 1 << 30).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,
&mut Vec::new(),
&mut Vec::new(),
&mut Vec::new(),
&mut Vec::new(),
&mut (1 << 30),
)
.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,
&mut Vec::new(),
&mut Vec::new(),
&mut Vec::new(),
&mut Vec::new(),
&mut (1 << 30),
)
.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,
&mut Vec::new(),
&mut Vec::new(),
&mut Vec::new(),
&mut Vec::new(),
&mut (1 << 30),
)
.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,
&mut Vec::new(),
&mut Vec::new(),
&mut Vec::new(),
&mut Vec::new(),
&mut (1 << 30),
)
.expect("walk succeeds despite bad pssh");
assert!(moof_psshes.is_empty(), "malformed pssh dropped silently");
}
#[test]
fn parse_traf_collects_fragment_local_sample_groups() {
let mut tfhd_body = vec![0u8, 0, 0, 0];
tfhd_body.extend_from_slice(&1u32.to_be_bytes());
let tfhd = wrap_box(b"tfhd", &tfhd_body);
let sgpd =
crate::sample_groups::build_sgpd(&crate::sample_groups::SampleGroupDescription {
grouping_type: *b"seig",
default_sample_description_index: None,
entries: vec![vec![0x00, 0x01]],
});
let sbgp = crate::sample_groups::build_sbgp(&crate::sample_groups::SampleToGroup {
grouping_type: *b"roll",
grouping_type_parameter: None,
entries: vec![(2, 1)],
});
let csgp = crate::sample_groups::build_csgp(&crate::sample_groups::CompactSampleToGroup {
grouping_type: *b"seig",
grouping_type_parameter: None,
index_msb_indicates_fragment_local_description: true,
patterns: vec![crate::sample_groups::CompactSampleToGroupPattern {
sample_count: 2,
indices: vec![0x81],
}],
});
let mut traf_body = Vec::new();
traf_body.extend_from_slice(&tfhd);
traf_body.extend_from_slice(&sgpd);
traf_body.extend_from_slice(&sbgp);
traf_body.extend_from_slice(&csgp);
let mut track = fresh_track();
track.track_id = 1;
let tracks = [track];
let mut samples: Vec<super::SampleRef> = Vec::new();
let mut next_dts: Vec<i64> = vec![0];
let mut senc: Vec<super::SencRecord> = Vec::new();
let mut sai: Vec<super::SaiRecord> = Vec::new();
let mut groups: Vec<super::TrafSampleGroupRecord> = Vec::new();
super::parse_traf(
&traf_body,
0,
&tracks,
&mut samples,
&mut next_dts,
9,
&mut senc,
&mut sai,
&mut groups,
&mut Vec::new(),
&mut Vec::new(),
&mut (1 << 30),
)
.expect("traf walk succeeds");
assert_eq!(groups.len(), 1);
let r = &groups[0];
assert_eq!(r.track_idx, 0);
assert_eq!(r.moof_sequence, 9);
assert_eq!(r.sgpd.len(), 1);
assert_eq!(r.sgpd[0].grouping_type, *b"seig");
assert_eq!(r.sgpd[0].entries, vec![vec![0x00, 0x01]]);
assert_eq!(r.sbgp.len(), 1);
assert_eq!(r.sbgp[0].grouping_type, *b"roll");
assert_eq!(r.sbgp[0].entries, vec![(2, 1)]);
assert_eq!(r.csgp.len(), 1);
assert_eq!(r.csgp[0].grouping_type, *b"seig");
assert!(r.csgp[0].index_msb_indicates_fragment_local_description);
assert_eq!(r.csgp[0].patterns[0].sample_count, 2);
assert_eq!(r.csgp[0].patterns[0].indices, vec![0x81]);
let resolved = r.csgp[0].patterns[0]
.resolve_index(0, true, r.csgp[0].index_field_bits)
.unwrap();
assert!(resolved.fragment_local);
assert_eq!(resolved.value, 1);
}
#[test]
fn parse_traf_without_sample_groups_records_nothing() {
let mut tfhd_body = vec![0u8, 0, 0, 0];
tfhd_body.extend_from_slice(&1u32.to_be_bytes());
let traf_body = wrap_box(b"tfhd", &tfhd_body);
let mut track = fresh_track();
track.track_id = 1;
let tracks = [track];
let mut samples: Vec<super::SampleRef> = Vec::new();
let mut next_dts: Vec<i64> = vec![0];
let mut senc: Vec<super::SencRecord> = Vec::new();
let mut sai: Vec<super::SaiRecord> = Vec::new();
let mut groups: Vec<super::TrafSampleGroupRecord> = Vec::new();
super::parse_traf(
&traf_body,
0,
&tracks,
&mut samples,
&mut next_dts,
1,
&mut senc,
&mut sai,
&mut groups,
&mut Vec::new(),
&mut Vec::new(),
&mut (1 << 30),
)
.expect("traf walk succeeds");
assert!(groups.is_empty());
}
#[test]
fn parse_aux_info_run_plain_ivs() {
let buf: Vec<u8> = (0..16).collect();
let senc = super::parse_aux_info_run(&buf, &[8, 8], 8).expect("parses");
assert_eq!(senc.flags, 0);
assert_eq!(senc.samples.len(), 2);
assert_eq!(
senc.samples[0].initialization_vector,
(0..8).collect::<Vec<u8>>()
);
assert_eq!(
senc.samples[1].initialization_vector,
(8..16).collect::<Vec<u8>>()
);
assert!(senc.samples[1].subsamples.is_empty());
}
#[test]
fn parse_aux_info_run_subsample_cell() {
let mut buf = vec![0xAA; 8];
buf.extend_from_slice(&1u16.to_be_bytes());
buf.extend_from_slice(&7u16.to_be_bytes());
buf.extend_from_slice(&9u32.to_be_bytes());
let senc = super::parse_aux_info_run(&buf, &[16], 8).expect("parses");
assert_eq!(senc.flags, 0x0000_0002);
assert_eq!(senc.samples[0].subsamples.len(), 1);
assert_eq!(senc.samples[0].subsamples[0].bytes_of_clear_data, 7);
assert_eq!(senc.samples[0].subsamples[0].bytes_of_protected_data, 9);
}
#[test]
fn parse_aux_info_run_rejects_malformed_cells() {
assert!(super::parse_aux_info_run(&[0u8; 12], &[12], 8).is_none());
assert!(super::parse_aux_info_run(&[0u8; 4], &[4], 8).is_none());
assert!(super::parse_aux_info_run(&[0u8; 8], &[8, 8], 8).is_none());
let mut buf = vec![0u8; 8];
buf.extend_from_slice(&9u16.to_be_bytes()); assert!(super::parse_aux_info_run(&buf, &[10], 8).is_none());
}
#[test]
fn parse_aux_info_run_zero_size_cells() {
let senc = super::parse_aux_info_run(&[], &[0, 0], 0).expect("parses");
assert_eq!(senc.samples.len(), 2);
assert!(senc.samples[0].initialization_vector.is_empty());
assert!(senc.samples[0].subsamples.is_empty());
assert_eq!(senc.flags, 0);
}
#[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)
);
}
fn box_bytes(fourcc: &[u8; 4], body: &[u8]) -> Vec<u8> {
let total = (8 + body.len()) as u32;
let mut v = total.to_be_bytes().to_vec();
v.extend_from_slice(fourcc);
v.extend_from_slice(body);
v
}
#[test]
fn parse_iloc_v0_single_extent() {
let mut b = vec![0u8, 0, 0, 0]; b.push(0x44); b.push(0x00); b.extend_from_slice(&1u16.to_be_bytes()); b.extend_from_slice(&1u16.to_be_bytes()); b.extend_from_slice(&0u16.to_be_bytes()); b.extend_from_slice(&1u16.to_be_bytes()); b.extend_from_slice(&0x1000u32.to_be_bytes()); b.extend_from_slice(&0x40u32.to_be_bytes()); let iloc = super::parse_iloc_box(&b).expect("iloc parses");
assert_eq!(iloc.version, 0);
assert_eq!(iloc.offset_size, 4);
assert_eq!(iloc.length_size, 4);
assert_eq!(iloc.base_offset_size, 0);
assert_eq!(iloc.items.len(), 1);
let it = &iloc.items[0];
assert_eq!(it.item_id, 1);
assert_eq!(it.construction_method, 0);
assert_eq!(it.base_offset, 0);
assert_eq!(it.extents.len(), 1);
assert_eq!(it.extents[0].extent_offset, 0x1000);
assert_eq!(it.extents[0].extent_length, 0x40);
}
#[test]
fn parse_iloc_v1_construction_method_and_index() {
let mut b = vec![1u8, 0, 0, 0];
b.push(0x88); b.push(0x44); b.extend_from_slice(&1u16.to_be_bytes()); b.extend_from_slice(&7u16.to_be_bytes()); b.extend_from_slice(&0x0001u16.to_be_bytes()); b.extend_from_slice(&0u16.to_be_bytes()); b.extend_from_slice(&0x10u32.to_be_bytes()); b.extend_from_slice(&1u16.to_be_bytes()); b.extend_from_slice(&2u32.to_be_bytes()); b.extend_from_slice(&0x20u64.to_be_bytes()); b.extend_from_slice(&0x30u64.to_be_bytes()); let iloc = super::parse_iloc_box(&b).expect("iloc v1 parses");
assert_eq!(iloc.version, 1);
assert_eq!(iloc.index_size, 4);
let it = &iloc.items[0];
assert_eq!(it.item_id, 7);
assert_eq!(it.construction_method, 1);
assert_eq!(it.base_offset, 0x10);
assert_eq!(it.extents[0].extent_index, 2);
assert_eq!(it.extents[0].extent_offset, 0x20);
assert_eq!(it.extents[0].extent_length, 0x30);
}
#[test]
fn parse_iloc_v2_32bit_item_id() {
let mut b = vec![2u8, 0, 0, 0];
b.push(0x44); b.push(0x00); b.extend_from_slice(&1u32.to_be_bytes()); b.extend_from_slice(&0x0001_0000u32.to_be_bytes()); b.extend_from_slice(&0x0000u16.to_be_bytes()); b.extend_from_slice(&0u16.to_be_bytes()); b.extend_from_slice(&1u16.to_be_bytes()); b.extend_from_slice(&0x55u32.to_be_bytes()); b.extend_from_slice(&0x66u32.to_be_bytes()); let iloc = super::parse_iloc_box(&b).expect("iloc v2 parses");
assert_eq!(iloc.items[0].item_id, 0x0001_0000);
assert_eq!(iloc.items[0].extents[0].extent_offset, 0x55);
}
#[test]
fn parse_iloc_rejects_bad_field_width() {
let mut b = vec![0u8, 0, 0, 0];
b.push(0x54); b.push(0x00);
b.extend_from_slice(&0u16.to_be_bytes());
assert!(super::parse_iloc_box(&b).is_none());
}
#[test]
fn parse_pitm_v0_and_v1() {
let mut v0 = vec![0u8, 0, 0, 0];
v0.extend_from_slice(&0x1234u16.to_be_bytes());
assert_eq!(super::parse_pitm_box(&v0), Some(0x1234));
let mut v1 = vec![1u8, 0, 0, 0];
v1.extend_from_slice(&0x0001_2345u32.to_be_bytes());
assert_eq!(super::parse_pitm_box(&v1), Some(0x0001_2345));
}
#[test]
fn parse_iinf_v0_with_infe_v2() {
let mut infe_body = vec![2u8, 0, 0, 0];
infe_body.extend_from_slice(&1u16.to_be_bytes());
infe_body.extend_from_slice(&0u16.to_be_bytes());
infe_body.extend_from_slice(b"hvc1");
infe_body.extend_from_slice(b"image\0");
let infe = box_bytes(b"infe", &infe_body);
let mut iinf_body = vec![0u8, 0, 0, 0];
iinf_body.extend_from_slice(&1u16.to_be_bytes()); iinf_body.extend_from_slice(&infe);
let iinf = super::parse_iinf_box(&iinf_body).expect("iinf parses");
assert_eq!(iinf.entries.len(), 1);
let e = &iinf.entries[0];
assert_eq!(e.item_id, 1);
assert_eq!(&e.item_type, b"hvc1");
assert_eq!(e.item_name, "image");
assert_eq!(e.version, 2);
}
#[test]
fn parse_infe_v0_name_and_content_type() {
let mut body = vec![0u8, 0, 0, 0];
body.extend_from_slice(&5u16.to_be_bytes());
body.extend_from_slice(&0u16.to_be_bytes());
body.extend_from_slice(b"thumb\0");
body.extend_from_slice(b"image/jpeg\0");
let infe = box_bytes(b"infe", &body);
let mut iinf_body = vec![0u8, 0, 0, 0];
iinf_body.extend_from_slice(&1u16.to_be_bytes());
iinf_body.extend_from_slice(&infe);
let iinf = super::parse_iinf_box(&iinf_body).unwrap();
let e = &iinf.entries[0];
assert_eq!(e.item_id, 5);
assert_eq!(e.item_name, "thumb");
assert_eq!(e.content_type, "image/jpeg");
assert_eq!(e.version, 0);
assert_eq!(&e.item_type, &[0u8; 4]);
}
#[test]
fn parse_iref_v0_groups() {
let mut dimg = 1u16.to_be_bytes().to_vec();
dimg.extend_from_slice(&2u16.to_be_bytes()); dimg.extend_from_slice(&2u16.to_be_bytes());
dimg.extend_from_slice(&3u16.to_be_bytes());
let dimg = box_bytes(b"dimg", &dimg);
let mut thmb = 4u16.to_be_bytes().to_vec();
thmb.extend_from_slice(&1u16.to_be_bytes());
thmb.extend_from_slice(&1u16.to_be_bytes());
let thmb = box_bytes(b"thmb", &thmb);
let mut body = vec![0u8, 0, 0, 0]; body.extend_from_slice(&dimg);
body.extend_from_slice(&thmb);
let iref = super::parse_iref_box(&body).expect("iref parses");
assert_eq!(iref.version, 0);
assert_eq!(iref.references.len(), 2);
assert_eq!(&iref.references[0].reference_type, b"dimg");
assert_eq!(iref.references[0].from_item_id, 1);
assert_eq!(iref.references[0].to_item_ids, vec![2, 3]);
assert_eq!(&iref.references[1].reference_type, b"thmb");
assert_eq!(iref.references[1].from_item_id, 4);
assert_eq!(iref.references[1].to_item_ids, vec![1]);
}
#[test]
fn parse_iref_v1_32bit_ids() {
let mut g = 0x0001_0000u32.to_be_bytes().to_vec();
g.extend_from_slice(&1u16.to_be_bytes()); g.extend_from_slice(&0x0002_0000u32.to_be_bytes());
let g = box_bytes(b"cdsc", &g);
let mut body = vec![1u8, 0, 0, 0]; body.extend_from_slice(&g);
let iref = super::parse_iref_box(&body).unwrap();
assert_eq!(iref.version, 1);
assert_eq!(iref.references[0].from_item_id, 0x0001_0000);
assert_eq!(iref.references[0].to_item_ids, vec![0x0002_0000]);
}
#[test]
fn parse_meta_items_full_heif_meta() {
let mut hdlr = vec![0u8, 0, 0, 0]; hdlr.extend_from_slice(&[0, 0, 0, 0]); hdlr.extend_from_slice(b"pict"); hdlr.extend_from_slice(&[0u8; 12]); hdlr.push(0); let hdlr = box_bytes(b"hdlr", &hdlr);
let mut pitm = vec![0u8, 0, 0, 0];
pitm.extend_from_slice(&1u16.to_be_bytes());
let pitm = box_bytes(b"pitm", &pitm);
let mut iloc_body = vec![0u8, 0, 0, 0];
iloc_body.push(0x44);
iloc_body.push(0x00);
iloc_body.extend_from_slice(&1u16.to_be_bytes()); iloc_body.extend_from_slice(&1u16.to_be_bytes()); iloc_body.extend_from_slice(&0u16.to_be_bytes()); iloc_body.extend_from_slice(&1u16.to_be_bytes()); iloc_body.extend_from_slice(&0x100u32.to_be_bytes());
iloc_body.extend_from_slice(&0x10u32.to_be_bytes());
let iloc = box_bytes(b"iloc", &iloc_body);
let mut infe_body = vec![2u8, 0, 0, 0];
infe_body.extend_from_slice(&1u16.to_be_bytes());
infe_body.extend_from_slice(&0u16.to_be_bytes());
infe_body.extend_from_slice(b"hvc1");
infe_body.extend_from_slice(b"\0");
let infe = box_bytes(b"infe", &infe_body);
let mut iinf_body = vec![0u8, 0, 0, 0];
iinf_body.extend_from_slice(&1u16.to_be_bytes());
iinf_body.extend_from_slice(&infe);
let iinf = box_bytes(b"iinf", &iinf_body);
let mut dimg = 1u16.to_be_bytes().to_vec();
dimg.extend_from_slice(&1u16.to_be_bytes());
dimg.extend_from_slice(&2u16.to_be_bytes());
let dimg = box_bytes(b"dimg", &dimg);
let mut iref_body = vec![0u8, 0, 0, 0];
iref_body.extend_from_slice(&dimg);
let iref = box_bytes(b"iref", &iref_body);
let mut meta = vec![0u8, 0, 0, 0];
meta.extend_from_slice(&hdlr);
meta.extend_from_slice(&pitm);
meta.extend_from_slice(&iloc);
meta.extend_from_slice(&iinf);
meta.extend_from_slice(&iref);
let mi = super::parse_meta_items(&meta);
assert_eq!(&mi.handler_type, b"pict");
assert_eq!(mi.primary_item_id, Some(1));
assert_eq!(mi.iloc.as_ref().unwrap().items.len(), 1);
assert_eq!(mi.iinf.as_ref().unwrap().entries[0].item_name, "");
assert_eq!(&mi.iinf.as_ref().unwrap().entries[0].item_type, b"hvc1");
assert_eq!(mi.iref.as_ref().unwrap().references.len(), 1);
assert!(!mi.is_empty());
}
#[test]
fn meta_walk_surfaces_fiin_fd_item_info() {
use crate::fd::{FiinBox, FparBox, PartitionEntry};
let mut hdlr = vec![0u8, 0, 0, 0];
hdlr.extend_from_slice(&[0, 0, 0, 0]);
hdlr.extend_from_slice(b"fdel");
hdlr.extend_from_slice(&[0u8; 12]);
hdlr.push(0);
let hdlr = box_bytes(b"hdlr", &hdlr);
let fiin = FiinBox {
partition_entries: vec![PartitionEntry {
fpar: Some(FparBox {
version: 0,
item_id: 1,
packet_payload_size: 1400,
fec_encoding_id: 0,
fec_instance_id: 0,
max_source_block_length: 16,
encoding_symbol_length: 1024,
max_number_of_encoding_symbols: 16,
scheme_specific_info: String::new(),
entries: vec![],
}),
fecr: None,
fire: None,
}],
session_info: None,
group_id_to_name: None,
};
let fiin_bytes = crate::fd::build_fiin_box(&fiin);
let mut meta = vec![0u8, 0, 0, 0];
meta.extend_from_slice(&hdlr);
meta.extend_from_slice(&fiin_bytes);
let mi = super::parse_meta_items(&meta);
assert_eq!(&mi.handler_type, b"fdel");
let parsed = mi.fiin.as_ref().expect("fiin parsed");
assert_eq!(parsed.partition_entries.len(), 1);
assert_eq!(parsed, &fiin);
assert!(!mi.is_empty());
let mut kv = Vec::new();
super::surface_meta_items(&mi, &mut kv);
assert!(kv
.iter()
.any(|(k, v)| k == "meta_fiin_partitions" && v == "1"));
}
#[test]
fn mere_round_trips() {
let r = super::MereRelation {
first_metabox_handler_type: *b"mp7t",
second_metabox_handler_type: *b"mdir",
metabox_relation: 5,
};
let bytes = super::build_mere_box(&r);
assert_eq!(&bytes[4..8], b"mere");
let body = &bytes[8..];
assert_eq!(body.len(), 13);
assert_eq!(super::parse_mere_box(body).unwrap(), r);
}
#[test]
fn mere_truncated_rejected() {
assert!(super::parse_mere_box(&[0, 0, 0, 0, b'a', b'b', b'c', b'd']).is_none());
}
#[test]
fn meco_walk_collects_metas_and_relations() {
fn meta_with_handler(h: &[u8; 4]) -> Vec<u8> {
let mut hdlr = vec![0u8, 0, 0, 0];
hdlr.extend_from_slice(&[0, 0, 0, 0]);
hdlr.extend_from_slice(h);
hdlr.extend_from_slice(&[0u8; 12]);
hdlr.push(0);
let hdlr = box_bytes(b"hdlr", &hdlr);
let mut meta = vec![0u8, 0, 0, 0];
meta.extend_from_slice(&hdlr);
box_bytes(b"meta", &meta)
}
let meta_a = meta_with_handler(b"mp7t");
let meta_b = meta_with_handler(b"mdir");
let mere = super::build_mere_box(&super::MereRelation {
first_metabox_handler_type: *b"mp7t",
second_metabox_handler_type: *b"mdir",
metabox_relation: 3,
});
let mut meco_body = Vec::new();
meco_body.extend_from_slice(&meta_a);
meco_body.extend_from_slice(&meta_b);
meco_body.extend_from_slice(&mere);
let meco = super::parse_meco_box(&meco_body);
assert!(!meco.is_empty());
assert_eq!(meco.metas.len(), 2);
assert_eq!(&meco.metas[0].handler_type, b"mp7t");
assert_eq!(&meco.metas[1].handler_type, b"mdir");
assert_eq!(meco.relations.len(), 1);
assert_eq!(meco.relations[0].metabox_relation, 3);
assert_eq!(&meco.relations[0].first_metabox_handler_type, b"mp7t");
}
#[test]
fn meco_empty_body_is_empty() {
assert!(super::parse_meco_box(&[]).is_empty());
}
#[test]
fn meta_items_idat_resolution_and_lookups() {
let idat_payload: Vec<u8> = (0u8..16).collect();
let idat = box_bytes(b"idat", &idat_payload);
let mut iloc = vec![1u8, 0, 0, 0]; iloc.push(0x44); iloc.push(0x00); iloc.extend_from_slice(&1u16.to_be_bytes()); iloc.extend_from_slice(&1u16.to_be_bytes()); iloc.extend_from_slice(&0x0001u16.to_be_bytes()); iloc.extend_from_slice(&0u16.to_be_bytes()); iloc.extend_from_slice(&1u16.to_be_bytes()); iloc.extend_from_slice(&4u32.to_be_bytes()); iloc.extend_from_slice(&6u32.to_be_bytes()); let iloc = box_bytes(b"iloc", &iloc);
let mut infe = vec![2u8, 0, 0, 0];
infe.extend_from_slice(&1u16.to_be_bytes());
infe.extend_from_slice(&0u16.to_be_bytes());
infe.extend_from_slice(b"Exif");
infe.extend_from_slice(b"exif\0");
let infe = box_bytes(b"infe", &infe);
let mut iinf = vec![0u8, 0, 0, 0];
iinf.extend_from_slice(&1u16.to_be_bytes());
iinf.extend_from_slice(&infe);
let iinf = box_bytes(b"iinf", &iinf);
let mut meta = vec![0u8, 0, 0, 0];
meta.extend_from_slice(&iloc);
meta.extend_from_slice(&iinf);
meta.extend_from_slice(&idat);
let mi = super::parse_meta_items(&meta);
assert_eq!(mi.idat.len(), 16);
assert_eq!(mi.item_data_from_idat(1), Some(vec![4u8, 5, 6, 7, 8, 9]));
let ranges = mi.item_byte_ranges(1).unwrap();
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].offset, 4);
assert_eq!(ranges[0].length, 6);
assert_eq!(mi.iloc_item(1).unwrap().construction_method, 1);
assert_eq!(mi.item_info(1).unwrap().item_name, "exif");
assert!(mi.iloc_item(99).is_none());
assert!(mi.item_data_from_idat(99).is_none());
}
#[test]
fn meta_items_idat_overrun_rejected() {
let idat = box_bytes(b"idat", &[1u8, 2, 3, 4]);
let mut iloc = vec![1u8, 0, 0, 0];
iloc.push(0x44);
iloc.push(0x00);
iloc.extend_from_slice(&1u16.to_be_bytes());
iloc.extend_from_slice(&1u16.to_be_bytes());
iloc.extend_from_slice(&0x0001u16.to_be_bytes()); iloc.extend_from_slice(&0u16.to_be_bytes());
iloc.extend_from_slice(&1u16.to_be_bytes());
iloc.extend_from_slice(&2u32.to_be_bytes()); iloc.extend_from_slice(&10u32.to_be_bytes()); let iloc = box_bytes(b"iloc", &iloc);
let mut meta = vec![0u8, 0, 0, 0];
meta.extend_from_slice(&iloc);
meta.extend_from_slice(&idat);
let mi = super::parse_meta_items(&meta);
assert!(mi.item_data_from_idat(1).is_none());
}
fn box_body(built: &[u8]) -> &[u8] {
&built[8..]
}
#[test]
fn build_iloc_round_trips_v0_v1_v2() {
let cases = [
super::IlocBox {
version: 0,
offset_size: 4,
length_size: 4,
base_offset_size: 0,
index_size: 0,
items: vec![super::IlocItem {
item_id: 3,
construction_method: 0,
data_reference_index: 0,
base_offset: 0,
extents: vec![super::IlocExtent {
extent_index: 0,
extent_offset: 0x1234,
extent_length: 0x56,
}],
}],
},
super::IlocBox {
version: 1,
offset_size: 8,
length_size: 8,
base_offset_size: 4,
index_size: 4,
items: vec![super::IlocItem {
item_id: 9,
construction_method: 1,
data_reference_index: 2,
base_offset: 0x40,
extents: vec![super::IlocExtent {
extent_index: 5,
extent_offset: 0x9999,
extent_length: 0x1000,
}],
}],
},
super::IlocBox {
version: 2,
offset_size: 4,
length_size: 4,
base_offset_size: 0,
index_size: 0,
items: vec![super::IlocItem {
item_id: 0x0010_0000,
construction_method: 0,
data_reference_index: 0,
base_offset: 0,
extents: vec![super::IlocExtent {
extent_index: 0,
extent_offset: 7,
extent_length: 8,
}],
}],
},
];
for c in cases {
let built = super::build_iloc_box(&c).expect("build");
let parsed = super::parse_iloc_box(box_body(&built)).expect("parse");
assert_eq!(parsed, c, "iloc v{} round-trip", c.version);
}
}
#[test]
fn build_iloc_rejects_inconsistent_records() {
let bad = super::IlocBox {
version: 0,
offset_size: 4,
length_size: 4,
base_offset_size: 0,
index_size: 0,
items: vec![super::IlocItem {
item_id: 1,
construction_method: 1, data_reference_index: 0,
base_offset: 0,
extents: vec![],
}],
};
assert!(super::build_iloc_box(&bad).is_none());
let bad2 = super::IlocBox {
version: 0,
offset_size: 4,
length_size: 4,
base_offset_size: 0,
index_size: 4,
items: vec![],
};
assert!(super::build_iloc_box(&bad2).is_none());
}
#[test]
fn build_pitm_round_trips() {
let built = super::build_pitm_box(42);
assert_eq!(super::parse_pitm_box(box_body(&built)), Some(42));
let built = super::build_pitm_box(0x0010_0000);
assert_eq!(super::parse_pitm_box(box_body(&built)), Some(0x0010_0000));
}
#[test]
fn build_iinf_round_trips_mixed_versions() {
let b = super::IinfBox {
entries: vec![
super::ItemInfoEntry {
item_id: 1,
protection_index: 0,
item_type: *b"hvc1",
item_name: "primary".to_string(),
content_type: String::new(),
content_encoding: String::new(),
version: 2,
},
super::ItemInfoEntry {
item_id: 2,
protection_index: 0,
item_type: *b"mime",
item_name: "meta".to_string(),
content_type: "application/rdf+xml".to_string(),
content_encoding: String::new(),
version: 2,
},
super::ItemInfoEntry {
item_id: 3,
protection_index: 0,
item_type: [0; 4],
item_name: "legacy".to_string(),
content_type: "image/jpeg".to_string(),
content_encoding: String::new(),
version: 0,
},
],
};
let built = super::build_iinf_box(&b).expect("build");
let parsed = super::parse_iinf_box(box_body(&built)).expect("parse");
assert_eq!(parsed, b);
}
#[test]
fn build_iref_round_trips_v0_v1() {
let v0 = super::IrefBox {
version: 0,
references: vec![
super::ItemReference {
reference_type: *b"dimg",
from_item_id: 1,
to_item_ids: vec![2, 3, 4],
},
super::ItemReference {
reference_type: *b"thmb",
from_item_id: 5,
to_item_ids: vec![1],
},
],
};
let built = super::build_iref_box(&v0).unwrap();
assert_eq!(super::parse_iref_box(box_body(&built)), Some(v0));
let v1 = super::IrefBox {
version: 1,
references: vec![super::ItemReference {
reference_type: *b"cdsc",
from_item_id: 0x0001_0000,
to_item_ids: vec![0x0002_0000],
}],
};
let built = super::build_iref_box(&v1).unwrap();
assert_eq!(super::parse_iref_box(box_body(&built)), Some(v1));
}
#[test]
fn build_idat_round_trips_through_meta_walk() {
let payload: Vec<u8> = (10u8..30).collect();
let idat = super::build_idat_box(&payload);
let iloc = super::build_iloc_box(&super::IlocBox {
version: 1,
offset_size: 4,
length_size: 4,
base_offset_size: 0,
index_size: 0,
items: vec![super::IlocItem {
item_id: 1,
construction_method: 1,
data_reference_index: 0,
base_offset: 0,
extents: vec![super::IlocExtent {
extent_index: 0,
extent_offset: 3,
extent_length: 5,
}],
}],
})
.unwrap();
let mut meta = vec![0u8, 0, 0, 0];
meta.extend_from_slice(&iloc);
meta.extend_from_slice(&idat);
let mi = super::parse_meta_items(&meta);
assert_eq!(mi.item_data_from_idat(1), Some(payload[3..8].to_vec()));
}
#[test]
fn parse_meta_items_plain_itunes_is_empty() {
let mut hdlr = vec![0u8, 0, 0, 0];
hdlr.extend_from_slice(&[0, 0, 0, 0]);
hdlr.extend_from_slice(b"mdir");
hdlr.extend_from_slice(&[0u8; 12]);
hdlr.push(0);
let hdlr = box_bytes(b"hdlr", &hdlr);
let ilst = box_bytes(b"ilst", &[]);
let mut meta = vec![0u8, 0, 0, 0];
meta.extend_from_slice(&hdlr);
meta.extend_from_slice(&ilst);
let mi = super::parse_meta_items(&meta);
assert_eq!(&mi.handler_type, b"mdir");
assert!(mi.is_empty());
}
}