use crate::error::{AvifError as Error, Result};
use crate::box_parser::{b, find_box, iter_boxes, parse_full_box, read_u32, read_u64, BoxType};
use oxideav_heif::props as hprops;
use oxideav_heif::{sequence, HeifFile, HeifFileRef};
const MOOV: BoxType = b(b"moov");
const TRAK: BoxType = b(b"trak");
const STBL: BoxType = b(b"stbl");
const AV01: BoxType = b(b"av01");
const PRFT: BoxType = b(b"prft");
const SSIX: BoxType = b(b"ssix");
pub const NTP_UNIX_EPOCH_OFFSET_SECONDS: u64 = 2_208_988_800;
pub const HANDLER_PICT: BoxType = *b"pict";
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Sample {
pub offset: u64,
pub size: u32,
pub duration: u32,
pub is_sync: bool,
}
impl Sample {
#[allow(clippy::too_many_arguments)]
pub fn new(offset: u64, size: u32, duration: u32, is_sync: bool) -> Self {
Self {
offset,
size,
duration,
is_sync,
}
}
}
#[doc(hidden)]
#[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 {
#[allow(clippy::too_many_arguments)]
pub fn new(
segment_duration: u64,
media_time: i64,
media_rate_integer: i16,
media_rate_fraction: i16,
) -> Self {
Self {
segment_duration,
media_time,
media_rate_integer,
media_rate_fraction,
}
}
}
impl EditListEntry {
pub fn is_empty_edit(&self) -> bool {
self.media_time == -1
}
pub fn is_dwell(&self) -> bool {
self.media_rate_integer == 0
}
pub fn media_time_seconds(&self, media_timescale: u32) -> Option<f64> {
if media_timescale == 0 || self.is_empty_edit() {
None
} else {
Some(self.media_time as f64 / media_timescale as f64)
}
}
pub fn segment_duration_seconds(&self, movie_timescale: u32) -> Option<f64> {
if movie_timescale == 0 {
None
} else {
Some(self.segment_duration as f64 / movie_timescale as f64)
}
}
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ProducerReferenceTime {
pub version: u8,
pub flags: u32,
pub reference_track_id: u32,
pub ntp_timestamp: u64,
pub media_time: u64,
}
impl ProducerReferenceTime {
#[allow(clippy::too_many_arguments)]
pub fn new(
version: u8,
flags: u32,
reference_track_id: u32,
ntp_timestamp: u64,
media_time: u64,
) -> Self {
Self {
version,
flags,
reference_track_id,
ntp_timestamp,
media_time,
}
}
}
impl ProducerReferenceTime {
pub fn is_encoder_input_output(&self) -> bool {
self.flags & 0x000001 != 0
}
pub fn is_finalization_time(&self) -> bool {
self.flags & 0x000002 != 0
}
pub fn is_file_write_time(&self) -> bool {
self.flags & 0x000004 != 0
}
pub fn ntp_seconds(&self) -> u32 {
(self.ntp_timestamp >> 32) as u32
}
pub fn ntp_fraction(&self) -> u32 {
(self.ntp_timestamp & 0xFFFF_FFFF) as u32
}
pub fn unix_seconds(&self) -> Option<f64> {
let secs = self.ntp_seconds() as u64;
let unix_secs = secs.checked_sub(NTP_UNIX_EPOCH_OFFSET_SECONDS)?;
let frac = self.ntp_fraction() as f64 / u64::from(u32::MAX).wrapping_add(1) as f64;
Some(unix_secs as f64 + frac)
}
}
pub fn parse_prft(payload: &[u8]) -> Result<ProducerReferenceTime> {
let (version, flags, rest) = parse_full_box(payload)?;
if version > 1 {
return Err(Error::InvalidData(format!(
"avis: prft version {version} unsupported (expected 0 or 1)"
)));
}
let reference_track_id = read_u32(rest, 0)?;
let ntp_timestamp = read_u64(rest, 4)?;
let media_time = if version == 0 {
u64::from(read_u32(rest, 12)?)
} else {
read_u64(rest, 12)?
};
Ok(ProducerReferenceTime {
version,
flags,
reference_track_id,
ntp_timestamp,
media_time,
})
}
pub fn parse_producer_reference_times(file: &[u8]) -> Vec<ProducerReferenceTime> {
let mut out = Vec::new();
for hdr in iter_boxes(file) {
let Ok(hdr) = hdr else { break };
if hdr.box_type != PRFT {
continue;
}
let payload = &file[hdr.payload_start..hdr.end()];
if let Ok(prft) = parse_prft(payload) {
out.push(prft);
}
}
out
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SubsegmentRange {
pub level: u8,
pub range_size: u32,
}
impl SubsegmentRange {
#[allow(clippy::too_many_arguments)]
pub fn new(level: u8, range_size: u32) -> Self {
Self { level, range_size }
}
}
#[doc(hidden)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SubsegmentIndex {
pub subsegments: Vec<Vec<SubsegmentRange>>,
}
impl SubsegmentIndex {
#[allow(clippy::too_many_arguments)]
pub fn new(subsegments: Vec<Vec<SubsegmentRange>>) -> Self {
Self { subsegments }
}
}
pub fn parse_ssix(payload: &[u8]) -> Result<SubsegmentIndex> {
let (version, _flags, rest) = parse_full_box(payload)?;
if version != 0 {
return Err(Error::InvalidData(format!(
"avis: ssix version {version} unsupported (expected 0)"
)));
}
let subsegment_count = read_u32(rest, 0)?;
let mut subsegments = Vec::with_capacity(subsegment_count.min(1024) as usize);
let mut at = 4usize;
for _ in 0..subsegment_count {
let range_count = read_u32(rest, at)?;
at = at
.checked_add(4)
.ok_or_else(|| Error::InvalidData("avis: ssix offset overflow".to_string()))?;
let mut ranges = Vec::with_capacity(range_count.min(1024) as usize);
for _ in 0..range_count {
let level = *rest
.get(at)
.ok_or_else(|| Error::InvalidData("avis: ssix truncated range".to_string()))?;
let b0 = u32::from(
*rest
.get(at + 1)
.ok_or_else(|| Error::InvalidData("avis: ssix truncated range".to_string()))?,
);
let b1 = u32::from(
*rest
.get(at + 2)
.ok_or_else(|| Error::InvalidData("avis: ssix truncated range".to_string()))?,
);
let b2 = u32::from(
*rest
.get(at + 3)
.ok_or_else(|| Error::InvalidData("avis: ssix truncated range".to_string()))?,
);
let range_size = (b0 << 16) | (b1 << 8) | b2;
ranges.push(SubsegmentRange { level, range_size });
at = at
.checked_add(4)
.ok_or_else(|| Error::InvalidData("avis: ssix offset overflow".to_string()))?;
}
subsegments.push(ranges);
}
Ok(SubsegmentIndex { subsegments })
}
pub fn parse_subsegment_indexes(file: &[u8]) -> Vec<SubsegmentIndex> {
let mut out = Vec::new();
for hdr in iter_boxes(file) {
let Ok(hdr) = hdr else { break };
if hdr.box_type != SSIX {
continue;
}
let payload = &file[hdr.payload_start..hdr.end()];
if let Ok(ssix) = parse_ssix(payload) {
out.push(ssix);
}
}
out
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct AvisMeta {
pub timescale: u32,
pub media_timescale: Option<u32>,
pub display_dims: Option<(u32, u32)>,
pub samples: Vec<Sample>,
pub av1_codec_config: Option<Vec<u8>>,
pub colr: Option<crate::meta::Colr>,
pub handler: Option<BoxType>,
pub sample_description_types: Vec<BoxType>,
pub edit_list: Vec<EditListEntry>,
pub sample_to_groups: Vec<crate::sample_group::SampleToGroup>,
pub sample_group_descriptions: Vec<crate::sample_group::SampleGroupDescription>,
pub producer_reference_times: Vec<ProducerReferenceTime>,
pub subsegment_indexes: Vec<SubsegmentIndex>,
}
impl AvisMeta {
#[allow(clippy::too_many_arguments)]
pub fn new(
timescale: u32,
media_timescale: Option<u32>,
display_dims: Option<(u32, u32)>,
samples: Vec<Sample>,
av1_codec_config: Option<Vec<u8>>,
colr: Option<crate::meta::Colr>,
handler: Option<BoxType>,
sample_description_types: Vec<BoxType>,
edit_list: Vec<EditListEntry>,
sample_to_groups: Vec<crate::sample_group::SampleToGroup>,
sample_group_descriptions: Vec<crate::sample_group::SampleGroupDescription>,
producer_reference_times: Vec<ProducerReferenceTime>,
subsegment_indexes: Vec<SubsegmentIndex>,
) -> Self {
Self {
timescale,
media_timescale,
display_dims,
samples,
av1_codec_config,
colr,
handler,
sample_description_types,
edit_list,
sample_to_groups,
sample_group_descriptions,
producer_reference_times,
subsegment_indexes,
}
}
}
pub fn parse_avis(file: &[u8]) -> Result<AvisMeta> {
if find_box(file, &MOOV)?.is_none() {
return Err(Error::InvalidData("avis: missing moov".to_string()));
}
let heif = HeifFile::parse_borrowed(file)?;
let movie = sequence::parse_movie(&heif)?
.ok_or_else(|| Error::InvalidData("avis: missing moov".to_string()))?;
let track = movie
.tracks
.first()
.ok_or_else(|| Error::InvalidData("avis: missing trak/mdia/minf/stbl".to_string()))?;
let samples = track
.samples
.iter()
.map(|s| Sample {
offset: s.offset,
size: s.size,
duration: s.duration,
is_sync: s.is_sync,
})
.collect();
let av1_codec_config = track
.sample_entries
.iter()
.find(|e| e.entry_type == AV01)
.and_then(|e| e.av1c.as_ref().map(|c| c.raw.clone()));
let colr = track
.sample_entries
.iter()
.find(|e| e.entry_type == AV01)
.and_then(|e| {
e.colr
.iter()
.find(|c| matches!(c, hprops::Colr::Nclx { .. }))
.or_else(|| e.colr.first())
.cloned()
})
.map(crate::meta::Colr::from);
let sample_description_types = track.sample_entries.iter().map(|e| e.entry_type).collect();
let edit_list = track
.edits
.iter()
.map(|e| 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,
})
.collect();
let (sample_to_groups, sample_group_descriptions) = match first_track_stbl(&heif, file) {
Some(stbl) => (
crate::sample_group::parse_sample_to_groups(stbl),
crate::sample_group::parse_sample_group_descriptions(stbl),
),
None => (Vec::new(), Vec::new()),
};
let producer_reference_times = parse_producer_reference_times(file);
let subsegment_indexes = parse_subsegment_indexes(file);
Ok(AvisMeta {
timescale: movie.timescale,
media_timescale: Some(track.timescale),
display_dims: Some((track.width, track.height)),
samples,
av1_codec_config,
colr,
handler: Some(track.handler),
sample_description_types,
edit_list,
sample_to_groups,
sample_group_descriptions,
producer_reference_times,
subsegment_indexes,
})
}
fn first_track_stbl<'a>(heif: &HeifFileRef<'_>, file: &'a [u8]) -> Option<&'a [u8]> {
let walk = heif.box_walk().ok()?;
let mut in_first_trak = false;
for entry in &walk {
let t = &entry.header.box_type;
if entry.depth == 1 && t == &TRAK {
if in_first_trak {
break;
}
in_first_trak = true;
continue;
}
if in_first_trak && t == &STBL {
let h = &entry.header;
return file.get(h.payload_start..h.end());
}
}
None
}
pub fn sample_duration_seconds(duration: u32, timescale: u32) -> (u32, u32) {
if timescale == 0 {
(duration, 1)
} else {
(duration, timescale)
}
}
pub fn sample_bytes<'a>(file: &'a [u8], sample: &Sample) -> Result<&'a [u8]> {
let start = sample.offset as usize;
let end = sample
.offset
.checked_add(sample.size as u64)
.ok_or_else(|| Error::InvalidData("avis: sample range overflow".to_string()))?
as usize;
if end > file.len() {
return Err(Error::InvalidData(format!(
"avis: sample {start}..{end} exceeds file length {}",
file.len()
)));
}
Ok(&file[start..end])
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct AvisSequenceCompliance {
pub handler_is_pict: bool,
pub single_sample_description: bool,
pub sample_description_is_av01: bool,
pub sequence_headers_identical: bool,
pub observed_handler: Option<BoxType>,
pub sample_description_count: u32,
pub sequence_header_obu_count: u32,
pub samples_out_of_range: u32,
}
impl AvisSequenceCompliance {
#[allow(clippy::too_many_arguments)]
pub fn new(
handler_is_pict: bool,
single_sample_description: bool,
sample_description_is_av01: bool,
sequence_headers_identical: bool,
observed_handler: Option<BoxType>,
sample_description_count: u32,
sequence_header_obu_count: u32,
samples_out_of_range: u32,
) -> Self {
Self {
handler_is_pict,
single_sample_description,
sample_description_is_av01,
sequence_headers_identical,
observed_handler,
sample_description_count,
sequence_header_obu_count,
samples_out_of_range,
}
}
}
impl AvisSequenceCompliance {
pub fn is_compliant(&self) -> bool {
self.handler_is_pict
&& self.single_sample_description
&& self.sample_description_is_av01
&& self.sequence_headers_identical
}
pub fn missing(&self) -> Vec<&'static str> {
let mut out = Vec::new();
if !self.handler_is_pict {
out.push("avis-handler-not-pict");
}
if !self.single_sample_description {
out.push("avis-sample-description-not-single");
}
if !self.sample_description_is_av01 {
out.push("avis-sample-description-not-av01");
}
if !self.sequence_headers_identical {
out.push("avis-sequence-header-obus-differ");
}
out
}
}
pub fn audit_avis_sequence(meta: &AvisMeta, file: &[u8]) -> AvisSequenceCompliance {
let handler_is_pict = meta.handler == Some(HANDLER_PICT);
let single_sample_description = meta.sample_description_types.len() == 1;
let sample_description_is_av01 = meta
.sample_description_types
.first()
.map(|t| t == &AV01)
.unwrap_or(false);
let mut first_sh: Option<Vec<u8>> = None;
let mut sh_total: u32 = 0;
let mut sh_identical = true;
let mut samples_out_of_range: u32 = 0;
for s in &meta.samples {
let payload = match sample_bytes(file, s) {
Ok(p) => p,
Err(_) => {
samples_out_of_range = samples_out_of_range.saturating_add(1);
continue;
}
};
for sh in walk_sequence_header_obus(payload) {
sh_total = sh_total.saturating_add(1);
match &first_sh {
None => first_sh = Some(sh),
Some(canonical) => {
if canonical != &sh {
sh_identical = false;
}
}
}
}
}
AvisSequenceCompliance {
handler_is_pict,
single_sample_description,
sample_description_is_av01,
sequence_headers_identical: sh_identical,
observed_handler: meta.handler,
sample_description_count: meta.sample_description_types.len() as u32,
sequence_header_obu_count: sh_total,
samples_out_of_range,
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AvisProfileCompliance {
pub profile: crate::derived::AvifProfile,
pub seq_profile: Option<u8>,
pub seq_level_idx_0: Option<u8>,
pub missing_av1c: bool,
}
impl AvisProfileCompliance {
#[allow(clippy::too_many_arguments)]
pub fn new(
profile: crate::derived::AvifProfile,
seq_profile: Option<u8>,
seq_level_idx_0: Option<u8>,
missing_av1c: bool,
) -> Self {
Self {
profile,
seq_profile,
seq_level_idx_0,
missing_av1c,
}
}
}
impl AvisProfileCompliance {
pub fn is_compliant(&self) -> bool {
match (self.seq_profile, self.seq_level_idx_0) {
(Some(p), Some(l)) => match self.profile {
crate::derived::AvifProfile::Baseline => p == 0 && l <= 13,
crate::derived::AvifProfile::Advanced => p <= 1 && l <= 16,
},
_ => false,
}
}
pub fn missing(&self) -> Vec<&'static str> {
let mut out = Vec::new();
if self.missing_av1c {
out.push("avis-track-missing-av1C");
return out;
}
if self.seq_profile.is_none() || self.seq_level_idx_0.is_none() {
out.push("avis-track-av1C-truncated");
return out;
}
let p = self.seq_profile.unwrap();
let l = self.seq_level_idx_0.unwrap();
let (max_p, max_l) = match self.profile {
crate::derived::AvifProfile::Baseline => (0u8, 13u8),
crate::derived::AvifProfile::Advanced => (1u8, 16u8),
};
if p > max_p {
out.push("avis-seq-profile-out-of-range");
}
if l > max_l {
out.push("avis-seq-level-idx-out-of-range");
}
out
}
}
pub fn audit_avis_profile_compliance(
meta: &AvisMeta,
brands: &crate::parser::BrandClass,
) -> Vec<AvisProfileCompliance> {
let mut out = Vec::new();
if !brands.is_baseline_profile && !brands.is_advanced_profile {
return out;
}
let (seq_profile, seq_level_idx_0, missing_av1c) = match meta.av1_codec_config.as_deref() {
Some(bytes) => (
crate::derived::decode_av1c_seq_profile(bytes),
crate::derived::decode_av1c_seq_level_idx_0(bytes),
false,
),
None => (None, None, true),
};
if brands.is_baseline_profile {
out.push(AvisProfileCompliance {
profile: crate::derived::AvifProfile::Baseline,
seq_profile,
seq_level_idx_0,
missing_av1c,
});
}
if brands.is_advanced_profile {
out.push(AvisProfileCompliance {
profile: crate::derived::AvifProfile::Advanced,
seq_profile,
seq_level_idx_0,
missing_av1c,
});
}
out
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct EditListCompliance {
pub media_rate_in_range: bool,
pub last_entry_not_empty: bool,
pub entry_count: u32,
pub empty_edit_count: u32,
pub dwell_entry_count: u32,
pub out_of_range_rate_count: u32,
}
impl EditListCompliance {
#[allow(clippy::too_many_arguments)]
pub fn new(
media_rate_in_range: bool,
last_entry_not_empty: bool,
entry_count: u32,
empty_edit_count: u32,
dwell_entry_count: u32,
out_of_range_rate_count: u32,
) -> Self {
Self {
media_rate_in_range,
last_entry_not_empty,
entry_count,
empty_edit_count,
dwell_entry_count,
out_of_range_rate_count,
}
}
}
impl EditListCompliance {
pub fn is_compliant(&self) -> bool {
self.media_rate_in_range && self.last_entry_not_empty
}
pub fn missing(&self) -> Vec<&'static str> {
let mut out = Vec::new();
if !self.media_rate_in_range {
out.push("avis-edit-list-media-rate-out-of-range");
}
if !self.last_entry_not_empty {
out.push("avis-edit-list-last-entry-empty");
}
out
}
}
pub fn audit_edit_list(meta: &AvisMeta) -> EditListCompliance {
let entry_count = meta.edit_list.len() as u32;
let empty_edit_count = meta.edit_list.iter().filter(|e| e.is_empty_edit()).count() as u32;
let dwell_entry_count = meta.edit_list.iter().filter(|e| e.is_dwell()).count() as u32;
let out_of_range_rate_count = meta
.edit_list
.iter()
.filter(|e| e.media_rate_integer != 0 && e.media_rate_integer != 1)
.count() as u32;
let media_rate_in_range = out_of_range_rate_count == 0;
let last_entry_not_empty = meta
.edit_list
.last()
.map(|e| !e.is_empty_edit())
.unwrap_or(true);
EditListCompliance {
media_rate_in_range,
last_entry_not_empty,
entry_count,
empty_edit_count,
dwell_entry_count,
out_of_range_rate_count,
}
}
fn walk_sequence_header_obus(payload: &[u8]) -> Vec<Vec<u8>> {
const AV1_OBU_TYPE_SEQUENCE_HEADER: u8 = 1;
let mut out = Vec::new();
let mut cursor = 0usize;
while cursor < payload.len() {
let obu_start = cursor;
let header = payload[cursor];
cursor += 1;
let obu_type = (header >> 3) & 0x0f;
let obu_extension_flag = (header >> 2) & 0x01;
let obu_has_size_field = (header >> 1) & 0x01;
if obu_extension_flag == 1 {
if cursor >= payload.len() {
return out;
}
cursor += 1;
}
if obu_has_size_field == 0 {
if obu_type == AV1_OBU_TYPE_SEQUENCE_HEADER {
out.push(payload[obu_start..].to_vec());
}
return out;
}
let mut size: u64 = 0;
let mut leb_len = 0usize;
let mut bad = false;
for i in 0..8 {
if cursor + i >= payload.len() {
bad = true;
break;
}
let b = payload[cursor + i];
size |= u64::from(b & 0x7f) << (i * 7);
if b & 0x80 == 0 {
leb_len = i + 1;
break;
}
if i == 7 {
bad = true;
break;
}
}
if bad || size > u64::from(u32::MAX) {
return out;
}
cursor += leb_len;
let payload_end = match cursor.checked_add(size as usize) {
Some(e) if e <= payload.len() => e,
_ => {
if obu_type == AV1_OBU_TYPE_SEQUENCE_HEADER {
out.push(payload[obu_start..].to_vec());
}
return out;
}
};
if obu_type == AV1_OBU_TYPE_SEQUENCE_HEADER {
out.push(payload[obu_start..payload_end].to_vec());
}
cursor = payload_end;
}
out
}
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct AvisInfo {
pub timescale: u32,
pub media_timescale: Option<u32>,
pub display_dims: Option<(u32, u32)>,
pub sample_count: u32,
pub total_sample_duration: u64,
pub has_av1_codec_config: bool,
pub handler: Option<BoxType>,
pub sample_description_types: Vec<BoxType>,
pub brands: crate::parser::BrandClass,
pub has_edit_list: bool,
pub sample_to_group_count: u32,
pub sequence_compliance: AvisSequenceCompliance,
pub profile_compliance: Vec<AvisProfileCompliance>,
pub edit_list_compliance: EditListCompliance,
}
impl AvisInfo {
#[allow(clippy::too_many_arguments)]
pub fn new(
timescale: u32,
media_timescale: Option<u32>,
display_dims: Option<(u32, u32)>,
sample_count: u32,
total_sample_duration: u64,
has_av1_codec_config: bool,
handler: Option<BoxType>,
sample_description_types: Vec<BoxType>,
brands: crate::parser::BrandClass,
has_edit_list: bool,
sample_to_group_count: u32,
sequence_compliance: AvisSequenceCompliance,
profile_compliance: Vec<AvisProfileCompliance>,
edit_list_compliance: EditListCompliance,
) -> Self {
Self {
timescale,
media_timescale,
display_dims,
sample_count,
total_sample_duration,
has_av1_codec_config,
handler,
sample_description_types,
brands,
has_edit_list,
sample_to_group_count,
sequence_compliance,
profile_compliance,
edit_list_compliance,
}
}
}
impl AvisInfo {
pub fn frame_count(&self) -> u32 {
self.sample_count
}
pub fn duration_seconds(&self) -> Option<f64> {
if self.timescale == 0 {
None
} else {
Some(self.total_sample_duration as f64 / self.timescale as f64)
}
}
pub fn media_duration_seconds(&self) -> Option<f64> {
match self.media_timescale {
Some(ts) if ts != 0 => Some(self.total_sample_duration as f64 / ts as f64),
_ => None,
}
}
pub fn is_compliant_all(&self) -> bool {
self.sequence_compliance.is_compliant()
&& self.profile_compliance.iter().all(|c| c.is_compliant())
&& self.edit_list_compliance.is_compliant()
}
pub fn missing_all(&self) -> Vec<&'static str> {
let mut out = self.sequence_compliance.missing();
for rec in &self.profile_compliance {
out.extend(rec.missing());
}
out.extend(self.edit_list_compliance.missing());
out
}
pub fn is_avis_brand(&self) -> bool {
self.brands.is_sequence
}
}
pub fn inspect_avis(file: &[u8]) -> Result<AvisInfo> {
let hdr = crate::parser::parse_header(file)?;
let brands = crate::parser::classify_brands(&hdr.major_brand, &hdr.compatible_brands)?;
let meta = parse_avis(file)?;
Ok(build_avis_info(meta, brands, file))
}
fn build_avis_info(meta: AvisMeta, brands: crate::parser::BrandClass, file: &[u8]) -> AvisInfo {
let total_sample_duration: u64 = meta
.samples
.iter()
.map(|s| u64::from(s.duration))
.sum::<u64>();
let sample_count = meta.samples.len() as u32;
let sequence_compliance = audit_avis_sequence(&meta, file);
let profile_compliance = audit_avis_profile_compliance(&meta, &brands);
let edit_list_compliance = audit_edit_list(&meta);
AvisInfo {
timescale: meta.timescale,
media_timescale: meta.media_timescale,
display_dims: meta.display_dims,
sample_count,
total_sample_duration,
has_av1_codec_config: meta.av1_codec_config.is_some(),
handler: meta.handler,
sample_description_types: meta.sample_description_types,
brands,
has_edit_list: !meta.edit_list.is_empty(),
sample_to_group_count: meta.sample_to_groups.len() as u32,
sequence_compliance,
profile_compliance,
edit_list_compliance,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn minimal_stbl() -> Vec<u8> {
fn full_box(v: u8, flags: u32, body: &[u8]) -> Vec<u8> {
let mut out = vec![v, (flags >> 16) as u8, (flags >> 8) as u8, flags as u8];
out.extend_from_slice(body);
out
}
fn wrap(btype: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let size = (8 + payload.len()) as u32;
let mut out = size.to_be_bytes().to_vec();
out.extend_from_slice(btype);
out.extend_from_slice(payload);
out
}
let stts_body = {
let mut b = 1u32.to_be_bytes().to_vec();
b.extend_from_slice(&3u32.to_be_bytes()); b.extend_from_slice(&100u32.to_be_bytes()); b
};
let stsc_body = {
let mut b = 1u32.to_be_bytes().to_vec();
b.extend_from_slice(&1u32.to_be_bytes()); b.extend_from_slice(&3u32.to_be_bytes()); b.extend_from_slice(&1u32.to_be_bytes()); b
};
let stsz_body = {
let mut b = 0u32.to_be_bytes().to_vec(); b.extend_from_slice(&3u32.to_be_bytes()); b.extend_from_slice(&10u32.to_be_bytes());
b.extend_from_slice(&20u32.to_be_bytes());
b.extend_from_slice(&30u32.to_be_bytes());
b
};
let stco_body = {
let mut b = 1u32.to_be_bytes().to_vec(); b.extend_from_slice(&100u32.to_be_bytes());
b
};
let stss_body = {
let mut b = 1u32.to_be_bytes().to_vec(); b.extend_from_slice(&1u32.to_be_bytes());
b
};
let mut out = Vec::new();
out.extend_from_slice(&wrap(b"stts", &full_box(0, 0, &stts_body)));
out.extend_from_slice(&wrap(b"stsc", &full_box(0, 0, &stsc_body)));
out.extend_from_slice(&wrap(b"stsz", &full_box(0, 0, &stsz_body)));
out.extend_from_slice(&wrap(b"stco", &full_box(0, 0, &stco_body)));
out.extend_from_slice(&wrap(b"stss", &full_box(0, 0, &stss_body)));
out
}
fn av01_entry_box(av1c_body: &[u8]) -> Vec<u8> {
let mut visual_header = vec![0u8; 78];
visual_header[7] = 1; let mut payload = visual_header;
payload.extend_from_slice(&wrap_box(b"av1C", av1c_body));
wrap_box(b"av01", &payload)
}
fn stsd_box(entries: &[Vec<u8>]) -> Vec<u8> {
let mut body = (entries.len() as u32).to_be_bytes().to_vec();
for e in entries {
body.extend_from_slice(e);
}
wrap_box(b"stsd", &full_box_bytes(0, 0, &body))
}
struct SyntheticMovie {
stbl: Vec<u8>,
elst_box: Option<Vec<u8>>,
mdhd_box: Option<Vec<u8>>,
pad: usize,
}
impl SyntheticMovie {
fn new(stbl: Vec<u8>) -> Self {
Self {
stbl,
elst_box: None,
mdhd_box: Some(build_mdhd_v0(600)),
pad: 512,
}
}
fn file(&self) -> Vec<u8> {
let mut ftyp = b"avis".to_vec();
ftyp.extend_from_slice(&0u32.to_be_bytes());
ftyp.extend_from_slice(b"avismsf1miaf");
let ftyp = wrap_box(b"ftyp", &ftyp);
let has_stsd = self.stbl.windows(4).any(|w| w == b"stsd");
let mut stbl = Vec::new();
if !has_stsd {
stbl.extend_from_slice(&stsd_box(&[av01_entry_box(&[0x81, 0x00, 0x0c, 0x00])]));
}
stbl.extend_from_slice(&self.stbl);
let mut mdia = Vec::new();
if let Some(m) = &self.mdhd_box {
mdia.extend_from_slice(m);
}
let mut hdlr = 0u32.to_be_bytes().to_vec();
hdlr.extend_from_slice(b"pict");
hdlr.extend_from_slice(&[0u8; 12]);
hdlr.push(0);
mdia.extend_from_slice(&wrap_box(b"hdlr", &full_box_bytes(0, 0, &hdlr)));
mdia.extend_from_slice(&wrap_box(b"minf", &wrap_box(b"stbl", &stbl)));
let mut tkhd = vec![0u8; 8]; tkhd.extend_from_slice(&1u32.to_be_bytes()); tkhd.extend_from_slice(&[0u8; 4]); tkhd.extend_from_slice(&6u32.to_be_bytes()); tkhd.extend_from_slice(&[0u8; 16]);
for m in [0x0001_0000u32, 0, 0, 0, 0x0001_0000, 0, 0, 0, 0x4000_0000] {
tkhd.extend_from_slice(&m.to_be_bytes());
}
tkhd.extend_from_slice(&(4u32 << 16).to_be_bytes());
tkhd.extend_from_slice(&(4u32 << 16).to_be_bytes());
let mut trak = wrap_box(b"tkhd", &full_box_bytes(0, 3, &tkhd));
if let Some(e) = &self.elst_box {
trak.extend_from_slice(&wrap_box(b"edts", e));
}
trak.extend_from_slice(&wrap_box(b"mdia", &mdia));
let mut mvhd = vec![0u8; 8];
mvhd.extend_from_slice(&1000u32.to_be_bytes());
mvhd.extend_from_slice(&6u32.to_be_bytes());
mvhd.extend_from_slice(&[0u8; 80]);
let mut moov = wrap_box(b"mvhd", &full_box_bytes(0, 0, &mvhd));
moov.extend_from_slice(&wrap_box(b"trak", &trak));
let mut out = ftyp;
out.extend_from_slice(&wrap_box(b"moov", &moov));
if out.len() < self.pad {
let fill = self.pad - out.len();
out.extend_from_slice(&wrap_box(b"free", &vec![0u8; fill.saturating_sub(8)]));
}
out
}
}
fn build_mdhd_v0(timescale: u32) -> Vec<u8> {
let mut body = vec![0u8; 20];
body[8..12].copy_from_slice(×cale.to_be_bytes());
wrap_box(b"mdhd", &full_box_bytes(0, 0, &body))
}
#[test]
fn sample_table_rejects_oversized_stsc_expansion() {
fn wrap(t: &[u8; 4], p: &[u8]) -> Vec<u8> {
let size = (8 + p.len()) as u32;
let mut out = size.to_be_bytes().to_vec();
out.extend_from_slice(t);
out.extend_from_slice(p);
out
}
fn full_box(v: u8, flags: u32, body: &[u8]) -> Vec<u8> {
let mut out = vec![v, (flags >> 16) as u8, (flags >> 8) as u8, flags as u8];
out.extend_from_slice(body);
out
}
let stts_body = {
let mut b = 1u32.to_be_bytes().to_vec();
b.extend_from_slice(&1u32.to_be_bytes());
b.extend_from_slice(&1u32.to_be_bytes());
b
};
let stsc_body = {
let mut b = 1u32.to_be_bytes().to_vec();
b.extend_from_slice(&1u32.to_be_bytes());
b.extend_from_slice(&u32::MAX.to_be_bytes());
b.extend_from_slice(&1u32.to_be_bytes());
b
};
let stsz_body = {
let mut b = 1u32.to_be_bytes().to_vec();
b.extend_from_slice(&1u32.to_be_bytes());
b
};
let stco_body = {
let mut b = 1u32.to_be_bytes().to_vec();
b.extend_from_slice(&100u32.to_be_bytes());
b
};
let mut stbl = Vec::new();
stbl.extend_from_slice(&wrap(b"stts", &full_box(0, 0, &stts_body)));
stbl.extend_from_slice(&wrap(b"stsc", &full_box(0, 0, &stsc_body)));
stbl.extend_from_slice(&wrap(b"stsz", &full_box(0, 0, &stsz_body)));
stbl.extend_from_slice(&wrap(b"stco", &full_box(0, 0, &stco_body)));
match parse_avis(&SyntheticMovie::new(stbl).file()) {
Ok(meta) => assert_eq!(meta.samples.len(), 1, "expansion bounded by stsz"),
Err(Error::InvalidData(s)) => assert!(
s.contains("sample"),
"expected the sample-count bound message, got: {s}"
),
Err(other) => panic!("unexpected error kind: {other:?}"),
}
}
#[test]
fn sample_table_three_samples() {
let file = SyntheticMovie::new(minimal_stbl()).file();
let samples = parse_avis(&file).unwrap().samples;
assert_eq!(samples.len(), 3);
assert_eq!(
samples[0],
Sample {
offset: 100,
size: 10,
duration: 100,
is_sync: true,
}
);
assert_eq!(
samples[1],
Sample {
offset: 110,
size: 20,
duration: 100,
is_sync: false,
}
);
assert_eq!(
samples[2],
Sample {
offset: 130,
size: 30,
duration: 100,
is_sync: false,
}
);
}
#[test]
fn sample_table_missing_stts_errors() {
let mut stbl = Vec::new();
let wrap = |t: &[u8; 4], p: &[u8]| {
let size = (8 + p.len()) as u32;
let mut out = size.to_be_bytes().to_vec();
out.extend_from_slice(t);
out.extend_from_slice(p);
out
};
stbl.extend_from_slice(&wrap(b"stsc", &[0u8; 8]));
stbl.extend_from_slice(&wrap(b"stsz", &[0u8; 12]));
stbl.extend_from_slice(&wrap(b"stco", &[0u8; 8]));
let err = parse_avis(&SyntheticMovie::new(stbl).file()).unwrap_err();
match err {
Error::InvalidData(_) => {}
_ => panic!("expected InvalidData"),
}
}
#[test]
fn sample_table_absent_stss_marks_all_sync() {
let full = minimal_stbl();
let idx = full
.windows(4)
.position(|w| w == b"stss")
.expect("stss present");
let stss_size_start = idx - 4;
let stss_size = u32::from_be_bytes([
full[stss_size_start],
full[stss_size_start + 1],
full[stss_size_start + 2],
full[stss_size_start + 3],
]) as usize;
let stss_end = stss_size_start + stss_size;
let stbl_no_stss: Vec<u8> = full
.iter()
.take(stss_size_start)
.chain(full.iter().skip(stss_end))
.copied()
.collect();
let samples = parse_avis(&SyntheticMovie::new(stbl_no_stss).file())
.unwrap()
.samples;
assert_eq!(samples.len(), 3);
assert!(samples.iter().all(|s| s.is_sync));
}
#[test]
fn stsd_av01_av1c_extraction_round_trip() {
let av1c_body: &[u8] = &[0x81, 0x04, 0x0c, 0x00];
let mut stbl = stsd_box(&[av01_entry_box(av1c_body)]);
stbl.extend_from_slice(&minimal_stbl());
let meta = parse_avis(&SyntheticMovie::new(stbl).file()).expect("parse");
assert_eq!(
meta.av1_codec_config.as_deref(),
Some(av1c_body),
"extracted av1C body must match the synthesized payload byte-for-byte"
);
}
#[test]
fn stsd_missing_av01_returns_none() {
let mut stbl = stsd_box(&[]);
stbl.extend_from_slice(&minimal_stbl());
match parse_avis(&SyntheticMovie::new(stbl).file()) {
Ok(meta) => {
assert!(
meta.av1_codec_config.is_none(),
"empty entry_count must produce no av1C"
);
assert!(meta.sample_description_types.is_empty());
}
Err(Error::InvalidData(_)) => {}
Err(other) => panic!("unexpected error kind: {other:?}"),
}
}
#[test]
fn stsd_truncated_av01_payload_returns_none() {
let mut stbl = stsd_box(&[wrap_box(b"av01", &[0u8; 32])]);
stbl.extend_from_slice(&minimal_stbl());
match parse_avis(&SyntheticMovie::new(stbl).file()) {
Ok(meta) => assert!(
meta.av1_codec_config.is_none(),
"truncated av01 payload must not yield an av1C"
),
Err(Error::InvalidData(_)) => {}
Err(other) => panic!("unexpected error kind: {other:?}"),
}
}
#[test]
fn alpha_video_avis_exposes_av1c() {
let bytes = include_bytes!("../tests/fixtures/alpha_video.avif");
let meta = parse_avis(bytes).expect("parse_avis alpha_video");
let av1c = meta
.av1_codec_config
.expect("alpha_video.avif must surface an av1C from stsd");
assert!(
av1c.len() >= 4,
"av1C must carry at least the 4-byte AV1CodecConfigurationRecord prefix, got {} bytes",
av1c.len()
);
assert_eq!(
av1c[0] & 0x80,
0x80,
"av1C[0] marker bit must be set, got {:#04x}",
av1c[0]
);
}
fn obu_with_size(obu_type: u8, payload_byte: u8) -> Vec<u8> {
let header = (obu_type & 0x0f) << 3 | 0b0000_0010;
vec![header, 0x01, payload_byte]
}
#[test]
fn walk_sequence_header_obus_pulls_out_sh_obus_only() {
let mut buf = Vec::new();
buf.extend_from_slice(&obu_with_size(6, 0xaa));
buf.extend_from_slice(&obu_with_size(1, 0xbb));
buf.extend_from_slice(&obu_with_size(2, 0xcc));
let shs = walk_sequence_header_obus(&buf);
assert_eq!(shs.len(), 1, "exactly one SH OBU expected");
assert_eq!(shs[0][0], 0x0a);
assert_eq!(shs[0][2], 0xbb);
}
#[test]
fn walk_sequence_header_obus_empty_input_returns_empty_vec() {
assert!(walk_sequence_header_obus(&[]).is_empty());
}
#[test]
fn walk_sequence_header_obus_truncated_size_stops_walk() {
let buf = vec![0x0a];
let shs = walk_sequence_header_obus(&buf);
assert!(shs.is_empty(), "truncated leb must skip the SH OBU");
}
#[test]
fn walk_sequence_header_obus_truncated_body_still_surfaces_sh_header() {
let buf = vec![0x0a, 0x03, 0xff];
let shs = walk_sequence_header_obus(&buf);
assert_eq!(shs.len(), 1);
assert_eq!(shs[0][0], 0x0a);
}
#[test]
fn walk_sequence_header_obus_has_size_zero_stops_walk_after_sh() {
let buf = vec![0x08, 0xff, 0xee]; let shs = walk_sequence_header_obus(&buf);
assert_eq!(shs.len(), 1);
assert_eq!(shs[0], vec![0x08, 0xff, 0xee]);
}
#[test]
fn audit_avis_sequence_all_shalls_satisfied() {
let sh_obu = obu_with_size(1, 0xab);
let mut file = vec![0u8; 100];
file.splice(50..50, sh_obu.iter().copied());
let _ = file;
let mut file = vec![0u8; 50];
file.extend_from_slice(&sh_obu);
file.extend_from_slice(&sh_obu); let sh_len = sh_obu.len();
let meta = AvisMeta {
timescale: 1000,
media_timescale: Some(1000),
display_dims: Some((16, 16)),
samples: vec![
Sample {
offset: 50,
size: sh_len as u32,
duration: 33,
is_sync: true,
},
Sample {
offset: (50 + sh_len) as u64,
size: sh_len as u32,
duration: 33,
is_sync: false,
},
],
av1_codec_config: Some(vec![0x81, 0x04, 0x0c, 0x00]),
colr: None,
handler: Some(HANDLER_PICT),
sample_description_types: vec![AV01],
edit_list: Vec::new(),
sample_to_groups: Vec::new(),
sample_group_descriptions: Vec::new(),
producer_reference_times: Vec::new(),
subsegment_indexes: Vec::new(),
};
let audit = audit_avis_sequence(&meta, &file);
assert!(
audit.is_compliant(),
"compliance audit must pass: {audit:?}"
);
assert!(audit.missing().is_empty());
assert_eq!(audit.sequence_header_obu_count, 2);
assert!(audit.sequence_headers_identical);
assert_eq!(audit.samples_out_of_range, 0);
}
#[test]
fn audit_avis_sequence_handler_not_pict_flagged() {
let meta = AvisMeta {
timescale: 1000,
media_timescale: Some(1000),
display_dims: None,
samples: Vec::new(),
av1_codec_config: None,
colr: None,
handler: Some(*b"vide"),
sample_description_types: vec![AV01],
edit_list: Vec::new(),
sample_to_groups: Vec::new(),
sample_group_descriptions: Vec::new(),
producer_reference_times: Vec::new(),
subsegment_indexes: Vec::new(),
};
let audit = audit_avis_sequence(&meta, &[]);
assert!(!audit.is_compliant());
assert_eq!(audit.observed_handler, Some(*b"vide"));
assert!(audit.missing().contains(&"avis-handler-not-pict"));
assert_eq!(audit.missing(), vec!["avis-handler-not-pict"]);
}
#[test]
fn audit_avis_sequence_handler_missing_flagged_as_not_pict() {
let meta = AvisMeta {
timescale: 1000,
media_timescale: Some(1000),
display_dims: None,
samples: Vec::new(),
av1_codec_config: None,
colr: None,
handler: None,
sample_description_types: vec![AV01],
edit_list: Vec::new(),
sample_to_groups: Vec::new(),
sample_group_descriptions: Vec::new(),
producer_reference_times: Vec::new(),
subsegment_indexes: Vec::new(),
};
let audit = audit_avis_sequence(&meta, &[]);
assert!(!audit.handler_is_pict);
assert_eq!(audit.observed_handler, None);
assert!(audit.missing().contains(&"avis-handler-not-pict"));
}
#[test]
fn audit_avis_sequence_multiple_sample_descriptions_flagged() {
let meta = AvisMeta {
timescale: 1000,
media_timescale: Some(1000),
display_dims: None,
samples: Vec::new(),
av1_codec_config: None,
colr: None,
handler: Some(HANDLER_PICT),
sample_description_types: vec![AV01, AV01],
edit_list: Vec::new(),
sample_to_groups: Vec::new(),
sample_group_descriptions: Vec::new(),
producer_reference_times: Vec::new(),
subsegment_indexes: Vec::new(),
};
let audit = audit_avis_sequence(&meta, &[]);
assert!(!audit.is_compliant());
assert_eq!(audit.sample_description_count, 2);
assert!(audit
.missing()
.contains(&"avis-sample-description-not-single"));
assert!(audit.sample_description_is_av01);
}
#[test]
fn audit_avis_sequence_zero_sample_descriptions_flagged() {
let meta = AvisMeta {
timescale: 1000,
media_timescale: Some(1000),
display_dims: None,
samples: Vec::new(),
av1_codec_config: None,
colr: None,
handler: Some(HANDLER_PICT),
sample_description_types: Vec::new(),
edit_list: Vec::new(),
sample_to_groups: Vec::new(),
sample_group_descriptions: Vec::new(),
producer_reference_times: Vec::new(),
subsegment_indexes: Vec::new(),
};
let audit = audit_avis_sequence(&meta, &[]);
assert!(!audit.single_sample_description);
assert!(!audit.sample_description_is_av01);
assert!(audit
.missing()
.contains(&"avis-sample-description-not-single"));
assert!(audit
.missing()
.contains(&"avis-sample-description-not-av01"));
}
#[test]
fn audit_avis_sequence_non_av01_sample_description_flagged() {
let meta = AvisMeta {
timescale: 1000,
media_timescale: Some(1000),
display_dims: None,
samples: Vec::new(),
av1_codec_config: None,
colr: None,
handler: Some(HANDLER_PICT),
sample_description_types: vec![*b"hvc1"],
edit_list: Vec::new(),
sample_to_groups: Vec::new(),
sample_group_descriptions: Vec::new(),
producer_reference_times: Vec::new(),
subsegment_indexes: Vec::new(),
};
let audit = audit_avis_sequence(&meta, &[]);
assert!(audit.single_sample_description);
assert!(!audit.sample_description_is_av01);
assert_eq!(
audit.missing(),
vec!["avis-sample-description-not-av01"],
"right count, wrong type must surface only the av01 token"
);
}
#[test]
fn audit_avis_sequence_diverging_sequence_headers_flagged() {
let sh_a = obu_with_size(1, 0xaa);
let sh_b = obu_with_size(1, 0xbb);
let mut file = Vec::new();
file.extend_from_slice(&sh_a);
file.extend_from_slice(&sh_b);
let meta = AvisMeta {
timescale: 1000,
media_timescale: Some(1000),
display_dims: None,
samples: vec![
Sample {
offset: 0,
size: sh_a.len() as u32,
duration: 1,
is_sync: true,
},
Sample {
offset: sh_a.len() as u64,
size: sh_b.len() as u32,
duration: 1,
is_sync: false,
},
],
av1_codec_config: None,
colr: None,
handler: Some(HANDLER_PICT),
sample_description_types: vec![AV01],
edit_list: Vec::new(),
sample_to_groups: Vec::new(),
sample_group_descriptions: Vec::new(),
producer_reference_times: Vec::new(),
subsegment_indexes: Vec::new(),
};
let audit = audit_avis_sequence(&meta, &file);
assert_eq!(audit.sequence_header_obu_count, 2);
assert!(!audit.sequence_headers_identical);
assert!(audit
.missing()
.contains(&"avis-sequence-header-obus-differ"));
}
#[test]
fn audit_avis_sequence_single_sequence_header_is_vacuously_identical() {
let sh = obu_with_size(1, 0x42);
let meta = AvisMeta {
timescale: 1000,
media_timescale: Some(1000),
display_dims: None,
samples: vec![Sample {
offset: 0,
size: sh.len() as u32,
duration: 1,
is_sync: true,
}],
av1_codec_config: None,
colr: None,
handler: Some(HANDLER_PICT),
sample_description_types: vec![AV01],
edit_list: Vec::new(),
sample_to_groups: Vec::new(),
sample_group_descriptions: Vec::new(),
producer_reference_times: Vec::new(),
subsegment_indexes: Vec::new(),
};
let audit = audit_avis_sequence(&meta, &sh);
assert!(audit.is_compliant());
assert_eq!(audit.sequence_header_obu_count, 1);
assert!(audit.sequence_headers_identical);
}
#[test]
fn audit_avis_sequence_zero_sequence_headers_is_vacuously_identical() {
let frame = obu_with_size(6, 0xff);
let meta = AvisMeta {
timescale: 1000,
media_timescale: Some(1000),
display_dims: None,
samples: vec![Sample {
offset: 0,
size: frame.len() as u32,
duration: 1,
is_sync: true,
}],
av1_codec_config: None,
colr: None,
handler: Some(HANDLER_PICT),
sample_description_types: vec![AV01],
edit_list: Vec::new(),
sample_to_groups: Vec::new(),
sample_group_descriptions: Vec::new(),
producer_reference_times: Vec::new(),
subsegment_indexes: Vec::new(),
};
let audit = audit_avis_sequence(&meta, &frame);
assert!(audit.is_compliant());
assert_eq!(audit.sequence_header_obu_count, 0);
assert!(audit.sequence_headers_identical);
}
#[test]
fn audit_avis_sequence_out_of_range_samples_counted_and_skipped() {
let sh = obu_with_size(1, 0x42);
let meta = AvisMeta {
timescale: 1000,
media_timescale: Some(1000),
display_dims: None,
samples: vec![
Sample {
offset: 0,
size: sh.len() as u32,
duration: 1,
is_sync: true,
},
Sample {
offset: 1_000_000,
size: 10,
duration: 1,
is_sync: false,
},
],
av1_codec_config: None,
colr: None,
handler: Some(HANDLER_PICT),
sample_description_types: vec![AV01],
edit_list: Vec::new(),
sample_to_groups: Vec::new(),
sample_group_descriptions: Vec::new(),
producer_reference_times: Vec::new(),
subsegment_indexes: Vec::new(),
};
let audit = audit_avis_sequence(&meta, &sh);
assert_eq!(audit.sequence_header_obu_count, 1);
assert!(audit.sequence_headers_identical);
assert_eq!(audit.samples_out_of_range, 1);
assert!(audit.is_compliant());
}
#[test]
fn alpha_video_avis_meets_section_3_compliance() {
let bytes = include_bytes!("../tests/fixtures/alpha_video.avif");
let meta = parse_avis(bytes).expect("parse_avis alpha_video");
assert_eq!(meta.handler, Some(HANDLER_PICT));
assert_eq!(meta.sample_description_types, vec![AV01]);
let audit = audit_avis_sequence(&meta, bytes);
assert!(
audit.is_compliant(),
"alpha_video.avif must satisfy av1-avif §3: {audit:?}"
);
assert_eq!(audit.observed_handler, Some(HANDLER_PICT));
assert_eq!(audit.sample_description_count, 1);
assert_eq!(audit.samples_out_of_range, 0);
assert!(audit.sequence_header_obu_count >= 1);
}
#[test]
fn sample_description_types_round_trip() {
let av1c_body: &[u8] = &[0x81, 0x04, 0x0c, 0x00];
let mut hvc1_header = vec![0u8; 78];
hvc1_header[7] = 1;
let mut stbl = stsd_box(&[av01_entry_box(av1c_body), wrap_box(b"hvc1", &hvc1_header)]);
stbl.extend_from_slice(&minimal_stbl());
let meta = parse_avis(&SyntheticMovie::new(stbl).file()).expect("parse");
assert_eq!(meta.sample_description_types, vec![*b"av01", *b"hvc1"]);
}
#[test]
fn sample_description_types_missing_stsd_returns_empty() {
let mut stbl = stsd_box(&[]);
stbl.extend_from_slice(&minimal_stbl());
let types = parse_avis(&SyntheticMovie::new(stbl).file())
.map(|m| m.sample_description_types)
.unwrap_or_default();
assert!(types.is_empty());
}
use crate::derived::AvifProfile;
fn brands_with(baseline: bool, advanced: bool) -> crate::parser::BrandClass {
crate::parser::BrandClass {
is_image: true,
is_miaf: true,
is_baseline_profile: baseline,
is_advanced_profile: advanced,
..crate::parser::BrandClass::default()
}
}
fn av1c_with(seq_profile: u8, seq_level_idx_0: u8) -> Vec<u8> {
let b1 = (seq_profile << 5) | (seq_level_idx_0 & 0x1F);
vec![0x81, b1, 0x00, 0x00]
}
fn avis_meta_with_av1c(av1c: Option<Vec<u8>>) -> AvisMeta {
AvisMeta {
timescale: 1000,
media_timescale: Some(1000),
display_dims: None,
samples: Vec::new(),
av1_codec_config: av1c,
colr: None,
handler: Some(HANDLER_PICT),
sample_description_types: vec![AV01],
edit_list: Vec::new(),
sample_to_groups: Vec::new(),
sample_group_descriptions: Vec::new(),
producer_reference_times: Vec::new(),
subsegment_indexes: Vec::new(),
}
}
#[test]
fn audit_avis_profile_no_brand_claim_short_circuits() {
let meta = avis_meta_with_av1c(Some(av1c_with(0, 13)));
let r = audit_avis_profile_compliance(&meta, &brands_with(false, false));
assert!(r.is_empty());
}
#[test]
fn audit_avis_profile_baseline_main_level_5_1_compliant() {
let meta = avis_meta_with_av1c(Some(av1c_with(0, 13)));
let r = audit_avis_profile_compliance(&meta, &brands_with(true, false));
assert_eq!(r.len(), 1);
assert_eq!(r[0].profile, AvifProfile::Baseline);
assert_eq!(r[0].seq_profile, Some(0));
assert_eq!(r[0].seq_level_idx_0, Some(13));
assert!(r[0].is_compliant());
assert!(r[0].missing().is_empty());
}
#[test]
fn audit_avis_profile_baseline_rejects_high_profile() {
let meta = avis_meta_with_av1c(Some(av1c_with(1, 8)));
let r = audit_avis_profile_compliance(&meta, &brands_with(true, false));
assert_eq!(r.len(), 1);
assert!(!r[0].is_compliant());
assert!(r[0].missing().contains(&"avis-seq-profile-out-of-range"));
}
#[test]
fn audit_avis_profile_baseline_rejects_level_above_5_1() {
let meta = avis_meta_with_av1c(Some(av1c_with(0, 16)));
let r = audit_avis_profile_compliance(&meta, &brands_with(true, false));
assert_eq!(r.len(), 1);
assert!(!r[0].is_compliant());
assert!(r[0].missing().contains(&"avis-seq-level-idx-out-of-range"));
}
#[test]
fn audit_avis_profile_advanced_high_level_6_0_compliant() {
let meta = avis_meta_with_av1c(Some(av1c_with(1, 16)));
let r = audit_avis_profile_compliance(&meta, &brands_with(false, true));
assert_eq!(r.len(), 1);
assert_eq!(r[0].profile, AvifProfile::Advanced);
assert!(r[0].is_compliant());
}
#[test]
fn audit_avis_profile_advanced_accepts_main_profile_track() {
let meta = avis_meta_with_av1c(Some(av1c_with(0, 8)));
let r = audit_avis_profile_compliance(&meta, &brands_with(false, true));
assert!(r[0].is_compliant());
}
#[test]
fn audit_avis_profile_advanced_rejects_professional_profile() {
let meta = avis_meta_with_av1c(Some(av1c_with(2, 8)));
let r = audit_avis_profile_compliance(&meta, &brands_with(false, true));
assert!(!r[0].is_compliant());
assert!(r[0].missing().contains(&"avis-seq-profile-out-of-range"));
}
#[test]
fn audit_avis_profile_level_31_rejected_for_both_profiles() {
let meta = avis_meta_with_av1c(Some(av1c_with(0, 31)));
let r = audit_avis_profile_compliance(&meta, &brands_with(true, true));
assert_eq!(r.len(), 2);
assert_eq!(r[0].profile, AvifProfile::Baseline);
assert_eq!(r[1].profile, AvifProfile::Advanced);
assert!(!r[0].is_compliant());
assert!(!r[1].is_compliant());
assert!(r[0].missing().contains(&"avis-seq-level-idx-out-of-range"));
assert!(r[1].missing().contains(&"avis-seq-level-idx-out-of-range"));
}
#[test]
fn audit_avis_profile_missing_av1c_flagged_distinctly() {
let meta = avis_meta_with_av1c(None);
let r = audit_avis_profile_compliance(&meta, &brands_with(true, false));
assert_eq!(r.len(), 1);
assert!(r[0].missing_av1c);
assert_eq!(r[0].seq_profile, None);
assert_eq!(r[0].seq_level_idx_0, None);
assert!(!r[0].is_compliant());
assert_eq!(r[0].missing(), vec!["avis-track-missing-av1C"]);
}
#[test]
fn audit_avis_profile_truncated_av1c_flagged_distinctly() {
let meta = avis_meta_with_av1c(Some(vec![0x81]));
let r = audit_avis_profile_compliance(&meta, &brands_with(true, false));
assert_eq!(r.len(), 1);
assert!(!r[0].missing_av1c);
assert_eq!(r[0].seq_profile, None);
assert_eq!(r[0].seq_level_idx_0, None);
assert!(!r[0].is_compliant());
assert_eq!(r[0].missing(), vec!["avis-track-av1C-truncated"]);
}
#[test]
fn audit_avis_profile_dual_brand_emits_two_records_in_order() {
let meta = avis_meta_with_av1c(Some(av1c_with(0, 8)));
let r = audit_avis_profile_compliance(&meta, &brands_with(true, true));
assert_eq!(r.len(), 2);
assert_eq!(r[0].profile, AvifProfile::Baseline);
assert_eq!(r[1].profile, AvifProfile::Advanced);
assert!(r[0].is_compliant());
assert!(r[1].is_compliant());
}
fn full_box_bytes(version: u8, flags: u32, body: &[u8]) -> Vec<u8> {
let mut out = vec![
version,
(flags >> 16) as u8,
(flags >> 8) as u8,
flags as u8,
];
out.extend_from_slice(body);
out
}
fn wrap_box(btype: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let size = (8 + payload.len()) as u32;
let mut out = size.to_be_bytes().to_vec();
out.extend_from_slice(btype);
out.extend_from_slice(payload);
out
}
fn build_elst_v0(entries: &[(u32, i32, i16, i16)]) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&(entries.len() as u32).to_be_bytes());
for &(seg, mt, ri, rf) in entries {
body.extend_from_slice(&seg.to_be_bytes());
body.extend_from_slice(&mt.to_be_bytes());
body.extend_from_slice(&ri.to_be_bytes());
body.extend_from_slice(&rf.to_be_bytes());
}
wrap_box(b"elst", &full_box_bytes(0, 0, &body))
}
fn build_elst_v1(entries: &[(u64, i64, i16, i16)]) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&(entries.len() as u32).to_be_bytes());
for &(seg, mt, ri, rf) in entries {
body.extend_from_slice(&seg.to_be_bytes());
body.extend_from_slice(&mt.to_be_bytes());
body.extend_from_slice(&ri.to_be_bytes());
body.extend_from_slice(&rf.to_be_bytes());
}
wrap_box(b"elst", &full_box_bytes(1, 0, &body))
}
fn edit_list_of(elst_box: Vec<u8>) -> Result<Vec<EditListEntry>> {
let mut m = SyntheticMovie::new(minimal_stbl());
m.elst_box = Some(elst_box);
parse_avis(&m.file()).map(|m| m.edit_list)
}
#[test]
fn parse_edit_list_v0_single_normal_entry_round_trips() {
let entries = edit_list_of(build_elst_v0(&[(1000, 200, 1, 0)])).expect("parse v0");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].segment_duration, 1000);
assert_eq!(entries[0].media_time, 200);
assert_eq!(entries[0].media_rate_integer, 1);
assert_eq!(entries[0].media_rate_fraction, 0);
assert!(!entries[0].is_empty_edit());
assert!(!entries[0].is_dwell());
}
#[test]
fn parse_edit_list_v0_empty_edit_sign_extends_to_minus_one() {
let entries = edit_list_of(build_elst_v0(&[(500, -1, 1, 0)])).expect("parse v0");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].media_time, -1);
assert!(entries[0].is_empty_edit());
}
#[test]
fn parse_edit_list_v1_large_media_time_round_trips() {
let big = 0x1_0000_0000i64; let entries =
edit_list_of(build_elst_v1(&[(0xFFFF_FFFFu64 + 1, big, 1, 0)])).expect("parse v1");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].segment_duration, 0xFFFF_FFFFu64 + 1);
assert_eq!(entries[0].media_time, big);
}
#[test]
fn parse_edit_list_truncated_entry_table_stops_walk() {
let full = build_elst_v0(&[(10, 0, 1, 0), (20, 5, 1, 0), (30, 9, 1, 0)]);
let mut truncated = full[..full.len() - 6].to_vec();
let size = truncated.len() as u32;
truncated[..4].copy_from_slice(&size.to_be_bytes());
match edit_list_of(truncated) {
Ok(entries) => {
assert!(entries.len() <= 2);
for (e, seg) in entries.iter().zip([10u64, 20]) {
assert_eq!(e.segment_duration, seg);
}
}
Err(Error::InvalidData(_)) => {}
Err(other) => panic!("unexpected error kind: {other:?}"),
}
}
#[test]
fn find_first_track_edit_list_reads_v0_elst() {
let entries = edit_list_of(build_elst_v0(&[(0, -1, 1, 0), (200, 0, 1, 0)])).expect("parse");
assert_eq!(entries.len(), 2);
assert!(entries[0].is_empty_edit());
assert_eq!(entries[1].segment_duration, 200);
}
#[test]
fn find_first_track_edit_list_absent_edts_returns_empty() {
let meta = parse_avis(&SyntheticMovie::new(minimal_stbl()).file()).expect("parse");
assert!(meta.edit_list.is_empty());
}
fn build_mdhd_v1(timescale: u32) -> Vec<u8> {
let mut body = vec![0u8; 36];
body[16..20].copy_from_slice(×cale.to_be_bytes());
wrap_box(b"mdhd", &full_box_bytes(1, 0, &body))
}
fn media_timescale_of(mdhd_box: Option<Vec<u8>>) -> Result<Option<u32>> {
let mut m = SyntheticMovie::new(minimal_stbl());
m.mdhd_box = mdhd_box;
parse_avis(&m.file()).map(|m| m.media_timescale)
}
#[test]
fn find_first_track_media_timescale_v0_reads_timescale() {
let ts = media_timescale_of(Some(build_mdhd_v0(24_000))).expect("parse");
assert_eq!(ts, Some(24_000));
}
#[test]
fn find_first_track_media_timescale_v1_reads_timescale() {
let ts = media_timescale_of(Some(build_mdhd_v1(90_000))).expect("parse");
assert_eq!(ts, Some(90_000));
}
#[test]
fn find_first_track_media_timescale_absent_mdhd_returns_none() {
assert!(matches!(
media_timescale_of(None),
Err(Error::InvalidData(_))
));
}
#[test]
fn find_first_track_media_timescale_unknown_version_returns_none() {
let mdhd = wrap_box(b"mdhd", &full_box_bytes(2, 0, &[0u8; 36]));
match media_timescale_of(Some(mdhd)) {
Ok(ts) => assert_eq!(ts.unwrap_or(0), 0),
Err(Error::InvalidData(_)) => {}
Err(other) => panic!("unexpected error kind: {other:?}"),
}
}
#[test]
fn find_first_track_media_timescale_truncated_v0_returns_none() {
let mdhd = wrap_box(b"mdhd", &full_box_bytes(0, 0, &[0u8; 8]));
match media_timescale_of(Some(mdhd)) {
Ok(ts) => assert_eq!(ts, None),
Err(Error::InvalidData(_)) => {}
Err(other) => panic!("unexpected error kind: {other:?}"),
}
}
#[test]
fn edit_list_entry_media_time_seconds_v0_normal_entry() {
let e = EditListEntry {
segment_duration: 1000,
media_time: 9000,
media_rate_integer: 1,
media_rate_fraction: 0,
};
let s = e.media_time_seconds(90_000).expect("seconds");
assert!((s - 0.1).abs() < 1e-9);
}
#[test]
fn edit_list_entry_media_time_seconds_empty_edit_returns_none() {
let e = EditListEntry {
segment_duration: 1000,
media_time: -1,
media_rate_integer: 1,
media_rate_fraction: 0,
};
assert!(e.media_time_seconds(90_000).is_none());
}
#[test]
fn edit_list_entry_media_time_seconds_zero_timescale_returns_none() {
let e = EditListEntry {
segment_duration: 1000,
media_time: 50,
media_rate_integer: 1,
media_rate_fraction: 0,
};
assert!(e.media_time_seconds(0).is_none());
}
#[test]
fn edit_list_entry_segment_duration_seconds_typical_case() {
let e = EditListEntry {
segment_duration: 12_000,
media_time: 0,
media_rate_integer: 1,
media_rate_fraction: 0,
};
let s = e.segment_duration_seconds(24_000).expect("seconds");
assert!((s - 0.5).abs() < 1e-9);
}
#[test]
fn edit_list_entry_segment_duration_seconds_zero_timescale_returns_none() {
let e = EditListEntry {
segment_duration: 12_000,
media_time: 0,
media_rate_integer: 1,
media_rate_fraction: 0,
};
assert!(e.segment_duration_seconds(0).is_none());
}
#[test]
fn parse_edit_list_unknown_version_returns_empty() {
let body = vec![0u8, 0, 0, 1, 0, 0, 0, 0]; let elst = wrap_box(b"elst", &full_box_bytes(2, 0, &body));
match edit_list_of(elst) {
Ok(entries) => assert!(entries.is_empty()),
Err(Error::InvalidData(_)) => {}
Err(other) => panic!("unexpected error kind: {other:?}"),
}
}
#[test]
fn audit_edit_list_empty_is_vacuously_compliant() {
let meta = avis_meta_with_av1c(None);
let r = audit_edit_list(&meta);
assert!(r.is_compliant());
assert!(r.missing().is_empty());
assert_eq!(r.entry_count, 0);
assert_eq!(r.empty_edit_count, 0);
assert_eq!(r.dwell_entry_count, 0);
}
#[test]
fn audit_edit_list_leading_empty_then_normal_is_compliant() {
let mut meta = avis_meta_with_av1c(None);
meta.edit_list = vec![
EditListEntry {
segment_duration: 1000,
media_time: -1,
media_rate_integer: 1,
media_rate_fraction: 0,
},
EditListEntry {
segment_duration: 5000,
media_time: 0,
media_rate_integer: 1,
media_rate_fraction: 0,
},
];
let r = audit_edit_list(&meta);
assert!(r.is_compliant());
assert_eq!(r.entry_count, 2);
assert_eq!(r.empty_edit_count, 1);
assert_eq!(r.dwell_entry_count, 0);
}
#[test]
fn audit_edit_list_trailing_empty_flagged() {
let mut meta = avis_meta_with_av1c(None);
meta.edit_list = vec![
EditListEntry {
segment_duration: 500,
media_time: 0,
media_rate_integer: 1,
media_rate_fraction: 0,
},
EditListEntry {
segment_duration: 100,
media_time: -1,
media_rate_integer: 1,
media_rate_fraction: 0,
},
];
let r = audit_edit_list(&meta);
assert!(!r.is_compliant());
assert_eq!(r.empty_edit_count, 1);
assert_eq!(r.missing(), vec!["avis-edit-list-last-entry-empty"]);
}
#[test]
fn audit_edit_list_dwell_entry_is_compliant_and_counted() {
let mut meta = avis_meta_with_av1c(None);
meta.edit_list = vec![EditListEntry {
segment_duration: 250,
media_time: 33,
media_rate_integer: 0,
media_rate_fraction: 0,
}];
let r = audit_edit_list(&meta);
assert!(r.is_compliant());
assert_eq!(r.dwell_entry_count, 1);
assert_eq!(r.out_of_range_rate_count, 0);
}
#[test]
fn audit_edit_list_out_of_range_media_rate_flagged() {
let mut meta = avis_meta_with_av1c(None);
meta.edit_list = vec![EditListEntry {
segment_duration: 100,
media_time: 0,
media_rate_integer: 2,
media_rate_fraction: 0,
}];
let r = audit_edit_list(&meta);
assert!(!r.is_compliant());
assert_eq!(r.out_of_range_rate_count, 1);
assert_eq!(r.missing(), vec!["avis-edit-list-media-rate-out-of-range"]);
}
#[test]
fn audit_edit_list_negative_media_rate_flagged() {
let mut meta = avis_meta_with_av1c(None);
meta.edit_list = vec![EditListEntry {
segment_duration: 100,
media_time: 0,
media_rate_integer: -1,
media_rate_fraction: 0,
}];
let r = audit_edit_list(&meta);
assert!(!r.is_compliant());
assert_eq!(r.out_of_range_rate_count, 1);
}
#[test]
fn audit_edit_list_both_shalls_fail_simultaneously() {
let mut meta = avis_meta_with_av1c(None);
meta.edit_list = vec![EditListEntry {
segment_duration: 100,
media_time: -1,
media_rate_integer: 7,
media_rate_fraction: 0,
}];
let r = audit_edit_list(&meta);
assert!(!r.is_compliant());
let m = r.missing();
assert!(m.contains(&"avis-edit-list-media-rate-out-of-range"));
assert!(m.contains(&"avis-edit-list-last-entry-empty"));
}
#[test]
fn build_avis_info_aggregates_a_clean_avis() {
let mut meta = avis_meta_with_av1c(Some(av1c_with(0, 8)));
meta.timescale = 24_000;
meta.display_dims = Some((640, 480));
meta.samples = vec![
Sample {
offset: 0,
size: 0,
duration: 1000,
is_sync: true,
},
Sample {
offset: 0,
size: 0,
duration: 1000,
is_sync: false,
},
];
let info = build_avis_info(meta, brands_with(true, false), &[]);
assert_eq!(info.timescale, 24_000);
assert_eq!(info.display_dims, Some((640, 480)));
assert_eq!(info.sample_count, 2);
assert_eq!(info.total_sample_duration, 2_000);
assert_eq!(info.frame_count(), 2);
assert!(info.has_av1_codec_config);
assert_eq!(info.handler, Some(HANDLER_PICT));
assert_eq!(info.sample_description_types, vec![AV01]);
assert!(info.brands.is_baseline_profile);
assert!(!info.has_edit_list);
assert!(info.sequence_compliance.is_compliant());
assert_eq!(info.profile_compliance.len(), 1);
assert_eq!(info.profile_compliance[0].profile, AvifProfile::Baseline);
assert!(info.profile_compliance[0].is_compliant());
assert!(info.edit_list_compliance.is_compliant());
assert!(info.is_compliant_all());
assert!(info.missing_all().is_empty());
let d = info.duration_seconds().unwrap();
assert!((d - (2000.0 / 24_000.0)).abs() < 1e-9);
}
#[test]
fn build_avis_info_duration_seconds_undefined_when_timescale_zero() {
let mut meta = avis_meta_with_av1c(Some(av1c_with(0, 8)));
meta.timescale = 0;
let info = build_avis_info(meta, brands_with(false, false), &[]);
assert!(info.duration_seconds().is_none());
}
#[test]
fn build_avis_info_no_brand_claim_empty_profile_records() {
let meta = avis_meta_with_av1c(Some(av1c_with(0, 8)));
let info = build_avis_info(meta, brands_with(false, false), &[]);
assert!(info.profile_compliance.is_empty());
assert!(info.is_compliant_all());
assert!(!info.is_avis_brand());
}
#[test]
fn build_avis_info_dual_brand_records_in_order() {
let meta = avis_meta_with_av1c(Some(av1c_with(0, 8)));
let info = build_avis_info(meta, brands_with(true, true), &[]);
assert_eq!(info.profile_compliance.len(), 2);
assert_eq!(info.profile_compliance[0].profile, AvifProfile::Baseline);
assert_eq!(info.profile_compliance[1].profile, AvifProfile::Advanced);
}
#[test]
fn build_avis_info_is_avis_brand_tracks_sequence_flag() {
let meta = avis_meta_with_av1c(Some(av1c_with(0, 8)));
let mut brands = brands_with(false, false);
brands.is_sequence = true;
let info = build_avis_info(meta, brands, &[]);
assert!(info.is_avis_brand());
}
#[test]
fn build_avis_info_missing_all_concatenates_in_audit_order() {
let mut meta = avis_meta_with_av1c(Some(av1c_with(0, 8)));
meta.handler = Some(*b"vide"); meta.edit_list = vec![EditListEntry {
segment_duration: 100,
media_time: -1, media_rate_integer: 1,
media_rate_fraction: 0,
}];
let info = build_avis_info(meta, brands_with(true, false), &[]);
assert!(!info.is_compliant_all());
let m = info.missing_all();
assert!(
m.contains(&"avis-handler-not-pict"),
"§3 token must be present: {m:?}"
);
assert!(
m.contains(&"avis-edit-list-last-entry-empty"),
"§8.6.6.3 token must be present: {m:?}"
);
let i_seq = m
.iter()
.position(|t| *t == "avis-handler-not-pict")
.unwrap();
let i_elst = m
.iter()
.position(|t| *t == "avis-edit-list-last-entry-empty")
.unwrap();
assert!(
i_seq < i_elst,
"§3 tokens must precede §8.6.6.3 tokens: {m:?}"
);
}
#[test]
fn build_avis_info_total_sample_duration_widens_to_u64() {
let mut meta = avis_meta_with_av1c(Some(av1c_with(0, 8)));
meta.samples = vec![
Sample {
offset: 0,
size: 0,
duration: u32::MAX,
is_sync: true,
},
Sample {
offset: 0,
size: 0,
duration: u32::MAX,
is_sync: false,
},
];
let info = build_avis_info(meta, brands_with(false, false), &[]);
assert_eq!(info.total_sample_duration, 2 * u64::from(u32::MAX));
}
#[test]
fn build_avis_info_has_edit_list_reflects_entry_presence() {
let empty = avis_meta_with_av1c(Some(av1c_with(0, 8)));
let info_empty = build_avis_info(empty, brands_with(false, false), &[]);
assert!(!info_empty.has_edit_list);
let mut with_elst = avis_meta_with_av1c(Some(av1c_with(0, 8)));
with_elst.edit_list = vec![EditListEntry {
segment_duration: 100,
media_time: 0,
media_rate_integer: 1,
media_rate_fraction: 0,
}];
let info_present = build_avis_info(with_elst, brands_with(false, false), &[]);
assert!(info_present.has_edit_list);
}
#[test]
fn build_avis_info_has_av1_codec_config_tracks_av1c_presence() {
let meta = avis_meta_with_av1c(None);
let info = build_avis_info(meta, brands_with(false, false), &[]);
assert!(!info.has_av1_codec_config);
}
#[test]
fn build_avis_info_media_timescale_carries_forward() {
let mut meta = avis_meta_with_av1c(Some(av1c_with(0, 8)));
meta.media_timescale = Some(90_000);
let info = build_avis_info(meta, brands_with(false, false), &[]);
assert_eq!(info.media_timescale, Some(90_000));
}
#[test]
fn build_avis_info_media_timescale_none_when_mdhd_absent() {
let mut meta = avis_meta_with_av1c(Some(av1c_with(0, 8)));
meta.media_timescale = None;
let info = build_avis_info(meta, brands_with(false, false), &[]);
assert_eq!(info.media_timescale, None);
assert!(info.media_duration_seconds().is_none());
}
#[test]
fn build_avis_info_media_duration_seconds_uses_media_timescale() {
let mut meta = avis_meta_with_av1c(Some(av1c_with(0, 8)));
meta.timescale = 1000;
meta.media_timescale = Some(90_000);
meta.samples = vec![
Sample {
offset: 0,
size: 0,
duration: 3000,
is_sync: true,
},
Sample {
offset: 0,
size: 0,
duration: 3000,
is_sync: false,
},
];
let info = build_avis_info(meta, brands_with(false, false), &[]);
let media_s = info.media_duration_seconds().expect("media seconds");
assert!((media_s - 6000.0 / 90_000.0).abs() < 1e-12);
let movie_s = info.duration_seconds().expect("movie seconds");
assert!((movie_s - 6.0).abs() < 1e-9);
assert!((media_s - movie_s).abs() > 1e-3);
}
#[test]
fn build_avis_info_media_duration_seconds_zero_media_timescale_none() {
let mut meta = avis_meta_with_av1c(Some(av1c_with(0, 8)));
meta.media_timescale = Some(0);
meta.samples = vec![Sample {
offset: 0,
size: 0,
duration: 50,
is_sync: true,
}];
let info = build_avis_info(meta, brands_with(false, false), &[]);
assert!(info.media_duration_seconds().is_none());
}
fn prft_body(version: u8, flags: u32, track_id: u32, ntp: u64, media_time: u64) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&track_id.to_be_bytes());
body.extend_from_slice(&ntp.to_be_bytes());
if version == 0 {
body.extend_from_slice(&(media_time as u32).to_be_bytes());
} else {
body.extend_from_slice(&media_time.to_be_bytes());
}
full_box_bytes(version, flags, &body)
}
#[test]
fn parse_prft_v0_reads_32bit_media_time() {
let ntp = (3_900_000_000u64 << 32) | 0x8000_0000;
let payload = prft_body(0, 0, 7, ntp, 123_456);
let p = parse_prft(&payload).unwrap();
assert_eq!(p.version, 0);
assert_eq!(p.flags, 0);
assert_eq!(p.reference_track_id, 7);
assert_eq!(p.ntp_timestamp, ntp);
assert_eq!(p.media_time, 123_456);
assert_eq!(p.ntp_seconds(), 3_900_000_000);
assert_eq!(p.ntp_fraction(), 0x8000_0000);
}
#[test]
fn parse_prft_v1_reads_64bit_media_time() {
let big = 0x1_0000_0001u64; let payload = prft_body(1, 0, 1, 0, big);
let p = parse_prft(&payload).unwrap();
assert_eq!(p.version, 1);
assert_eq!(p.media_time, big);
}
#[test]
fn parse_prft_rejects_unknown_version() {
let payload = prft_body(1, 0, 1, 0, 0);
let mut bad = payload.clone();
bad[0] = 2; assert!(parse_prft(&bad).is_err());
}
#[test]
fn parse_prft_rejects_truncated_v0_body() {
let payload = &prft_body(0, 0, 1, 0, 0)[..16];
assert!(parse_prft(payload).is_err());
}
#[test]
fn parse_prft_rejects_truncated_v1_body() {
let payload = &prft_body(1, 0, 1, 0, 0)[..16];
assert!(parse_prft(payload).is_err());
}
#[test]
fn prft_flag_bits_decode() {
let p = parse_prft(&prft_body(0, 0x000001, 1, 0, 0)).unwrap();
assert!(p.is_encoder_input_output());
assert!(!p.is_finalization_time());
let p = parse_prft(&prft_body(0, 0x000002, 1, 0, 0)).unwrap();
assert!(p.is_finalization_time());
let p = parse_prft(&prft_body(0, 0x000004, 1, 0, 0)).unwrap();
assert!(p.is_file_write_time());
}
#[test]
fn prft_unix_seconds_converts_ntp_epoch() {
let ntp = NTP_UNIX_EPOCH_OFFSET_SECONDS << 32;
let p = parse_prft(&prft_body(0, 0, 1, ntp, 0)).unwrap();
let unix = p.unix_seconds().unwrap();
assert!((unix - 0.0).abs() < 1e-6, "got {unix}");
let ntp = ((NTP_UNIX_EPOCH_OFFSET_SECONDS + 10) << 32) | 0x8000_0000;
let p = parse_prft(&prft_body(0, 0, 1, ntp, 0)).unwrap();
let unix = p.unix_seconds().unwrap();
assert!((unix - 10.5).abs() < 1e-6, "got {unix}");
}
#[test]
fn prft_unix_seconds_none_before_unix_epoch() {
let ntp = (1000u64) << 32;
let p = parse_prft(&prft_body(0, 0, 1, ntp, 0)).unwrap();
assert!(p.unix_seconds().is_none());
}
#[test]
fn parse_producer_reference_times_walks_top_level_in_order() {
let mut file = Vec::new();
file.extend_from_slice(&wrap_box(b"styp", &[0u8; 8]));
file.extend_from_slice(&wrap_box(b"prft", &prft_body(0, 0, 1, 100 << 32, 10)));
file.extend_from_slice(&wrap_box(b"prft", &prft_body(1, 0, 2, 200 << 32, 20)));
file.extend_from_slice(&wrap_box(b"moof", &[0u8; 4]));
let prfts = parse_producer_reference_times(&file);
assert_eq!(prfts.len(), 2);
assert_eq!(prfts[0].reference_track_id, 1);
assert_eq!(prfts[0].ntp_seconds(), 100);
assert_eq!(prfts[1].reference_track_id, 2);
assert_eq!(prfts[1].version, 1);
}
#[test]
fn parse_producer_reference_times_empty_when_absent() {
let file = wrap_box(b"moov", &[0u8; 4]);
assert!(parse_producer_reference_times(&file).is_empty());
}
#[test]
fn parse_producer_reference_times_skips_malformed() {
let mut file = Vec::new();
file.extend_from_slice(&wrap_box(b"prft", &prft_body(0, 0, 1, 100 << 32, 10)[..10]));
file.extend_from_slice(&wrap_box(b"prft", &prft_body(0, 0, 9, 300 << 32, 30)));
let prfts = parse_producer_reference_times(&file);
assert_eq!(prfts.len(), 1);
assert_eq!(prfts[0].reference_track_id, 9);
}
fn ssix_body(subsegments: &[&[(u8, u32)]]) -> Vec<u8> {
let mut body = vec![0u8, 0, 0, 0]; body.extend_from_slice(&(subsegments.len() as u32).to_be_bytes());
for ss in subsegments {
body.extend_from_slice(&(ss.len() as u32).to_be_bytes());
for &(level, range_size) in *ss {
body.push(level);
body.push((range_size >> 16) as u8);
body.push((range_size >> 8) as u8);
body.push(range_size as u8);
}
}
body
}
#[test]
fn parse_ssix_reads_levels_and_24bit_ranges() {
let body = ssix_body(&[
&[(1, 0x10_0000), (2, 0x20_0000)],
&[(1, 100), (2, 200), (3, 300)],
]);
let ssix = parse_ssix(&body).unwrap();
assert_eq!(ssix.subsegments.len(), 2);
assert_eq!(ssix.subsegments[0].len(), 2);
assert_eq!(
ssix.subsegments[0][0],
SubsegmentRange {
level: 1,
range_size: 0x10_0000
}
);
assert_eq!(
ssix.subsegments[0][1],
SubsegmentRange {
level: 2,
range_size: 0x20_0000
}
);
assert_eq!(ssix.subsegments[1].len(), 3);
assert_eq!(ssix.subsegments[1][2].level, 3);
assert_eq!(ssix.subsegments[1][2].range_size, 300);
}
#[test]
fn parse_ssix_max_24bit_range_size() {
let body = ssix_body(&[&[(0, 0xFF_FFFF), (1, 0)]]);
let ssix = parse_ssix(&body).unwrap();
assert_eq!(ssix.subsegments[0][0].range_size, 0xFF_FFFF);
assert_eq!(ssix.subsegments[0][1].range_size, 0);
}
#[test]
fn parse_ssix_empty_subsegment_count() {
let body = ssix_body(&[]);
let ssix = parse_ssix(&body).unwrap();
assert!(ssix.subsegments.is_empty());
}
#[test]
fn parse_ssix_rejects_unknown_version() {
let mut body = ssix_body(&[&[(1, 1), (2, 2)]]);
body[0] = 1; assert!(parse_ssix(&body).is_err());
}
#[test]
fn parse_ssix_rejects_truncated_range() {
let mut body = ssix_body(&[&[(1, 0x123456)]]);
body.pop();
assert!(parse_ssix(&body).is_err());
}
#[test]
fn parse_ssix_rejects_truncated_range_count() {
let body = vec![0u8, 0, 0, 0, 0, 0, 0, 1];
assert!(parse_ssix(&body).is_err());
}
#[test]
fn parse_subsegment_indexes_walks_top_level_in_order() {
let mut file = Vec::new();
file.extend_from_slice(&wrap_box(b"styp", &[0u8; 8]));
file.extend_from_slice(&wrap_box(b"sidx", &[0u8; 12]));
file.extend_from_slice(&wrap_box(b"ssix", &ssix_body(&[&[(1, 10), (2, 20)]])));
file.extend_from_slice(&wrap_box(b"ssix", &ssix_body(&[&[(1, 30), (2, 40)]])));
file.extend_from_slice(&wrap_box(b"moof", &[0u8; 4]));
let idx = parse_subsegment_indexes(&file);
assert_eq!(idx.len(), 2);
assert_eq!(idx[0].subsegments[0][0].range_size, 10);
assert_eq!(idx[1].subsegments[0][1].range_size, 40);
}
#[test]
fn parse_subsegment_indexes_empty_when_absent() {
let file = wrap_box(b"moov", &[0u8; 4]);
assert!(parse_subsegment_indexes(&file).is_empty());
}
#[test]
fn parse_subsegment_indexes_skips_malformed() {
let truncated = {
let mut b = ssix_body(&[&[(1, 0x123456)]]);
b.pop();
b
};
let mut file = Vec::new();
file.extend_from_slice(&wrap_box(b"ssix", &truncated));
file.extend_from_slice(&wrap_box(b"ssix", &ssix_body(&[&[(7, 77), (8, 88)]])));
let idx = parse_subsegment_indexes(&file);
assert_eq!(idx.len(), 1);
assert_eq!(idx[0].subsegments[0][0].level, 7);
}
}