diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 966f12e..ce7b558 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -13,7 +13,7 @@ body: attributes: label: mp4forge Version description: Which version are you using? - placeholder: "0.4.0" + placeholder: "0.5.0" validations: required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 226cc4c..7eacf07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 0.5.0 (April 25, 2026) + +- Added first-class encrypted metadata coverage for typed `senc`, typed `sgpd(seig)`, resolved sample-encryption helpers, and broader encrypted fragmented fixture coverage across extraction, rewrite, and probe flows +- Added additive top-level `sidx` analysis, planning, rewrite, documentation, and example support for the supported fragmented-file layouts +- Expanded typed box coverage across fragmented timing, metadata, and codec families, including `clap`, `SmDm`, `CoLL`, `dec3`, `dac4`, `vvcC`, AVS3, FLAC, MPEG-H, `subs`, `elng`, `ssix`, `leva`, `evte`, `silb`, `emib`, `emeb`, `ID32`, loudness boxes, `prft`, typed `tref` children, `sthd`, `nmhd`, `kind`, `mime`, `cdat`, and selected legacy `uuid` payloads +- Improved low-level robustness by preserving legal trailing bytes in `VisualSampleEntry` layouts and carrying those bytes cleanly through traversal and rewrite paths +- Added `prft` timestamp and flag helpers, richer examples, and broader regression coverage for fragmented, encrypted, metadata-rich, and legacy MP4 layouts + # 0.4.0 (April 22, 2026) - Added richer additive probe surfaces for broader codec families, codec-specific details, media-characteristics reporting, and lighter-weight probe controls for large-file inspection diff --git a/Cargo.toml b/Cargo.toml index 7f816c2..b1d92b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mp4forge" -version = "0.4.0" +version = "0.5.0" edition = "2024" rust-version = "1.88" authors = ["bakgio"] @@ -9,7 +9,7 @@ description = "Rust library and CLI for inspecting, probing, extracting, and rew repository = "https://github.com/bakgio/mp4forge" readme = "README.md" keywords = ["mp4", "isobmff", "parser", "video", "cli"] -categories = ["command-line-utilities", "multimedia::video", "parser-implementations"] +categories = ["command-line-utilities", "multimedia::video"] exclude = [".github/**", "fuzz/**", "tests/**"] [package.metadata.docs.rs] diff --git a/README.md b/README.md index 2f0725d..ac98030 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ - Typed MP4 and ISOBMFF box model with registry-backed custom box support - Low-level traversal, extraction, stringify, probe, and writer APIs - Thin typed path-based helpers and byte-slice convenience wrappers for common extraction, rewrite, and probe flows +- Fragmented top-level `sidx` analysis, planning, and rewrite APIs for supported layouts - Built-in CLI for `dump`, `extract`, `probe`, `psshdump`, `edit`, and `divide` - Shared-fixture coverage for regular MP4, fragmented MP4, encrypted init segments, QuickTime-style metadata cases, and derived real codec fixtures for additional codec-family coverage @@ -26,10 +27,10 @@ ```toml [dependencies] -mp4forge = "0.4.0" +mp4forge = "0.5.0" # With optional features: -# mp4forge = { version = "0.4.0", features = ["serde"] } +# mp4forge = { version = "0.5.0", features = ["serde"] } ``` Install the CLI from crates.io: diff --git a/examples/probe_extended_media_characteristics.rs b/examples/probe_extended_media_characteristics.rs new file mode 100644 index 0000000..d70296a --- /dev/null +++ b/examples/probe_extended_media_characteristics.rs @@ -0,0 +1,47 @@ +use std::env; +use std::fs::File; + +use mp4forge::probe::probe_extended_media_characteristics; + +fn main() -> Result<(), Box> { + let Some(input_path) = env::args().nth(1) else { + eprintln!("usage: cargo run --example probe_extended_media_characteristics -- "); + std::process::exit(1); + }; + + let mut file = File::open(input_path)?; + let summary = probe_extended_media_characteristics(&mut file)?; + + for track in &summary.tracks { + println!( + "track {} sample_entry_type={:?}", + track.summary.summary.track_id, track.summary.sample_entry_type + ); + if let Some(aperture) = track.visual_metadata.clean_aperture.as_ref() { + println!( + " clean aperture: {}/{} x {}/{}", + aperture.width_numerator, + aperture.width_denominator, + aperture.height_numerator, + aperture.height_denominator + ); + } + if let Some(light) = track.visual_metadata.content_light_level.as_ref() { + println!( + " content light level: max_cll={} max_fall={}", + light.max_cll, light.max_fall + ); + } + if let Some(display) = track.visual_metadata.mastering_display.as_ref() { + println!( + " mastering display: white_point=({}, {}) luminance={}..{}", + display.white_point_chromaticity_x, + display.white_point_chromaticity_y, + display.luminance_min, + display.luminance_max + ); + } + } + + Ok(()) +} diff --git a/examples/refresh_top_level_sidx.rs b/examples/refresh_top_level_sidx.rs new file mode 100644 index 0000000..cf6e05a --- /dev/null +++ b/examples/refresh_top_level_sidx.rs @@ -0,0 +1,60 @@ +use std::env; +use std::error::Error; +use std::io::Cursor; + +use mp4forge::sidx::{ + TopLevelSidxPlanAction, TopLevelSidxPlanOptions, apply_top_level_sidx_plan, + plan_top_level_sidx_update_bytes, +}; + +fn main() { + if let Err(error) = run() { + eprintln!("{error}"); + std::process::exit(1); + } +} + +fn run() -> Result<(), Box> { + let args = env::args().skip(1).collect::>(); + let (input_path, output_path, non_zero_ept) = match args.as_slice() { + [input_path, output_path] => (input_path.as_str(), output_path.as_str(), false), + [input_path, output_path, flag] if flag == "--non-zero-ept" => { + (input_path.as_str(), output_path.as_str(), true) + } + _ => { + return Err( + "usage: cargo run --example refresh_top_level_sidx -- [--non-zero-ept]" + .into(), + ); + } + }; + + let input = std::fs::read(input_path)?; + let Some(plan) = plan_top_level_sidx_update_bytes( + &input, + TopLevelSidxPlanOptions { + add_if_not_exists: true, + non_zero_ept, + }, + )? + else { + return Err("no top-level sidx change was needed".into()); + }; + + let action = match &plan.action { + TopLevelSidxPlanAction::Insert => "inserted", + TopLevelSidxPlanAction::Replace { .. } => "updated", + }; + + let mut output = Vec::with_capacity(input.len().saturating_add(plan.encoded_box_size as usize)); + let applied = apply_top_level_sidx_plan(&mut Cursor::new(&input), &mut output, &plan)?; + std::fs::write(output_path, &output)?; + + println!( + "{action} top-level sidx at offset {} with {} references", + applied.info.offset(), + applied.sidx.reference_count + ); + + Ok(()) +} diff --git a/examples/resolve_sample_encryption.rs b/examples/resolve_sample_encryption.rs new file mode 100644 index 0000000..67d64b5 --- /dev/null +++ b/examples/resolve_sample_encryption.rs @@ -0,0 +1,121 @@ +use std::env; +use std::error::Error; + +use mp4forge::FourCc; +use mp4forge::boxes::iso14496_12::{Saiz, Sbgp, Sgpd}; +use mp4forge::boxes::iso23001_7::{Senc, Tenc}; +use mp4forge::encryption::{ + ResolvedSampleEncryptionSource, SampleEncryptionContext, resolve_sample_encryption, +}; +use mp4forge::extract::extract_box_as_bytes; +use mp4forge::walk::BoxPath; + +fn main() { + if let Err(error) = run() { + eprintln!("{error}"); + std::process::exit(1); + } +} + +fn run() -> Result<(), Box> { + let Some(path) = env::args().nth(1) else { + return Err("usage: cargo run --example resolve_sample_encryption -- ".into()); + }; + + let input = std::fs::read(path)?; + let tenc = extract_box_as_bytes::( + &input, + BoxPath::from([ + FourCc::from_bytes(*b"moov"), + FourCc::from_bytes(*b"trak"), + FourCc::from_bytes(*b"mdia"), + FourCc::from_bytes(*b"minf"), + FourCc::from_bytes(*b"stbl"), + FourCc::from_bytes(*b"stsd"), + FourCc::ANY, + FourCc::from_bytes(*b"sinf"), + FourCc::from_bytes(*b"schi"), + FourCc::from_bytes(*b"tenc"), + ]), + )? + .into_iter() + .next(); + + let senc = extract_box_as_bytes::( + &input, + BoxPath::from([ + FourCc::from_bytes(*b"moof"), + FourCc::from_bytes(*b"traf"), + FourCc::from_bytes(*b"senc"), + ]), + )? + .into_iter() + .next() + .ok_or("no senc box found")?; + + let sgpd = extract_box_as_bytes::( + &input, + BoxPath::from([ + FourCc::from_bytes(*b"moof"), + FourCc::from_bytes(*b"traf"), + FourCc::from_bytes(*b"sgpd"), + ]), + )? + .into_iter() + .next(); + let sbgp = extract_box_as_bytes::( + &input, + BoxPath::from([ + FourCc::from_bytes(*b"moof"), + FourCc::from_bytes(*b"traf"), + FourCc::from_bytes(*b"sbgp"), + ]), + )? + .into_iter() + .next(); + let saiz = extract_box_as_bytes::( + &input, + BoxPath::from([ + FourCc::from_bytes(*b"moof"), + FourCc::from_bytes(*b"traf"), + FourCc::from_bytes(*b"saiz"), + ]), + )? + .into_iter() + .next(); + + let resolved = resolve_sample_encryption( + &senc, + SampleEncryptionContext { + tenc: tenc.as_ref(), + sgpd: sgpd.as_ref(), + sbgp: sbgp.as_ref(), + saiz: saiz.as_ref(), + }, + )?; + + for sample in resolved.samples { + let source = match sample.metadata_source { + ResolvedSampleEncryptionSource::TrackEncryptionBox => "tenc".to_string(), + ResolvedSampleEncryptionSource::SampleGroupDescription { + group_description_index, + description_index, + fragment_local, + } => format!( + "sgpd(seig) group_description_index={} description_index={} fragment_local={}", + group_description_index, description_index, fragment_local + ), + }; + + println!( + "sample {} source={} protected={} iv_len={} aux_size={}", + sample.sample_index, + source, + sample.is_protected, + sample.effective_initialization_vector().len(), + sample.auxiliary_info_size + ); + } + + Ok(()) +} diff --git a/src/boxes/avs3.rs b/src/boxes/avs3.rs new file mode 100644 index 0000000..52b6175 --- /dev/null +++ b/src/boxes/avs3.rs @@ -0,0 +1,155 @@ +//! AVS3 sample-entry and decoder-configuration box definitions. + +use std::io::Write; + +use super::iso14496_12::VisualSampleEntry; +use crate::boxes::BoxRegistry; +use crate::codec::{ + CodecBox, CodecError, FieldHooks, FieldTable, FieldValue, FieldValueError, FieldValueRead, + FieldValueWrite, ImmutableBox, MutableBox, +}; +use crate::{FourCc, codec_field}; + +fn missing_field(field_name: &'static str) -> FieldValueError { + FieldValueError::MissingField { field_name } +} + +fn unexpected_field(field_name: &'static str, value: FieldValue) -> FieldValueError { + FieldValueError::UnexpectedType { + field_name, + expected: "matching codec field value", + actual: value.kind_name(), + } +} + +fn invalid_value(field_name: &'static str, reason: &'static str) -> FieldValueError { + FieldValueError::InvalidValue { field_name, reason } +} + +fn u8_from_unsigned(field_name: &'static str, value: u64) -> Result { + u8::try_from(value).map_err(|_| invalid_value(field_name, "value does not fit in u8")) +} + +fn u16_from_unsigned(field_name: &'static str, value: u64) -> Result { + u16::try_from(value).map_err(|_| invalid_value(field_name, "value does not fit in u16")) +} + +/// AVS3 decoder-configuration box carried by `avs3` sample entries. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Av3c { + /// Decoder-configuration record version. + pub configuration_version: u8, + /// Declared byte length of the sequence header. + pub sequence_header_length: u16, + /// Opaque AVS3 sequence-header bytes. + pub sequence_header: Vec, + /// Two-bit library-dependency identifier. + pub library_dependency_idc: u8, +} + +impl FieldHooks for Av3c { + fn field_length(&self, name: &'static str) -> Option { + match name { + "SequenceHeader" => Some(u32::from(self.sequence_header_length)), + _ => None, + } + } +} + +impl ImmutableBox for Av3c { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"av3c") + } +} + +impl MutableBox for Av3c {} + +impl FieldValueRead for Av3c { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "ConfigurationVersion" => { + Ok(FieldValue::Unsigned(u64::from(self.configuration_version))) + } + "SequenceHeaderLength" => { + Ok(FieldValue::Unsigned(u64::from(self.sequence_header_length))) + } + "SequenceHeader" => Ok(FieldValue::Bytes(self.sequence_header.clone())), + "LibraryDependencyIDC" => { + Ok(FieldValue::Unsigned(u64::from(self.library_dependency_idc))) + } + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Av3c { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("ConfigurationVersion", FieldValue::Unsigned(value)) => { + self.configuration_version = u8_from_unsigned(field_name, value)?; + Ok(()) + } + ("SequenceHeaderLength", FieldValue::Unsigned(value)) => { + self.sequence_header_length = u16_from_unsigned(field_name, value)?; + Ok(()) + } + ("SequenceHeader", FieldValue::Bytes(value)) => { + self.sequence_header = value; + Ok(()) + } + ("LibraryDependencyIDC", FieldValue::Unsigned(value)) => { + self.library_dependency_idc = u8_from_unsigned(field_name, value)?; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Av3c { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("ConfigurationVersion", 0, with_bit_width(8)), + codec_field!("SequenceHeaderLength", 1, with_bit_width(16)), + codec_field!( + "SequenceHeader", + 2, + with_bit_width(8), + with_dynamic_length(), + as_bytes() + ), + codec_field!( + "Reserved", + 3, + with_bit_width(6), + with_constant("63"), + as_hidden() + ), + codec_field!("LibraryDependencyIDC", 4, with_bit_width(2), as_hex()), + ]); + + fn custom_marshal(&self, _writer: &mut dyn Write) -> Result, CodecError> { + if usize::from(self.sequence_header_length) != self.sequence_header.len() { + return Err(invalid_value( + "SequenceHeader", + "length does not match SequenceHeaderLength", + ) + .into()); + } + if self.library_dependency_idc > 0x03 { + return Err( + invalid_value("LibraryDependencyIDC", "value does not fit in 2 bits").into(), + ); + } + Ok(None) + } +} + +/// Registers the currently implemented AVS3 boxes in `registry`. +pub fn register_boxes(registry: &mut BoxRegistry) { + registry.register_any::(FourCc::from_bytes(*b"avs3")); + registry.register::(FourCc::from_bytes(*b"av3c")); +} diff --git a/src/boxes/etsi_ts_102_366.rs b/src/boxes/etsi_ts_102_366.rs index 80a7e74..d347542 100644 --- a/src/boxes/etsi_ts_102_366.rs +++ b/src/boxes/etsi_ts_102_366.rs @@ -1,6 +1,9 @@ -//! ETSI TS 102 366 AC-3 sample-entry and decoder-configuration box definitions. +//! ETSI TS 102 366 AC-3 and E-AC-3 sample-entry and decoder-configuration box definitions. + +use std::io::{Cursor, Read}; use super::iso14496_12::AudioSampleEntry; +use crate::bitio::{BitReader, BitWriter}; use crate::boxes::BoxRegistry; use crate::codec::{ CodecBox, FieldHooks, FieldTable, FieldValue, FieldValueError, FieldValueRead, FieldValueWrite, @@ -28,6 +31,259 @@ fn u8_from_unsigned(field_name: &'static str, value: u64) -> Result Result { + u16::try_from(value).map_err(|_| invalid_value(field_name, "value does not fit in u16")) +} + +fn read_u8_bits( + reader: &mut BitReader>, + width: usize, + field_name: &'static str, +) -> Result { + let data = reader + .read_bits(width) + .map_err(|_| invalid_value(field_name, "substream payload is truncated"))?; + Ok(data + .into_iter() + .fold(0_u16, |acc, byte| (acc << 8) | u16::from(byte)) as u8) +} + +fn read_u16_bits( + reader: &mut BitReader>, + width: usize, + field_name: &'static str, +) -> Result { + let data = reader + .read_bits(width) + .map_err(|_| invalid_value(field_name, "substream payload is truncated"))?; + Ok(data + .into_iter() + .fold(0_u16, |acc, byte| (acc << 8) | u16::from(byte))) +} + +fn format_bytes(bytes: &[u8]) -> String { + let rendered = bytes + .iter() + .map(|byte| format!("0x{byte:x}")) + .collect::>() + .join(", "); + format!("[{rendered}]") +} + +fn render_ec3_substream(substream: &Ec3Substream) -> String { + format!( + "{{FSCod=0x{:x} BSID=0x{:x} ASVC=0x{:x} BSMod=0x{:x} ACMod=0x{:x} LFEOn=0x{:x} NumDepSub=0x{:x} ChanLoc=0x{:x}}}", + substream.fscod, + substream.bsid, + substream.asvc, + substream.bsmod, + substream.acmod, + substream.lfe_on, + substream.num_dep_sub, + substream.chan_loc + ) +} + +fn render_ec3_substreams(substreams: &[Ec3Substream]) -> String { + let rendered = substreams + .iter() + .map(render_ec3_substream) + .collect::>() + .join(", "); + format!("[{rendered}]") +} + +fn require_ec3_substream_count( + field_name: &'static str, + num_ind_sub: u8, + actual_count: usize, +) -> Result<(), FieldValueError> { + let expected_count = usize::from(num_ind_sub) + 1; + if actual_count != expected_count { + return Err(invalid_value( + field_name, + "num_ind_sub does not match the parsed substream count", + )); + } + Ok(()) +} + +fn validate_ec3_substream( + field_name: &'static str, + substream: &Ec3Substream, +) -> Result<(), FieldValueError> { + if substream.fscod > 0x03 { + return Err(invalid_value( + field_name, + "substream fscod does not fit in 2 bits", + )); + } + if substream.bsid > 0x1f { + return Err(invalid_value( + field_name, + "substream bsid does not fit in 5 bits", + )); + } + if substream.asvc > 0x01 { + return Err(invalid_value( + field_name, + "substream asvc does not fit in 1 bit", + )); + } + if substream.bsmod > 0x07 { + return Err(invalid_value( + field_name, + "substream bsmod does not fit in 3 bits", + )); + } + if substream.acmod > 0x07 { + return Err(invalid_value( + field_name, + "substream acmod does not fit in 3 bits", + )); + } + if substream.lfe_on > 0x01 { + return Err(invalid_value( + field_name, + "substream lfe_on does not fit in 1 bit", + )); + } + if substream.num_dep_sub > 0x0f { + return Err(invalid_value( + field_name, + "substream num_dep_sub does not fit in 4 bits", + )); + } + if substream.chan_loc > 0x01ff { + return Err(invalid_value( + field_name, + "substream chan_loc does not fit in 9 bits", + )); + } + if substream.num_dep_sub == 0 && substream.chan_loc != 0 { + return Err(invalid_value( + field_name, + "substream chan_loc requires num_dep_sub to be non-zero", + )); + } + Ok(()) +} + +fn encode_ec3_substreams( + field_name: &'static str, + num_ind_sub: u8, + substreams: &[Ec3Substream], + reserved: &[u8], +) -> Result, FieldValueError> { + require_ec3_substream_count(field_name, num_ind_sub, substreams.len())?; + let mut writer = BitWriter::new(Vec::new()); + for substream in substreams { + validate_ec3_substream(field_name, substream)?; + writer + .write_bits(&[substream.fscod], 2) + .map_err(|_| invalid_value(field_name, "failed to encode substream payload"))?; + writer + .write_bits(&[substream.bsid], 5) + .map_err(|_| invalid_value(field_name, "failed to encode substream payload"))?; + writer + .write_bits(&[0], 1) + .map_err(|_| invalid_value(field_name, "failed to encode substream payload"))?; + writer + .write_bits(&[substream.asvc], 1) + .map_err(|_| invalid_value(field_name, "failed to encode substream payload"))?; + writer + .write_bits(&[substream.bsmod], 3) + .map_err(|_| invalid_value(field_name, "failed to encode substream payload"))?; + writer + .write_bits(&[substream.acmod], 3) + .map_err(|_| invalid_value(field_name, "failed to encode substream payload"))?; + writer + .write_bits(&[substream.lfe_on], 1) + .map_err(|_| invalid_value(field_name, "failed to encode substream payload"))?; + writer + .write_bits(&[0], 3) + .map_err(|_| invalid_value(field_name, "failed to encode substream payload"))?; + writer + .write_bits(&[substream.num_dep_sub], 4) + .map_err(|_| invalid_value(field_name, "failed to encode substream payload"))?; + if substream.num_dep_sub > 0 { + writer + .write_bits(&substream.chan_loc.to_be_bytes(), 9) + .map_err(|_| invalid_value(field_name, "failed to encode substream payload"))?; + } else { + writer + .write_bits(&[0], 1) + .map_err(|_| invalid_value(field_name, "failed to encode substream payload"))?; + } + } + let mut encoded = writer + .into_inner() + .map_err(|_| invalid_value(field_name, "encoded substream payload is not aligned"))?; + encoded.extend_from_slice(reserved); + Ok(encoded) +} + +fn parse_ec3_substreams( + field_name: &'static str, + num_ind_sub: u8, + payload: &[u8], +) -> Result<(Vec, Vec), FieldValueError> { + let expected_count = usize::from(num_ind_sub) + 1; + let mut reader = BitReader::new(Cursor::new(payload)); + let mut substreams = Vec::with_capacity(expected_count); + + for _ in 0..expected_count { + let fscod = read_u8_bits(&mut reader, 2, field_name)?; + let bsid = read_u8_bits(&mut reader, 5, field_name)?; + if read_u8_bits(&mut reader, 1, field_name)? != 0 { + return Err(invalid_value( + field_name, + "substream reserved bit is not zero", + )); + } + let asvc = read_u8_bits(&mut reader, 1, field_name)?; + let bsmod = read_u8_bits(&mut reader, 3, field_name)?; + let acmod = read_u8_bits(&mut reader, 3, field_name)?; + let lfe_on = read_u8_bits(&mut reader, 1, field_name)?; + if read_u8_bits(&mut reader, 3, field_name)? != 0 { + return Err(invalid_value( + field_name, + "substream reserved bits are not zero", + )); + } + let num_dep_sub = read_u8_bits(&mut reader, 4, field_name)?; + let chan_loc = if num_dep_sub > 0 { + read_u16_bits(&mut reader, 9, field_name)? + } else { + if read_u8_bits(&mut reader, 1, field_name)? != 0 { + return Err(invalid_value( + field_name, + "substream reserved chan_loc bit is not zero", + )); + } + 0 + }; + + substreams.push(Ec3Substream { + fscod, + bsid, + asvc, + bsmod, + acmod, + lfe_on, + num_dep_sub, + chan_loc, + }); + } + + let mut reserved = Vec::new(); + reader + .read_to_end(&mut reserved) + .map_err(|_| invalid_value(field_name, "substream payload is truncated"))?; + + Ok((substreams, reserved)) +} + /// AC-3 decoder configuration box carried by `ac-3` sample entries. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Dac3 { @@ -111,8 +367,108 @@ impl CodecBox for Dac3 { ]); } +/// E-AC-3 substream descriptor stored inside a `dec3` decoder configuration box. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Ec3Substream { + pub fscod: u8, + pub bsid: u8, + pub asvc: u8, + pub bsmod: u8, + pub acmod: u8, + pub lfe_on: u8, + pub num_dep_sub: u8, + pub chan_loc: u16, +} + +/// E-AC-3 decoder configuration box carried by `ec-3` sample entries. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Dec3 { + pub data_rate: u16, + pub num_ind_sub: u8, + pub ec3_substreams: Vec, + pub reserved: Vec, +} + +impl FieldHooks for Dec3 { + fn display_field(&self, name: &'static str) -> Option { + match name { + "EC3Subs" => Some(if self.reserved.is_empty() { + render_ec3_substreams(&self.ec3_substreams) + } else { + format!( + "{} (Reserved={})", + render_ec3_substreams(&self.ec3_substreams), + format_bytes(&self.reserved) + ) + }), + _ => None, + } + } +} + +impl ImmutableBox for Dec3 { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"dec3") + } +} + +impl MutableBox for Dec3 {} + +impl FieldValueRead for Dec3 { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "DataRate" => Ok(FieldValue::Unsigned(u64::from(self.data_rate))), + "NumIndSub" => Ok(FieldValue::Unsigned(u64::from(self.num_ind_sub))), + "EC3Subs" => Ok(FieldValue::Bytes(encode_ec3_substreams( + field_name, + self.num_ind_sub, + &self.ec3_substreams, + &self.reserved, + )?)), + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Dec3 { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("DataRate", FieldValue::Unsigned(value)) => { + self.data_rate = u16_from_unsigned(field_name, value)?; + Ok(()) + } + ("NumIndSub", FieldValue::Unsigned(value)) => { + self.num_ind_sub = u8_from_unsigned(field_name, value)?; + Ok(()) + } + ("EC3Subs", FieldValue::Bytes(value)) => { + let (ec3_substreams, reserved) = + parse_ec3_substreams(field_name, self.num_ind_sub, &value)?; + self.ec3_substreams = ec3_substreams; + self.reserved = reserved; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Dec3 { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("DataRate", 0, with_bit_width(13)), + codec_field!("NumIndSub", 1, with_bit_width(3)), + codec_field!("EC3Subs", 2, with_bit_width(8), as_bytes()), + ]); +} + /// Registers the currently implemented ETSI TS 102 366 boxes in `registry`. pub fn register_boxes(registry: &mut BoxRegistry) { registry.register_any::(FourCc::from_bytes(*b"ac-3")); + registry.register_any::(FourCc::from_bytes(*b"ec-3")); registry.register::(FourCc::from_bytes(*b"dac3")); + registry.register::(FourCc::from_bytes(*b"dec3")); } diff --git a/src/boxes/etsi_ts_103_190.rs b/src/boxes/etsi_ts_103_190.rs new file mode 100644 index 0000000..e66a9b4 --- /dev/null +++ b/src/boxes/etsi_ts_103_190.rs @@ -0,0 +1,76 @@ +//! ETSI TS 103 190 AC-4 sample-entry and decoder-configuration box definitions. + +use super::iso14496_12::AudioSampleEntry; +use crate::boxes::BoxRegistry; +use crate::codec::{ + CodecBox, FieldHooks, FieldTable, FieldValue, FieldValueError, FieldValueRead, FieldValueWrite, + ImmutableBox, MutableBox, +}; +use crate::{FourCc, codec_field}; + +fn missing_field(field_name: &'static str) -> FieldValueError { + FieldValueError::MissingField { field_name } +} + +fn unexpected_field(field_name: &'static str, value: FieldValue) -> FieldValueError { + FieldValueError::UnexpectedType { + field_name, + expected: "matching codec field value", + actual: value.kind_name(), + } +} + +/// AC-4 decoder configuration box carried by `ac-4` sample entries. +/// +/// The decoder-specific syntax is intentionally preserved as raw bytes so the box remains +/// roundtrip-safe while the crate grows its typed AC-4 coverage incrementally. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Dac4 { + pub data: Vec, +} + +impl FieldHooks for Dac4 {} + +impl ImmutableBox for Dac4 { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"dac4") + } +} + +impl MutableBox for Dac4 {} + +impl FieldValueRead for Dac4 { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "Data" => Ok(FieldValue::Bytes(self.data.clone())), + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Dac4 { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("Data", FieldValue::Bytes(data)) => { + self.data = data; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Dac4 { + const FIELD_TABLE: FieldTable = + FieldTable::new(&[codec_field!("Data", 0, with_bit_width(8), as_bytes())]); +} + +/// Registers the currently implemented ETSI TS 103 190 boxes in `registry`. +pub fn register_boxes(registry: &mut BoxRegistry) { + registry.register_any::(FourCc::from_bytes(*b"ac-4")); + registry.register::(FourCc::from_bytes(*b"dac4")); +} diff --git a/src/boxes/flac.rs b/src/boxes/flac.rs new file mode 100644 index 0000000..3c1cd34 --- /dev/null +++ b/src/boxes/flac.rs @@ -0,0 +1,331 @@ +//! FLAC sample-entry and decoder-configuration box definitions. + +use std::io::Write; + +use super::iso14496_12::AudioSampleEntry; +use crate::boxes::BoxRegistry; +use crate::codec::{ + CodecBox, CodecError, FieldHooks, FieldTable, FieldValue, FieldValueError, FieldValueRead, + FieldValueWrite, ImmutableBox, MutableBox, ReadSeek, read_exact_vec_untrusted, +}; +use crate::{FourCc, codec_field}; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +struct FullBoxState { + version: u8, + flags: u32, +} + +fn missing_field(field_name: &'static str) -> FieldValueError { + FieldValueError::MissingField { field_name } +} + +fn unexpected_field(field_name: &'static str, value: FieldValue) -> FieldValueError { + FieldValueError::UnexpectedType { + field_name, + expected: "matching codec field value", + actual: value.kind_name(), + } +} + +fn invalid_value(field_name: &'static str, reason: &'static str) -> FieldValueError { + FieldValueError::InvalidValue { field_name, reason } +} + +fn u8_from_unsigned(field_name: &'static str, value: u64) -> Result { + u8::try_from(value).map_err(|_| invalid_value(field_name, "value does not fit in u8")) +} + +fn u32_from_unsigned(field_name: &'static str, value: u64) -> Result { + u32::try_from(value).map_err(|_| invalid_value(field_name, "value does not fit in u32")) +} + +fn render_hex_bytes(bytes: &[u8]) -> String { + format!( + "[{}]", + bytes + .iter() + .map(|byte| format!("0x{byte:x}")) + .collect::>() + .join(", ") + ) +} + +fn render_metadata_blocks(blocks: &[FlacMetadataBlock]) -> String { + format!( + "[{}]", + blocks + .iter() + .map(|block| { + format!( + "{{LastMetadataBlockFlag={} BlockType={} Length={} BlockData={}}}", + block.last_metadata_block_flag, + block.block_type, + block.length, + render_hex_bytes(&block.block_data) + ) + }) + .collect::>() + .join(", ") + ) +} + +fn write_u24(writer: &mut Vec, value: u32) { + writer.push(((value >> 16) & 0xff) as u8); + writer.push(((value >> 8) & 0xff) as u8); + writer.push((value & 0xff) as u8); +} + +fn read_u24(bytes: &[u8], offset: usize) -> u32 { + (u32::from(bytes[offset]) << 16) + | (u32::from(bytes[offset + 1]) << 8) + | u32::from(bytes[offset + 2]) +} + +fn encode_metadata_blocks( + field_name: &'static str, + blocks: &[FlacMetadataBlock], +) -> Result, FieldValueError> { + let mut bytes = Vec::new(); + + for (index, block) in blocks.iter().enumerate() { + if block.block_type > 0x7f { + return Err(invalid_value("BlockType", "value does not fit in 7 bits")); + } + if block.length > 0x00ff_ffff { + return Err(invalid_value("Length", "value does not fit in 24 bits")); + } + if usize::try_from(block.length).ok() != Some(block.block_data.len()) { + return Err(invalid_value( + field_name, + "block length does not match BlockData length", + )); + } + if index + 1 == blocks.len() { + if !block.last_metadata_block_flag { + return Err(invalid_value( + field_name, + "final metadata block flag must be set", + )); + } + } else if block.last_metadata_block_flag { + return Err(invalid_value( + field_name, + "last metadata block flag must only appear on the final block", + )); + } + + bytes.push((u8::from(block.last_metadata_block_flag) << 7) | block.block_type); + write_u24(&mut bytes, block.length); + bytes.extend_from_slice(&block.block_data); + } + + Ok(bytes) +} + +fn decode_metadata_blocks( + field_name: &'static str, + payload: &[u8], +) -> Result, FieldValueError> { + let mut offset = 0usize; + let mut blocks = Vec::new(); + + while offset != payload.len() { + if payload.len().saturating_sub(offset) < 4 { + return Err(invalid_value( + field_name, + "metadata block header is truncated", + )); + } + let first_byte = payload[offset]; + offset += 1; + let last_metadata_block_flag = (first_byte & 0x80) != 0; + let block_type = first_byte & 0x7f; + let length = read_u24(payload, offset); + offset += 3; + let block_len = usize::try_from(length).map_err(|_| { + invalid_value(field_name, "metadata block length does not fit in usize") + })?; + if payload.len().saturating_sub(offset) < block_len { + return Err(invalid_value( + field_name, + "metadata block payload is truncated", + )); + } + let block_data = payload[offset..offset + block_len].to_vec(); + offset += block_len; + + if last_metadata_block_flag && offset != payload.len() { + return Err(invalid_value( + field_name, + "last metadata block flag must only appear on the final block", + )); + } + + blocks.push(FlacMetadataBlock { + last_metadata_block_flag, + block_type, + length, + block_data, + }); + } + + if blocks + .last() + .is_some_and(|block| !block.last_metadata_block_flag) + { + return Err(invalid_value( + field_name, + "final metadata block flag must be set", + )); + } + + Ok(blocks) +} + +/// One FLAC metadata block carried by `dfLa`. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct FlacMetadataBlock { + /// Whether this block is the final block in the `dfLa` payload. + pub last_metadata_block_flag: bool, + /// Seven-bit FLAC metadata-block type. + pub block_type: u8, + /// Declared payload length in bytes. + pub length: u32, + /// Opaque block payload bytes. + pub block_data: Vec, +} + +/// FLAC-specific configuration box carried by `fLaC` sample entries. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct DfLa { + full_box: FullBoxState, + /// Ordered FLAC metadata blocks. + pub metadata_blocks: Vec, +} + +impl FieldHooks for DfLa { + fn display_field(&self, name: &'static str) -> Option { + match name { + "MetadataBlocks" => Some(render_metadata_blocks(&self.metadata_blocks)), + _ => None, + } + } +} + +impl ImmutableBox for DfLa { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"dfLa") + } + + fn version(&self) -> u8 { + self.full_box.version + } + + fn flags(&self) -> u32 { + self.full_box.flags + } +} + +impl MutableBox for DfLa { + fn set_version(&mut self, version: u8) { + self.full_box.version = version; + } + + fn set_flags(&mut self, flags: u32) { + self.full_box.flags = flags; + } +} + +impl FieldValueRead for DfLa { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "Version" => Ok(FieldValue::Unsigned(u64::from(self.version()))), + "Flags" => Ok(FieldValue::Unsigned(u64::from(self.flags()))), + "MetadataBlocks" => Ok(FieldValue::Bytes(encode_metadata_blocks( + field_name, + &self.metadata_blocks, + )?)), + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for DfLa { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("Version", FieldValue::Unsigned(value)) => { + self.full_box.version = u8_from_unsigned(field_name, value)?; + Ok(()) + } + ("Flags", FieldValue::Unsigned(value)) => { + self.full_box.flags = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("MetadataBlocks", FieldValue::Bytes(value)) => { + self.metadata_blocks = decode_metadata_blocks(field_name, &value)?; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for DfLa { + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("MetadataBlocks", 2, with_bit_width(8), as_bytes()), + ]); + + fn custom_marshal(&self, writer: &mut dyn Write) -> Result, CodecError> { + if self.version() != 0 { + return Err(invalid_value("Version", "unsupported version").into()); + } + if self.flags() != 0 { + return Err(invalid_value("Flags", "unsupported flags").into()); + } + + let blocks = encode_metadata_blocks("MetadataBlocks", &self.metadata_blocks)?; + writer.write_all(&[self.version()])?; + writer.write_all(&self.flags().to_be_bytes()[1..])?; + writer.write_all(&blocks)?; + Ok(Some(4 + blocks.len() as u64)) + } + + fn custom_unmarshal( + &mut self, + reader: &mut dyn ReadSeek, + payload_size: u64, + ) -> Result, CodecError> { + if payload_size < 4 { + return Err(invalid_value("Payload", "payload is too short").into()); + } + + let payload = read_exact_vec_untrusted(reader, payload_size as usize)?; + let version = payload[0]; + let flags = + (u32::from(payload[1]) << 16) | (u32::from(payload[2]) << 8) | u32::from(payload[3]); + if version != 0 { + return Err(invalid_value("Version", "unsupported version").into()); + } + if flags != 0 { + return Err(invalid_value("Flags", "unsupported flags").into()); + } + + self.full_box = FullBoxState { version, flags }; + self.metadata_blocks = decode_metadata_blocks("MetadataBlocks", &payload[4..])?; + Ok(Some(payload_size)) + } +} + +/// Registers the currently implemented FLAC boxes in `registry`. +pub fn register_boxes(registry: &mut BoxRegistry) { + registry.register_any::(FourCc::from_bytes(*b"fLaC")); + registry.register::(FourCc::from_bytes(*b"dfLa")); +} diff --git a/src/boxes/iso14496_12.rs b/src/boxes/iso14496_12.rs index afea8f5..6e91a87 100644 --- a/src/boxes/iso14496_12.rs +++ b/src/boxes/iso14496_12.rs @@ -1,13 +1,18 @@ //! Core ISO BMFF timing and structure boxes. -use std::io::{SeekFrom, Write}; +use std::io::{Cursor, SeekFrom, Write}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use crate::boxes::iso23001_7::{ + Senc, decode_senc_payload, encode_senc_payload, render_senc_samples_display, +}; use crate::boxes::{AnyTypeBox, BoxLookupContext, BoxRegistry}; use crate::codec::{ - CodecBox, CodecError, FieldHooks, FieldTable, FieldValue, FieldValueError, FieldValueRead, - FieldValueWrite, ImmutableBox, MutableBox, ReadSeek, StringFieldMode, read_exact_vec_untrusted, - untrusted_prealloc_hint, + ANY_VERSION, CodecBox, CodecError, FieldHooks, FieldTable, FieldValue, FieldValueError, + FieldValueRead, FieldValueWrite, ImmutableBox, MutableBox, ReadSeek, StringFieldMode, + read_exact_vec_untrusted, untrusted_prealloc_hint, }; +use crate::header::{BoxInfo, SMALL_HEADER_SIZE}; use crate::{FourCc, codec_field}; const URL_SELF_CONTAINED: u32 = 0x000001; @@ -19,6 +24,23 @@ const COLR_NCLX: FourCc = FourCc::from_bytes(*b"nclx"); const COLR_RICC: FourCc = FourCc::from_bytes(*b"rICC"); const COLR_PROF: FourCc = FourCc::from_bytes(*b"prof"); +/// User-type identifier for the spherical-video XML payload carried in `uuid` boxes. +pub const UUID_SPHERICAL_VIDEO_V1: [u8; 16] = [ + 0xff, 0xcc, 0x82, 0x63, 0xf8, 0x55, 0x4a, 0x93, 0x88, 0x14, 0x58, 0x7a, 0x02, 0x52, 0x1f, 0xdd, +]; +/// User-type identifier for the fragment-absolute-timing payload carried in `uuid` boxes. +pub const UUID_FRAGMENT_ABSOLUTE_TIMING: [u8; 16] = [ + 0x6d, 0x1d, 0x9b, 0x05, 0x42, 0xd5, 0x44, 0xe6, 0x80, 0xe2, 0x14, 0x1d, 0xaf, 0xf7, 0x57, 0xb2, +]; +/// User-type identifier for the fragment-run table payload carried in `uuid` boxes. +pub const UUID_FRAGMENT_RUN_TABLE: [u8; 16] = [ + 0xd4, 0x80, 0x7e, 0xf2, 0xca, 0x39, 0x46, 0x95, 0x8e, 0x54, 0x26, 0xcb, 0x9e, 0x46, 0xa7, 0x9f, +]; +/// User-type identifier for the sample-encryption payload carried in `uuid` boxes. +pub const UUID_SAMPLE_ENCRYPTION: [u8; 16] = [ + 0xa2, 0x39, 0x4f, 0x52, 0x5a, 0x9b, 0x4f, 0x14, 0xa2, 0x44, 0x6c, 0x42, 0x7c, 0x64, 0x8d, 0xf4, +]; + /// `tfhd` flag indicating that `base_data_offset` is present. pub const TFHD_BASE_DATA_OFFSET_PRESENT: u32 = 0x000001; /// `tfhd` flag indicating that `sample_description_index` is present. @@ -46,6 +68,23 @@ pub const TRUN_SAMPLE_SIZE_PRESENT: u32 = 0x000200; pub const TRUN_SAMPLE_FLAGS_PRESENT: u32 = 0x000400; /// `trun` flag indicating that each entry carries a composition time offset. pub const TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT: u32 = 0x000800; +/// Known `prft` flags value for timestamps captured at encoder input. +pub const PRFT_TIME_ENCODER_INPUT: u32 = 0x000000; +/// Known `prft` flags value for timestamps captured at encoder output. +pub const PRFT_TIME_ENCODER_OUTPUT: u32 = 0x000001; +/// Known `prft` flags value for timestamps captured when the containing `moof` was finalized. +pub const PRFT_TIME_MOOF_FINALIZED: u32 = 0x000002; +/// Known `prft` flags value for timestamps captured when the containing `moof` was written. +pub const PRFT_TIME_MOOF_WRITTEN: u32 = 0x000004; +/// Known `prft` flags value for timestamps captured at an arbitrary but internally consistent point. +pub const PRFT_TIME_ARBITRARY_CONSISTENT: u32 = 0x000008; +/// Known `prft` flags value for timestamps captured by an external time source. +pub const PRFT_TIME_CAPTURED: u32 = 0x000018; +/// Number of NTP whole seconds between `1900-01-01` and the UNIX epoch. +pub const PRFT_NTP_UNIX_EPOCH_OFFSET_SECONDS: u64 = 2_208_988_800; + +const PRFT_NTP_FRACTION_SCALE: u128 = 1u128 << 32; +const NANOS_PER_SECOND: u128 = 1_000_000_000; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] struct FullBoxState { @@ -127,6 +166,13 @@ fn bytes_to_fourcc_vec( }) } +fn bytes_to_track_id_vec( + field_name: &'static str, + bytes: Vec, +) -> Result, FieldValueError> { + parse_fixed_chunks(field_name, &bytes, 4, |chunk| read_u32(chunk, 0)) +} + fn fourcc_vec_to_bytes(values: &[FourCc]) -> Vec { let mut bytes = Vec::with_capacity(values.len() * 4); for value in values { @@ -135,6 +181,14 @@ fn fourcc_vec_to_bytes(values: &[FourCc]) -> Vec { bytes } +fn track_id_vec_to_bytes(values: &[u32]) -> Vec { + let mut bytes = Vec::with_capacity(values.len() * 4); + for value in values { + bytes.extend_from_slice(&value.to_be_bytes()); + } + bytes +} + fn parse_fixed_chunks( field_name: &'static str, bytes: &[u8], @@ -170,6 +224,619 @@ fn render_hex_bytes(bytes: &[u8]) -> String { render_array(bytes.iter().map(|byte| format!("0x{:x}", byte))) } +fn render_uuid(value: &[u8; 16]) -> String { + format!( + "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + value[0], + value[1], + value[2], + value[3], + value[4], + value[5], + value[6], + value[7], + value[8], + value[9], + value[10], + value[11], + value[12], + value[13], + value[14], + value[15], + ) +} + +fn bytes_to_uuid(field_name: &'static str, bytes: Vec) -> Result<[u8; 16], FieldValueError> { + bytes + .try_into() + .map_err(|_| invalid_value(field_name, "value must be exactly 16 bytes")) +} + +fn encode_uuid_full_box_header( + field_name: &'static str, + version: u8, + flags: u32, +) -> Result<[u8; 4], FieldValueError> { + if flags & 0xff00_0000 != 0 { + return Err(invalid_value(field_name, "flags exceed 24 bits")); + } + + Ok([ + version, + ((flags >> 16) & 0xff) as u8, + ((flags >> 8) & 0xff) as u8, + (flags & 0xff) as u8, + ]) +} + +fn render_uuid_fragment_run_entries(entries: &[UuidFragmentRunEntry]) -> String { + render_array(entries.iter().map(|entry| { + format!( + "{{FragmentAbsoluteTime={} FragmentAbsoluteDuration={}}}", + entry.fragment_absolute_time, entry.fragment_absolute_duration + ) + })) +} + +fn encode_uuid_fragment_absolute_timing( + field_name: &'static str, + timing: &UuidFragmentAbsoluteTiming, +) -> Result, FieldValueError> { + let mut payload = Vec::with_capacity( + if timing.version == 1 { + 20_usize + } else { + 12_usize + } + .max(4), + ); + payload.extend_from_slice(&encode_uuid_full_box_header( + field_name, + timing.version, + timing.flags, + )?); + match timing.version { + 0 => { + payload.extend_from_slice( + &u32::try_from(timing.fragment_absolute_time) + .map_err(|_| invalid_value(field_name, "version 0 time does not fit in u32"))? + .to_be_bytes(), + ); + payload.extend_from_slice( + &u32::try_from(timing.fragment_absolute_duration) + .map_err(|_| { + invalid_value(field_name, "version 0 duration does not fit in u32") + })? + .to_be_bytes(), + ); + } + 1 => { + payload.extend_from_slice(&timing.fragment_absolute_time.to_be_bytes()); + payload.extend_from_slice(&timing.fragment_absolute_duration.to_be_bytes()); + } + _ => { + return Err(invalid_value( + field_name, + "fragment timing payload version is not supported", + )); + } + } + Ok(payload) +} + +fn decode_uuid_fragment_absolute_timing( + field_name: &'static str, + payload: &[u8], +) -> Result { + if payload.len() < 4 { + return Err(invalid_value( + field_name, + "fragment timing payload is truncated", + )); + } + + let version = payload[0]; + let flags = u32::from_be_bytes([0, payload[1], payload[2], payload[3]]); + match version { + 0 => { + if payload.len() != 12 { + return Err(invalid_value( + field_name, + "fragment timing payload length does not match version 0", + )); + } + Ok(UuidFragmentAbsoluteTiming { + version, + flags, + fragment_absolute_time: u64::from(read_u32(payload, 4)), + fragment_absolute_duration: u64::from(read_u32(payload, 8)), + }) + } + 1 => { + if payload.len() != 20 { + return Err(invalid_value( + field_name, + "fragment timing payload length does not match version 1", + )); + } + Ok(UuidFragmentAbsoluteTiming { + version, + flags, + fragment_absolute_time: read_u64(payload, 4), + fragment_absolute_duration: read_u64(payload, 12), + }) + } + _ => Err(invalid_value( + field_name, + "fragment timing payload version is not supported", + )), + } +} + +fn encode_uuid_fragment_run_entries( + field_name: &'static str, + table: &UuidFragmentRunTable, +) -> Result, FieldValueError> { + if usize::from(table.fragment_count) != table.entries.len() { + return Err(invalid_value( + field_name, + "fragment count does not match the number of entries", + )); + } + + let mut payload = Vec::new(); + for entry in &table.entries { + match table.version { + 0 => { + payload.extend_from_slice( + &u32::try_from(entry.fragment_absolute_time) + .map_err(|_| { + invalid_value(field_name, "version 0 time does not fit in u32") + })? + .to_be_bytes(), + ); + payload.extend_from_slice( + &u32::try_from(entry.fragment_absolute_duration) + .map_err(|_| { + invalid_value(field_name, "version 0 duration does not fit in u32") + })? + .to_be_bytes(), + ); + } + 1 => { + payload.extend_from_slice(&entry.fragment_absolute_time.to_be_bytes()); + payload.extend_from_slice(&entry.fragment_absolute_duration.to_be_bytes()); + } + _ => { + return Err(invalid_value( + field_name, + "fragment run table payload version is not supported", + )); + } + } + } + + Ok(payload) +} + +fn encode_uuid_fragment_run_table( + field_name: &'static str, + table: &UuidFragmentRunTable, +) -> Result, FieldValueError> { + let mut payload = Vec::new(); + payload.extend_from_slice(&encode_uuid_full_box_header( + field_name, + table.version, + table.flags, + )?); + payload.push(table.fragment_count); + payload.extend_from_slice(&encode_uuid_fragment_run_entries(field_name, table)?); + Ok(payload) +} + +fn decode_uuid_fragment_run_entries( + field_name: &'static str, + version: u8, + fragment_count: u8, + payload: &[u8], +) -> Result, FieldValueError> { + let bytes_per_entry = match version { + 0 => 8_usize, + 1 => 16_usize, + _ => { + return Err(invalid_value( + field_name, + "fragment run table payload version is not supported", + )); + } + }; + let expected_len = usize::from(fragment_count) + .checked_mul(bytes_per_entry) + .ok_or_else(|| invalid_value(field_name, "fragment run table payload is too large"))?; + if payload.len() != expected_len { + return Err(invalid_value( + field_name, + "fragment run table payload length does not match the fragment count", + )); + } + + let mut entries = Vec::with_capacity(untrusted_prealloc_hint(usize::from(fragment_count))); + let mut offset = 0_usize; + while offset < payload.len() { + let (fragment_absolute_time, fragment_absolute_duration) = match version { + 0 => ( + u64::from(read_u32(payload, offset)), + u64::from(read_u32(payload, offset + 4)), + ), + 1 => (read_u64(payload, offset), read_u64(payload, offset + 8)), + _ => unreachable!(), + }; + entries.push(UuidFragmentRunEntry { + fragment_absolute_time, + fragment_absolute_duration, + }); + offset += bytes_per_entry; + } + Ok(entries) +} + +fn decode_uuid_fragment_run_table( + field_name: &'static str, + payload: &[u8], +) -> Result { + if payload.len() < 5 { + return Err(invalid_value( + field_name, + "fragment run table payload is truncated", + )); + } + + let version = payload[0]; + let flags = u32::from_be_bytes([0, payload[1], payload[2], payload[3]]); + let fragment_count = payload[4]; + let entries = + decode_uuid_fragment_run_entries(field_name, version, fragment_count, &payload[5..])?; + Ok(UuidFragmentRunTable { + version, + flags, + fragment_count, + entries, + }) +} + +fn encode_uuid_payload( + user_type: [u8; 16], + payload: &UuidPayload, +) -> Result, FieldValueError> { + match payload { + UuidPayload::Raw(bytes) => Ok(bytes.clone()), + UuidPayload::SphericalVideoV1(data) => { + if user_type != UUID_SPHERICAL_VIDEO_V1 { + return Err(invalid_value( + "Payload", + "spherical payload requires the spherical UUID user type", + )); + } + Ok(data.xml_data.clone()) + } + UuidPayload::FragmentAbsoluteTiming(data) => { + if user_type != UUID_FRAGMENT_ABSOLUTE_TIMING { + return Err(invalid_value( + "Payload", + "fragment timing payload requires the fragment-timing UUID user type", + )); + } + encode_uuid_fragment_absolute_timing("Payload", data) + } + UuidPayload::FragmentRunTable(data) => { + if user_type != UUID_FRAGMENT_RUN_TABLE { + return Err(invalid_value( + "Payload", + "fragment run table payload requires the fragment-run UUID user type", + )); + } + encode_uuid_fragment_run_table("Payload", data) + } + UuidPayload::SampleEncryption(data) => { + if user_type != UUID_SAMPLE_ENCRYPTION { + return Err(invalid_value( + "Payload", + "sample encryption payload requires the sample-encryption UUID user type", + )); + } + encode_senc_payload(data).map_err(|error| match error { + CodecError::FieldValue(field_error) => field_error, + CodecError::UnsupportedVersion { .. } => invalid_value( + "Payload", + "sample encryption payload version is not supported", + ), + CodecError::InvalidLength { .. } => invalid_value( + "Payload", + "sample count does not match the number of sample records", + ), + _ => invalid_value("Payload", "sample encryption payload is invalid"), + }) + } + } +} + +fn decode_uuid_payload( + user_type: [u8; 16], + payload: &[u8], +) -> Result { + if user_type == UUID_SPHERICAL_VIDEO_V1 { + return Ok(UuidPayload::SphericalVideoV1(SphericalVideoV1Metadata { + xml_data: payload.to_vec(), + })); + } + if user_type == UUID_FRAGMENT_ABSOLUTE_TIMING { + return Ok(UuidPayload::FragmentAbsoluteTiming( + decode_uuid_fragment_absolute_timing("Payload", payload)?, + )); + } + if user_type == UUID_FRAGMENT_RUN_TABLE { + return Ok(UuidPayload::FragmentRunTable( + decode_uuid_fragment_run_table("Payload", payload)?, + )); + } + if user_type == UUID_SAMPLE_ENCRYPTION { + return Ok(UuidPayload::SampleEncryption( + decode_senc_payload(payload).map_err(|error| match error { + CodecError::FieldValue(field_error) => field_error, + CodecError::UnsupportedVersion { .. } => invalid_value( + "Payload", + "sample encryption payload version is not supported", + ), + CodecError::InvalidLength { .. } => invalid_value( + "Payload", + "sample count does not match the number of sample records", + ), + _ => invalid_value("Payload", "sample encryption payload is invalid"), + })?, + )); + } + Ok(UuidPayload::Raw(payload.to_vec())) +} + +fn encoded_loudness_entries_len( + version: u8, + entries: &[LoudnessEntry], +) -> Result { + let bytes = encode_loudness_entries("Entries", version, entries)?; + u32::try_from(bytes.len()) + .map_err(|_| invalid_value("Entries", "encoded payload length does not fit in u32")) +} + +fn encode_loudness_entries( + field_name: &'static str, + version: u8, + entries: &[LoudnessEntry], +) -> Result, FieldValueError> { + if version > 1 { + return Err(invalid_value( + field_name, + "unsupported loudness box version", + )); + } + if version == 0 && entries.len() != 1 { + return Err(invalid_value( + field_name, + "version 0 loudness boxes must contain exactly one entry", + )); + } + if version == 1 && entries.len() > 0x3f { + return Err(invalid_value( + field_name, + "entry count does not fit in the loudness count field", + )); + } + + let mut bytes = Vec::new(); + if version >= 1 { + bytes.push(entries.len() as u8); + } + + for entry in entries { + if version >= 1 { + if entry.eq_set_id > 0x3f { + return Err(invalid_value("EQSetID", "value does not fit in 6 bits")); + } + bytes.push(entry.eq_set_id & 0x3f); + } + if entry.downmix_id > 0x03ff { + return Err(invalid_value("DownmixID", "value does not fit in 10 bits")); + } + if entry.drc_set_id > 0x3f { + return Err(invalid_value("DRCSetID", "value does not fit in 6 bits")); + } + if entry.bs_sample_peak_level > 0x0fff { + return Err(invalid_value( + "BsSamplePeakLevel", + "value does not fit in 12 bits", + )); + } + if entry.bs_true_peak_level > 0x0fff { + return Err(invalid_value( + "BsTruePeakLevel", + "value does not fit in 12 bits", + )); + } + if entry.measurement_system_for_tp > 0x0f { + return Err(invalid_value( + "MeasurementSystemForTP", + "value does not fit in 4 bits", + )); + } + if entry.reliability_for_tp > 0x0f { + return Err(invalid_value( + "ReliabilityForTP", + "value does not fit in 4 bits", + )); + } + if entry.measurements.len() > usize::from(u8::MAX) { + return Err(invalid_value( + "Measurements", + "entry count does not fit in u8", + )); + } + + let downmix_and_drc = (entry.downmix_id << 6) | u16::from(entry.drc_set_id & 0x3f); + bytes.extend_from_slice(&downmix_and_drc.to_be_bytes()); + + let peak_levels = (u32::from(entry.bs_sample_peak_level) << 12) + | u32::from(entry.bs_true_peak_level & 0x0fff); + push_uint("PeakLevels", &mut bytes, 3, u64::from(peak_levels))?; + bytes.push((entry.measurement_system_for_tp << 4) | (entry.reliability_for_tp & 0x0f)); + bytes.push(entry.measurements.len() as u8); + + for measurement in &entry.measurements { + if measurement.measurement_system > 0x0f { + return Err(invalid_value( + "MeasurementSystem", + "value does not fit in 4 bits", + )); + } + if measurement.reliability > 0x0f { + return Err(invalid_value("Reliability", "value does not fit in 4 bits")); + } + + bytes.push(measurement.method_definition); + bytes.push(measurement.method_value); + bytes.push((measurement.measurement_system << 4) | (measurement.reliability & 0x0f)); + } + } + + Ok(bytes) +} + +fn decode_loudness_entries( + field_name: &'static str, + version: u8, + payload: &[u8], +) -> Result, FieldValueError> { + if version > 1 { + return Err(invalid_value( + field_name, + "unsupported loudness box version", + )); + } + + let mut offset = 0_usize; + let entry_count = if version >= 1 { + if payload.is_empty() { + return Err(invalid_value(field_name, "payload is truncated")); + } + let info_type = payload[0] >> 6; + if info_type != 0 { + return Err(invalid_value( + field_name, + "loudness info type is not supported", + )); + } + offset += 1; + usize::from(payload[0] & 0x3f) + } else { + 1 + }; + + let mut entries = Vec::with_capacity(untrusted_prealloc_hint(entry_count)); + for _ in 0..entry_count { + let eq_set_id = if version >= 1 { + if offset >= payload.len() { + return Err(invalid_value(field_name, "payload is truncated")); + } + let value = payload[offset] & 0x3f; + offset += 1; + value + } else { + 0 + }; + + if payload.len().saturating_sub(offset) < 7 { + return Err(invalid_value(field_name, "payload is truncated")); + } + + let downmix_and_drc = read_u16(payload, offset); + offset += 2; + let peak_levels = read_uint(payload, offset, 3) as u32; + offset += 3; + let measurement_system_and_reliability_for_tp = payload[offset]; + offset += 1; + let measurement_count = usize::from(payload[offset]); + offset += 1; + + let mut measurements = Vec::with_capacity(untrusted_prealloc_hint(measurement_count)); + for _ in 0..measurement_count { + if payload.len().saturating_sub(offset) < 3 { + return Err(invalid_value(field_name, "payload is truncated")); + } + let method_definition = payload[offset]; + let method_value = payload[offset + 1]; + let measurement_system_and_reliability = payload[offset + 2]; + offset += 3; + + measurements.push(LoudnessMeasurement { + method_definition, + method_value, + measurement_system: measurement_system_and_reliability >> 4, + reliability: measurement_system_and_reliability & 0x0f, + }); + } + + entries.push(LoudnessEntry { + eq_set_id, + downmix_id: downmix_and_drc >> 6, + drc_set_id: (downmix_and_drc & 0x3f) as u8, + bs_sample_peak_level: ((peak_levels >> 12) & 0x0fff) as u16, + bs_true_peak_level: (peak_levels & 0x0fff) as u16, + measurement_system_for_tp: measurement_system_and_reliability_for_tp >> 4, + reliability_for_tp: measurement_system_and_reliability_for_tp & 0x0f, + measurements, + }); + } + + if offset != payload.len() { + return Err(invalid_value(field_name, "payload has trailing bytes")); + } + + Ok(entries) +} + +fn render_loudness_measurements(measurements: &[LoudnessMeasurement]) -> String { + render_array(measurements.iter().map(|measurement| { + format!( + "{{MethodDefinition={} MethodValue={} MeasurementSystem={} Reliability={}}}", + measurement.method_definition, + measurement.method_value, + measurement.measurement_system, + measurement.reliability, + ) + })) +} + +fn render_loudness_entries(version: u8, entries: &[LoudnessEntry]) -> String { + render_array(entries.iter().map(|entry| { + let mut fields = Vec::new(); + if version >= 1 { + fields.push(format!("EQSetID={}", entry.eq_set_id)); + } + fields.push(format!("DownmixID={}", entry.downmix_id)); + fields.push(format!("DRCSetID={}", entry.drc_set_id)); + fields.push(format!("BsSamplePeakLevel={}", entry.bs_sample_peak_level)); + fields.push(format!("BsTruePeakLevel={}", entry.bs_true_peak_level)); + fields.push(format!( + "MeasurementSystemForTP={}", + entry.measurement_system_for_tp + )); + fields.push(format!("ReliabilityForTP={}", entry.reliability_for_tp)); + fields.push(format!( + "Measurements={}", + render_loudness_measurements(&entry.measurements) + )); + format!("{{{}}}", fields.join(" ")) + })) +} + fn quoted_fourcc(value: FourCc) -> String { format!("\"{value}\"") } @@ -616,7 +1283,35 @@ macro_rules! empty_box_codec { }; } -macro_rules! simple_container_box { +macro_rules! empty_full_box_codec { + ($name:ident) => { + impl FieldValueRead for $name { + fn field_value(&self, field_name: &'static str) -> Result { + Err(missing_field(field_name)) + } + } + + impl FieldValueWrite for $name { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + Err(unexpected_field(field_name, value)) + } + } + + impl CodecBox for $name { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; + } + }; +} + +macro_rules! simple_container_box { ($name:ident, $box_type:expr) => { #[doc = "Container box with no direct payload fields."] #[derive(Clone, Debug, Default, PartialEq, Eq)] @@ -669,6 +1364,72 @@ macro_rules! raw_data_box { }; } +macro_rules! track_id_list_box { + ($name:ident, $box_type:expr, $doc:literal) => { + #[doc = $doc] + #[derive(Clone, Debug, Default, PartialEq, Eq)] + pub struct $name { + pub track_ids: Vec, + } + + impl FieldHooks for $name { + fn field_length(&self, name: &'static str) -> Option { + match name { + "TrackIDs" => field_len_bytes(self.track_ids.len(), 4), + _ => None, + } + } + + fn display_field(&self, name: &'static str) -> Option { + match name { + "TrackIDs" => Some(render_array( + self.track_ids.iter().map(|track_id| track_id.to_string()), + )), + _ => None, + } + } + } + + impl ImmutableBox for $name { + fn box_type(&self) -> FourCc { + FourCc::from_bytes($box_type) + } + } + + impl MutableBox for $name {} + + impl FieldValueRead for $name { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "TrackIDs" => Ok(FieldValue::Bytes(track_id_vec_to_bytes(&self.track_ids))), + _ => Err(missing_field(field_name)), + } + } + } + + impl FieldValueWrite for $name { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("TrackIDs", FieldValue::Bytes(bytes)) => { + self.track_ids = bytes_to_track_id_vec(field_name, bytes)?; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } + } + + impl CodecBox for $name { + const FIELD_TABLE: FieldTable = + FieldTable::new(&[codec_field!("TrackIDs", 0, with_bit_width(8), as_bytes())]); + } + }; +} + simple_container_box!(Dinf, *b"dinf"); simple_container_box!(Edts, *b"edts"); simple_container_box!(Mdia, *b"mdia"); @@ -680,109 +1441,38 @@ simple_container_box!(Mfra, *b"mfra"); simple_container_box!(Stbl, *b"stbl"); simple_container_box!(Traf, *b"traf"); simple_container_box!(Trak, *b"trak"); -simple_container_box!(Udta, *b"udta"); +simple_container_box!(Tref, *b"tref"); raw_data_box!(Free, *b"free"); raw_data_box!(Skip, *b"skip"); raw_data_box!(Mdat, *b"mdat"); -/// File type and compatibility declaration box. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Ftyp { - pub major_brand: FourCc, - pub minor_version: u32, - pub compatible_brands: Vec, -} - -impl Default for Ftyp { - fn default() -> Self { - Self { - major_brand: FourCc::ANY, - minor_version: 0, - compatible_brands: Vec::new(), - } - } -} - -impl Ftyp { - /// Adds `brand` if it is not already listed as compatible. - pub fn add_compatible_brand(&mut self, brand: FourCc) { - if !self.has_compatible_brand(brand) { - self.compatible_brands.push(brand); - } - } - - /// Removes `brand` from the compatibility list. - pub fn remove_compatible_brand(&mut self, brand: FourCc) { - self.compatible_brands - .retain(|candidate| *candidate != brand); - } - - /// Returns `true` when `brand` is present in the compatibility list. - pub fn has_compatible_brand(&self, brand: FourCc) -> bool { - self.compatible_brands.contains(&brand) - } -} - -impl FieldHooks for Ftyp { - fn field_length(&self, name: &'static str) -> Option { - match name { - "CompatibleBrands" => field_len_bytes(self.compatible_brands.len(), 4), - _ => None, - } - } - - fn display_field(&self, name: &'static str) -> Option { - match name { - "MajorBrand" => Some(quoted_fourcc(self.major_brand)), - "CompatibleBrands" => { - Some(render_array(self.compatible_brands.iter().map(|brand| { - format!("{{CompatibleBrand={}}}", quoted_fourcc(*brand)) - }))) - } - _ => None, - } - } -} - -impl ImmutableBox for Ftyp { - fn box_type(&self) -> FourCc { - FourCc::from_bytes(*b"ftyp") - } +/// Closed-caption sample-data box that preserves its payload bytes verbatim. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Cdat { + pub data: Vec, } -impl MutableBox for Ftyp {} +impl_leaf_box!(Cdat, *b"cdat"); -impl FieldValueRead for Ftyp { +impl FieldValueRead for Cdat { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "MajorBrand" => Ok(FieldValue::Bytes(self.major_brand.as_bytes().to_vec())), - "MinorVersion" => Ok(FieldValue::Unsigned(u64::from(self.minor_version))), - "CompatibleBrands" => Ok(FieldValue::Bytes(fourcc_vec_to_bytes( - &self.compatible_brands, - ))), + "Data" => Ok(FieldValue::Bytes(self.data.clone())), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Ftyp { +impl FieldValueWrite for Cdat { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("MajorBrand", FieldValue::Bytes(bytes)) => { - self.major_brand = bytes_to_fourcc(field_name, bytes)?; - Ok(()) - } - ("MinorVersion", FieldValue::Unsigned(value)) => { - self.minor_version = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("CompatibleBrands", FieldValue::Bytes(bytes)) => { - self.compatible_brands = bytes_to_fourcc_vec(field_name, bytes)?; + ("Data", FieldValue::Bytes(data)) => { + self.data = data; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -790,247 +1480,494 @@ impl FieldValueWrite for Ftyp { } } -impl CodecBox for Ftyp { - const FIELD_TABLE: FieldTable = FieldTable::new(&[ - codec_field!( - "MajorBrand", - 0, - with_bit_width(8), - with_length(4), - as_bytes() - ), - codec_field!("MinorVersion", 1, with_bit_width(32)), - codec_field!("CompatibleBrands", 2, with_bit_width(8), as_bytes()), - ]); +impl CodecBox for Cdat { + const FIELD_TABLE: FieldTable = + FieldTable::new(&[codec_field!("Data", 0, with_bit_width(8), as_bytes())]); } -/// Segment type and compatibility declaration box. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Styp { - pub major_brand: FourCc, - pub minor_version: u32, - pub compatible_brands: Vec, -} +/// User-data container carried by boxes such as `moov` and `trak`. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Udta; -impl Default for Styp { - fn default() -> Self { - Self { - major_brand: FourCc::ANY, - minor_version: 0, - compatible_brands: Vec::new(), - } - } -} +impl_leaf_box!(Udta, *b"udta"); +empty_box_codec!(Udta); -impl FieldHooks for Styp { - fn field_length(&self, name: &'static str) -> Option { - match name { - "CompatibleBrands" => field_len_bytes(self.compatible_brands.len(), 4), - _ => None, - } - } +/// User-data loudness container that groups track and album loudness boxes. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Ludt; - fn display_field(&self, name: &'static str) -> Option { - match name { - "MajorBrand" => Some(quoted_fourcc(self.major_brand)), - "CompatibleBrands" => { - Some(render_array(self.compatible_brands.iter().map(|brand| { - format!("{{CompatibleBrand={}}}", quoted_fourcc(*brand)) - }))) - } - _ => None, - } - } -} +impl_leaf_box!(Ludt, *b"ludt"); +empty_box_codec!(Ludt); -impl ImmutableBox for Styp { - fn box_type(&self) -> FourCc { - FourCc::from_bytes(*b"styp") - } +/// One loudness measurement record carried by `tlou` and `alou`. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct LoudnessMeasurement { + pub method_definition: u8, + pub method_value: u8, + pub measurement_system: u8, + pub reliability: u8, } -impl MutableBox for Styp {} +/// One loudness entry carried by `tlou` and `alou`. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct LoudnessEntry { + pub eq_set_id: u8, + pub downmix_id: u16, + pub drc_set_id: u8, + pub bs_sample_peak_level: u16, + pub bs_true_peak_level: u16, + pub measurement_system_for_tp: u8, + pub reliability_for_tp: u8, + pub measurements: Vec, +} + +macro_rules! define_loudness_info_box { + ($(#[$doc:meta])* $name:ident, $box_type:expr) => { + $(#[$doc])* + #[derive(Clone, Debug, Default, PartialEq, Eq)] + pub struct $name { + full_box: FullBoxState, + pub entries: Vec, + } -impl FieldValueRead for Styp { - fn field_value(&self, field_name: &'static str) -> Result { - match field_name { - "MajorBrand" => Ok(FieldValue::Bytes(self.major_brand.as_bytes().to_vec())), - "MinorVersion" => Ok(FieldValue::Unsigned(u64::from(self.minor_version))), - "CompatibleBrands" => Ok(FieldValue::Bytes(fourcc_vec_to_bytes( - &self.compatible_brands, - ))), - _ => Err(missing_field(field_name)), + impl FieldHooks for $name { + fn field_length(&self, name: &'static str) -> Option { + match name { + "Entries" => encoded_loudness_entries_len(self.version(), &self.entries).ok(), + _ => None, + } + } + + fn display_field(&self, name: &'static str) -> Option { + match name { + "Entries" => Some(render_loudness_entries(self.version(), &self.entries)), + _ => None, + } + } } - } -} -impl FieldValueWrite for Styp { - fn set_field_value( - &mut self, - field_name: &'static str, - value: FieldValue, - ) -> Result<(), FieldValueError> { - match (field_name, value) { - ("MajorBrand", FieldValue::Bytes(bytes)) => { - self.major_brand = bytes_to_fourcc(field_name, bytes)?; - Ok(()) + impl ImmutableBox for $name { + fn box_type(&self) -> FourCc { + FourCc::from_bytes($box_type) } - ("MinorVersion", FieldValue::Unsigned(value)) => { - self.minor_version = u32_from_unsigned(field_name, value)?; - Ok(()) + + fn version(&self) -> u8 { + self.full_box.version } - ("CompatibleBrands", FieldValue::Bytes(bytes)) => { - self.compatible_brands = bytes_to_fourcc_vec(field_name, bytes)?; - Ok(()) + + fn flags(&self) -> u32 { + self.full_box.flags } - (field_name, value) => Err(unexpected_field(field_name, value)), } - } -} -impl CodecBox for Styp { - const FIELD_TABLE: FieldTable = FieldTable::new(&[ - codec_field!( - "MajorBrand", - 0, - with_bit_width(8), - with_length(4), - as_bytes() - ), - codec_field!("MinorVersion", 1, with_bit_width(32)), - codec_field!("CompatibleBrands", 2, with_bit_width(8), as_bytes()), - ]); + impl MutableBox for $name { + fn set_version(&mut self, version: u8) { + self.full_box.version = version; + } + + fn set_flags(&mut self, flags: u32) { + self.full_box.flags = flags; + } + } + + impl FieldValueRead for $name { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "Entries" => Ok(FieldValue::Bytes(encode_loudness_entries( + field_name, + self.version(), + &self.entries, + )?)), + _ => Err(missing_field(field_name)), + } + } + } + + impl FieldValueWrite for $name { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("Entries", FieldValue::Bytes(value)) => { + self.entries = decode_loudness_entries(field_name, self.version(), &value)?; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } + } + + impl CodecBox for $name { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("Entries", 2, with_bit_width(8), with_dynamic_length(), as_bytes()), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; + + fn custom_marshal( + &self, + writer: &mut dyn Write, + ) -> Result, CodecError> { + if self.version() > 1 { + return Err(CodecError::UnsupportedVersion { + box_type: self.box_type(), + version: self.version(), + }); + } + if self.flags() != 0 { + return Err(invalid_value("Flags", "non-zero flags are not supported").into()); + } + + let entries = encode_loudness_entries("Entries", self.version(), &self.entries)?; + let mut payload = Vec::with_capacity(4 + entries.len()); + payload.push(self.version()); + payload.extend_from_slice(&self.flags().to_be_bytes()[1..]); + payload.extend_from_slice(&entries); + writer.write_all(&payload)?; + Ok(Some(payload.len() as u64)) + } + + fn custom_unmarshal( + &mut self, + reader: &mut dyn ReadSeek, + payload_size: u64, + ) -> Result, CodecError> { + let payload_len = usize::try_from(payload_size) + .map_err(|_| invalid_value("Payload", "payload is too large to decode"))?; + if payload_len < 4 { + return Err(invalid_value("Payload", "payload is too short").into()); + } + + let payload = read_exact_vec_untrusted(reader, payload_len)?; + let version = payload[0]; + if version > 1 { + return Err(CodecError::UnsupportedVersion { + box_type: self.box_type(), + version, + }); + } + let flags = u32::from_be_bytes([0, payload[1], payload[2], payload[3]]); + if flags != 0 { + return Err(invalid_value("Flags", "non-zero flags are not supported").into()); + } + + self.full_box = FullBoxState { version, flags }; + self.entries = decode_loudness_entries("Entries", version, &payload[4..])?; + Ok(Some(payload_size)) + } + } + }; } -empty_hooks!( - Dref, Url, Urn, Mfhd, Mfro, Mehd, Mdhd, Tfdt, Tfhd, Trep, Trex, Vmhd, Stsd, Cslg +define_loudness_info_box!( + /// Track loudness metadata box carried under `ludt`. + TrackLoudnessInfo, + *b"tlou" ); -/// Data reference box that counts child data-entry boxes. +define_loudness_info_box!( + /// Album loudness metadata box carried under `ludt`. + AlbumLoudnessInfo, + *b"alou" +); + +/// Spherical-video metadata payload stored inside one `uuid` box subtype. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Dref { - full_box: FullBoxState, - pub entry_count: u32, +pub struct SphericalVideoV1Metadata { + pub xml_data: Vec, } -impl_full_box!(Dref, *b"dref"); +/// Fragment-absolute-timing payload stored inside one `uuid` box subtype. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct UuidFragmentAbsoluteTiming { + pub version: u8, + pub flags: u32, + pub fragment_absolute_time: u64, + pub fragment_absolute_duration: u64, +} -impl FieldValueRead for Dref { - fn field_value(&self, field_name: &'static str) -> Result { - match field_name { - "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), - _ => Err(missing_field(field_name)), - } - } +/// One fragment timing record carried by a fragment-run table `uuid` payload. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct UuidFragmentRunEntry { + pub fragment_absolute_time: u64, + pub fragment_absolute_duration: u64, } -impl FieldValueWrite for Dref { - fn set_field_value( - &mut self, - field_name: &'static str, - value: FieldValue, - ) -> Result<(), FieldValueError> { - match (field_name, value) { - ("EntryCount", FieldValue::Unsigned(value)) => { - self.entry_count = u32_from_unsigned(field_name, value)?; - Ok(()) - } - (field_name, value) => Err(unexpected_field(field_name, value)), - } - } +/// Fragment-run table payload stored inside one `uuid` box subtype. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct UuidFragmentRunTable { + pub version: u8, + pub flags: u32, + pub fragment_count: u8, + pub entries: Vec, } -impl CodecBox for Dref { - const FIELD_TABLE: FieldTable = FieldTable::new(&[ - codec_field!("Version", 0, with_bit_width(8), as_version_field()), - codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("EntryCount", 2, with_bit_width(32)), - ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0]; +/// Typed payload variants for `uuid` boxes. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum UuidPayload { + Raw(Vec), + SphericalVideoV1(SphericalVideoV1Metadata), + FragmentAbsoluteTiming(UuidFragmentAbsoluteTiming), + FragmentRunTable(UuidFragmentRunTable), + SampleEncryption(Senc), } -/// URL data-entry box. +impl Default for UuidPayload { + fn default() -> Self { + Self::Raw(Vec::new()) + } +} + +/// User-type box that keeps unknown payloads opaque while modeling selected UUID subtypes. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Url { - full_box: FullBoxState, - pub location: String, +pub struct Uuid { + pub user_type: [u8; 16], + pub payload: UuidPayload, } -impl_full_box!(Url, *b"url "); +impl FieldHooks for Uuid { + fn field_length(&self, name: &'static str) -> Option { + match name { + "RawPayload" => match &self.payload { + UuidPayload::Raw(bytes) => u32::try_from(bytes.len()).ok(), + _ => None, + }, + "XMLData" => match &self.payload { + UuidPayload::SphericalVideoV1(data) => u32::try_from(data.xml_data.len()).ok(), + _ => None, + }, + "Entries" => match &self.payload { + UuidPayload::FragmentRunTable(data) => { + u32::try_from(encode_uuid_fragment_run_entries(name, data).ok()?.len()).ok() + } + _ => None, + }, + "Samples" => match &self.payload { + UuidPayload::SampleEncryption(data) => { + let payload = encode_senc_payload(data).ok()?; + u32::try_from(payload.len().saturating_sub(8)).ok() + } + _ => None, + }, + _ => None, + } + } -impl FieldValueRead for Url { - fn field_value(&self, field_name: &'static str) -> Result { - match field_name { - "Location" => Ok(FieldValue::String(self.location.clone())), - _ => Err(missing_field(field_name)), + fn field_enabled(&self, name: &'static str) -> Option { + match name { + "RawPayload" => Some(matches!(self.payload, UuidPayload::Raw(_))), + "XMLData" => Some(matches!(self.payload, UuidPayload::SphericalVideoV1(_))), + "Version" | "Flags" => Some(matches!( + self.payload, + UuidPayload::FragmentAbsoluteTiming(_) + | UuidPayload::FragmentRunTable(_) + | UuidPayload::SampleEncryption(_) + )), + "FragmentAbsoluteTime" | "FragmentAbsoluteDuration" => Some(matches!( + self.payload, + UuidPayload::FragmentAbsoluteTiming(_) + )), + "FragmentCount" | "Entries" => { + Some(matches!(self.payload, UuidPayload::FragmentRunTable(_))) + } + "SampleCount" | "Samples" => { + Some(matches!(self.payload, UuidPayload::SampleEncryption(_))) + } + _ => None, } } -} -impl FieldValueWrite for Url { - fn set_field_value( - &mut self, - field_name: &'static str, - value: FieldValue, - ) -> Result<(), FieldValueError> { - match (field_name, value) { - ("Location", FieldValue::String(value)) => { - self.location = value; - Ok(()) + fn display_field(&self, name: &'static str) -> Option { + match (name, &self.payload) { + ("XMLData", UuidPayload::SphericalVideoV1(data)) => Some(quote_bytes(&data.xml_data)), + ("Entries", UuidPayload::FragmentRunTable(data)) => { + Some(render_uuid_fragment_run_entries(&data.entries)) } - (field_name, value) => Err(unexpected_field(field_name, value)), + ("Samples", UuidPayload::SampleEncryption(data)) => { + Some(render_senc_samples_display(&data.samples)) + } + _ => None, } } } -impl CodecBox for Url { - const FIELD_TABLE: FieldTable = FieldTable::new(&[ - codec_field!("Version", 0, with_bit_width(8), as_version_field()), - codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!( - "Location", - 2, - with_bit_width(8), - as_string(StringFieldMode::NullTerminated), - with_forbidden_flags(URL_SELF_CONTAINED) - ), - ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0]; -} +impl ImmutableBox for Uuid { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"uuid") + } -/// URN data-entry box. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Urn { - full_box: FullBoxState, - pub name: String, - pub location: String, + fn version(&self) -> u8 { + match &self.payload { + UuidPayload::FragmentAbsoluteTiming(data) => data.version, + UuidPayload::FragmentRunTable(data) => data.version, + UuidPayload::SampleEncryption(data) => data.version(), + _ => ANY_VERSION, + } + } + + fn flags(&self) -> u32 { + match &self.payload { + UuidPayload::FragmentAbsoluteTiming(data) => data.flags, + UuidPayload::FragmentRunTable(data) => data.flags, + UuidPayload::SampleEncryption(data) => data.flags(), + _ => 0, + } + } } -impl_full_box!(Urn, *b"urn "); +impl MutableBox for Uuid { + fn set_version(&mut self, version: u8) { + match &mut self.payload { + UuidPayload::FragmentAbsoluteTiming(data) => data.version = version, + UuidPayload::FragmentRunTable(data) => data.version = version, + UuidPayload::SampleEncryption(data) => data.set_version(version), + _ => {} + } + } -impl FieldValueRead for Urn { + fn set_flags(&mut self, flags: u32) { + match &mut self.payload { + UuidPayload::FragmentAbsoluteTiming(data) => data.flags = flags, + UuidPayload::FragmentRunTable(data) => data.flags = flags, + UuidPayload::SampleEncryption(data) => data.set_flags(flags), + _ => {} + } + } +} + +impl FieldValueRead for Uuid { fn field_value(&self, field_name: &'static str) -> Result { - match field_name { - "Name" => Ok(FieldValue::String(self.name.clone())), - "Location" => Ok(FieldValue::String(self.location.clone())), + match (field_name, &self.payload) { + ("UserType", _) => Ok(FieldValue::Bytes(self.user_type.to_vec())), + ("RawPayload", UuidPayload::Raw(bytes)) => Ok(FieldValue::Bytes(bytes.clone())), + ("XMLData", UuidPayload::SphericalVideoV1(data)) => { + Ok(FieldValue::Bytes(data.xml_data.clone())) + } + ("FragmentAbsoluteTime", UuidPayload::FragmentAbsoluteTiming(data)) => { + Ok(FieldValue::Unsigned(data.fragment_absolute_time)) + } + ("FragmentAbsoluteDuration", UuidPayload::FragmentAbsoluteTiming(data)) => { + Ok(FieldValue::Unsigned(data.fragment_absolute_duration)) + } + ("FragmentCount", UuidPayload::FragmentRunTable(data)) => { + Ok(FieldValue::Unsigned(u64::from(data.fragment_count))) + } + ("Entries", UuidPayload::FragmentRunTable(data)) => Ok(FieldValue::Bytes( + encode_uuid_fragment_run_entries(field_name, data)?, + )), + ("SampleCount", UuidPayload::SampleEncryption(data)) => { + Ok(FieldValue::Unsigned(u64::from(data.sample_count))) + } + ("Samples", UuidPayload::SampleEncryption(data)) => Ok(FieldValue::Bytes( + encode_senc_payload(data).map_err(|_| { + invalid_value(field_name, "sample encryption payload is invalid") + })?[8..] + .to_vec(), + )), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Urn { +impl FieldValueWrite for Uuid { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("Name", FieldValue::String(value)) => { - self.name = value; + ("UserType", FieldValue::Bytes(value)) => { + let payload_bytes = encode_uuid_payload(self.user_type, &self.payload)?; + self.user_type = bytes_to_uuid(field_name, value)?; + self.payload = if payload_bytes.is_empty() { + match self.user_type { + UUID_SPHERICAL_VIDEO_V1 => { + UuidPayload::SphericalVideoV1(SphericalVideoV1Metadata::default()) + } + UUID_FRAGMENT_ABSOLUTE_TIMING => UuidPayload::FragmentAbsoluteTiming( + UuidFragmentAbsoluteTiming::default(), + ), + UUID_FRAGMENT_RUN_TABLE => { + UuidPayload::FragmentRunTable(UuidFragmentRunTable::default()) + } + UUID_SAMPLE_ENCRYPTION => UuidPayload::SampleEncryption(Senc::default()), + _ => UuidPayload::Raw(Vec::new()), + } + } else { + decode_uuid_payload(self.user_type, &payload_bytes)? + }; Ok(()) } - ("Location", FieldValue::String(value)) => { - self.location = value; + ("RawPayload", FieldValue::Bytes(value)) => { + self.payload = UuidPayload::Raw(value); + Ok(()) + } + ("XMLData", FieldValue::Bytes(value)) => { + if self.user_type != UUID_SPHERICAL_VIDEO_V1 { + return Err(invalid_value( + field_name, + "field requires the spherical UUID user type", + )); + } + self.payload = + UuidPayload::SphericalVideoV1(SphericalVideoV1Metadata { xml_data: value }); + Ok(()) + } + ("FragmentAbsoluteTime", FieldValue::Unsigned(value)) => { + let UuidPayload::FragmentAbsoluteTiming(data) = &mut self.payload else { + return Err(missing_field(field_name)); + }; + data.fragment_absolute_time = value; + Ok(()) + } + ("FragmentAbsoluteDuration", FieldValue::Unsigned(value)) => { + let UuidPayload::FragmentAbsoluteTiming(data) = &mut self.payload else { + return Err(missing_field(field_name)); + }; + data.fragment_absolute_duration = value; + Ok(()) + } + ("FragmentCount", FieldValue::Unsigned(value)) => { + let UuidPayload::FragmentRunTable(data) = &mut self.payload else { + return Err(missing_field(field_name)); + }; + data.fragment_count = u8_from_unsigned(field_name, value)?; + Ok(()) + } + ("Entries", FieldValue::Bytes(value)) => { + let UuidPayload::FragmentRunTable(data) = &mut self.payload else { + return Err(missing_field(field_name)); + }; + data.entries = decode_uuid_fragment_run_entries( + field_name, + data.version, + data.fragment_count, + &value, + )?; + Ok(()) + } + ("SampleCount", FieldValue::Unsigned(value)) => { + let UuidPayload::SampleEncryption(data) = &mut self.payload else { + return Err(missing_field(field_name)); + }; + data.sample_count = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("Samples", FieldValue::Bytes(value)) => { + let UuidPayload::SampleEncryption(data) = &mut self.payload else { + return Err(missing_field(field_name)); + }; + let mut payload = Vec::with_capacity(8 + value.len()); + payload.push(data.version()); + payload.extend_from_slice(&(data.flags() & 0x00ff_ffff).to_be_bytes()[1..]); + payload.extend_from_slice(&data.sample_count.to_be_bytes()); + payload.extend_from_slice(&value); + *data = decode_senc_payload(&payload).map_err(|_| { + invalid_value(field_name, "sample encryption payload is invalid") + })?; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -1038,158 +1975,212 @@ impl FieldValueWrite for Urn { } } -impl CodecBox for Urn { +impl CodecBox for Uuid { const FIELD_TABLE: FieldTable = FieldTable::new(&[ - codec_field!("Version", 0, with_bit_width(8), as_version_field()), - codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), codec_field!( - "Name", - 2, + "UserType", + 0, with_bit_width(8), - as_string(StringFieldMode::NullTerminated), - with_forbidden_flags(URN_SELF_CONTAINED) + with_length(16), + as_bytes(), + as_uuid() ), codec_field!( - "Location", - 3, + "RawPayload", + 1, with_bit_width(8), - as_string(StringFieldMode::NullTerminated), - with_forbidden_flags(URN_SELF_CONTAINED) - ), + with_dynamic_length(), + as_bytes(), + with_dynamic_presence() + ), + codec_field!( + "XMLData", + 2, + with_bit_width(8), + with_dynamic_length(), + as_bytes(), + with_dynamic_presence() + ), + codec_field!( + "Version", + 3, + with_bit_width(8), + as_version_field(), + with_dynamic_presence() + ), + codec_field!( + "Flags", + 4, + with_bit_width(24), + as_flags_field(), + with_dynamic_presence() + ), + codec_field!( + "FragmentAbsoluteTime", + 5, + with_bit_width(64), + with_dynamic_presence() + ), + codec_field!( + "FragmentAbsoluteDuration", + 6, + with_bit_width(64), + with_dynamic_presence() + ), + codec_field!( + "FragmentCount", + 7, + with_bit_width(8), + with_dynamic_presence() + ), + codec_field!( + "Entries", + 8, + with_bit_width(8), + with_dynamic_length(), + as_bytes(), + with_dynamic_presence() + ), + codec_field!( + "SampleCount", + 9, + with_bit_width(32), + with_dynamic_presence() + ), + codec_field!( + "Samples", + 10, + with_bit_width(8), + with_dynamic_length(), + as_bytes(), + with_dynamic_presence() + ), ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0]; -} - -/// Movie fragment header box. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Mfhd { - full_box: FullBoxState, - pub sequence_number: u32, -} - -impl_full_box!(Mfhd, *b"mfhd"); -impl FieldValueRead for Mfhd { - fn field_value(&self, field_name: &'static str) -> Result { - match field_name { - "SequenceNumber" => Ok(FieldValue::Unsigned(u64::from(self.sequence_number))), - _ => Err(missing_field(field_name)), - } + fn custom_marshal(&self, writer: &mut dyn Write) -> Result, CodecError> { + let payload_bytes = encode_uuid_payload(self.user_type, &self.payload)?; + let mut payload = Vec::with_capacity(16 + payload_bytes.len()); + payload.extend_from_slice(&self.user_type); + payload.extend_from_slice(&payload_bytes); + writer.write_all(&payload)?; + Ok(Some(payload.len() as u64)) } -} -impl FieldValueWrite for Mfhd { - fn set_field_value( + fn custom_unmarshal( &mut self, - field_name: &'static str, - value: FieldValue, - ) -> Result<(), FieldValueError> { - match (field_name, value) { - ("SequenceNumber", FieldValue::Unsigned(value)) => { - self.sequence_number = u32_from_unsigned(field_name, value)?; - Ok(()) - } - (field_name, value) => Err(unexpected_field(field_name, value)), + reader: &mut dyn ReadSeek, + payload_size: u64, + ) -> Result, CodecError> { + let payload_len = usize::try_from(payload_size) + .map_err(|_| invalid_value("Payload", "payload is too large to decode"))?; + if payload_len < 16 { + return Err(invalid_value("Payload", "payload is too short").into()); } - } -} -impl CodecBox for Mfhd { - const FIELD_TABLE: FieldTable = FieldTable::new(&[ - codec_field!("Version", 0, with_bit_width(8), as_version_field()), - codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("SequenceNumber", 2, with_bit_width(32)), - ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0]; + let payload = read_exact_vec_untrusted(reader, payload_len)?; + self.user_type = payload[..16].try_into().unwrap(); + self.payload = decode_uuid_payload(self.user_type, &payload[16..])?; + Ok(Some(payload_size)) + } } -/// Movie fragment random access offset box. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Mfro { - full_box: FullBoxState, - pub size: u32, +/// File type and compatibility declaration box. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Ftyp { + pub major_brand: FourCc, + pub minor_version: u32, + pub compatible_brands: Vec, } -impl_full_box!(Mfro, *b"mfro"); - -impl FieldValueRead for Mfro { - fn field_value(&self, field_name: &'static str) -> Result { - match field_name { - "Size" => Ok(FieldValue::Unsigned(u64::from(self.size))), - _ => Err(missing_field(field_name)), +impl Default for Ftyp { + fn default() -> Self { + Self { + major_brand: FourCc::ANY, + minor_version: 0, + compatible_brands: Vec::new(), } } } -impl FieldValueWrite for Mfro { - fn set_field_value( - &mut self, - field_name: &'static str, - value: FieldValue, - ) -> Result<(), FieldValueError> { - match (field_name, value) { - ("Size", FieldValue::Unsigned(value)) => { - self.size = u32_from_unsigned(field_name, value)?; - Ok(()) - } - (field_name, value) => Err(unexpected_field(field_name, value)), +impl Ftyp { + /// Adds `brand` if it is not already listed as compatible. + pub fn add_compatible_brand(&mut self, brand: FourCc) { + if !self.has_compatible_brand(brand) { + self.compatible_brands.push(brand); } } -} -impl CodecBox for Mfro { - const FIELD_TABLE: FieldTable = FieldTable::new(&[ - codec_field!("Version", 0, with_bit_width(8), as_version_field()), - codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("Size", 2, with_bit_width(32)), - ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0]; -} + /// Removes `brand` from the compatibility list. + pub fn remove_compatible_brand(&mut self, brand: FourCc) { + self.compatible_brands + .retain(|candidate| *candidate != brand); + } -/// Movie extends header box. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Mehd { - full_box: FullBoxState, - pub fragment_duration_v0: u32, - pub fragment_duration_v1: u64, + /// Returns `true` when `brand` is present in the compatibility list. + pub fn has_compatible_brand(&self, brand: FourCc) -> bool { + self.compatible_brands.contains(&brand) + } } -impl_full_box!(Mehd, *b"mehd"); +impl FieldHooks for Ftyp { + fn field_length(&self, name: &'static str) -> Option { + match name { + "CompatibleBrands" => field_len_bytes(self.compatible_brands.len(), 4), + _ => None, + } + } -impl Mehd { - /// Returns the active fragment duration for the current box version. - pub fn fragment_duration(&self) -> u64 { - match self.version() { - 0 => u64::from(self.fragment_duration_v0), - 1 => self.fragment_duration_v1, - _ => 0, + fn display_field(&self, name: &'static str) -> Option { + match name { + "MajorBrand" => Some(quoted_fourcc(self.major_brand)), + "CompatibleBrands" => { + Some(render_array(self.compatible_brands.iter().map(|brand| { + format!("{{CompatibleBrand={}}}", quoted_fourcc(*brand)) + }))) + } + _ => None, } } } -impl FieldValueRead for Mehd { +impl ImmutableBox for Ftyp { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"ftyp") + } +} + +impl MutableBox for Ftyp {} + +impl FieldValueRead for Ftyp { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "FragmentDurationV0" => Ok(FieldValue::Unsigned(u64::from(self.fragment_duration_v0))), - "FragmentDurationV1" => Ok(FieldValue::Unsigned(self.fragment_duration_v1)), + "MajorBrand" => Ok(FieldValue::Bytes(self.major_brand.as_bytes().to_vec())), + "MinorVersion" => Ok(FieldValue::Unsigned(u64::from(self.minor_version))), + "CompatibleBrands" => Ok(FieldValue::Bytes(fourcc_vec_to_bytes( + &self.compatible_brands, + ))), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Mehd { +impl FieldValueWrite for Ftyp { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("FragmentDurationV0", FieldValue::Unsigned(value)) => { - self.fragment_duration_v0 = u32_from_unsigned(field_name, value)?; + ("MajorBrand", FieldValue::Bytes(bytes)) => { + self.major_brand = bytes_to_fourcc(field_name, bytes)?; Ok(()) } - ("FragmentDurationV1", FieldValue::Unsigned(value)) => { - self.fragment_duration_v1 = value; + ("MinorVersion", FieldValue::Unsigned(value)) => { + self.minor_version = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("CompatibleBrands", FieldValue::Bytes(bytes)) => { + self.compatible_brands = bytes_to_fourcc_vec(field_name, bytes)?; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -1197,138 +2188,97 @@ impl FieldValueWrite for Mehd { } } -impl CodecBox for Mehd { +impl CodecBox for Ftyp { const FIELD_TABLE: FieldTable = FieldTable::new(&[ - codec_field!("Version", 0, with_bit_width(8), as_version_field()), - codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("FragmentDurationV0", 2, with_bit_width(32), with_version(0)), - codec_field!("FragmentDurationV1", 3, with_bit_width(64), with_version(1)), + codec_field!( + "MajorBrand", + 0, + with_bit_width(8), + with_length(4), + as_bytes() + ), + codec_field!("MinorVersion", 1, with_bit_width(32)), + codec_field!("CompatibleBrands", 2, with_bit_width(8), as_bytes()), ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; } -/// Media header box. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Mdhd { - full_box: FullBoxState, - pub creation_time_v0: u32, - pub modification_time_v0: u32, - pub creation_time_v1: u64, - pub modification_time_v1: u64, - pub timescale: u32, - pub duration_v0: u32, - pub duration_v1: u64, - pub pad: bool, - pub language: [u8; 3], - pub pre_defined: u16, +/// Segment type and compatibility declaration box. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Styp { + pub major_brand: FourCc, + pub minor_version: u32, + pub compatible_brands: Vec, } -impl_full_box!(Mdhd, *b"mdhd"); - -impl Mdhd { - /// Returns the active media creation time for the current box version. - pub fn creation_time(&self) -> u64 { - match self.version() { - 0 => u64::from(self.creation_time_v0), - 1 => self.creation_time_v1, - _ => 0, +impl Default for Styp { + fn default() -> Self { + Self { + major_brand: FourCc::ANY, + minor_version: 0, + compatible_brands: Vec::new(), } } +} - /// Returns the active media modification time for the current box version. - pub fn modification_time(&self) -> u64 { - match self.version() { - 0 => u64::from(self.modification_time_v0), - 1 => self.modification_time_v1, - _ => 0, +impl FieldHooks for Styp { + fn field_length(&self, name: &'static str) -> Option { + match name { + "CompatibleBrands" => field_len_bytes(self.compatible_brands.len(), 4), + _ => None, } } - /// Returns the active media duration for the current box version. - pub fn duration(&self) -> u64 { - match self.version() { - 0 => u64::from(self.duration_v0), - 1 => self.duration_v1, - _ => 0, + fn display_field(&self, name: &'static str) -> Option { + match name { + "MajorBrand" => Some(quoted_fourcc(self.major_brand)), + "CompatibleBrands" => { + Some(render_array(self.compatible_brands.iter().map(|brand| { + format!("{{CompatibleBrand={}}}", quoted_fourcc(*brand)) + }))) + } + _ => None, } } } -impl FieldValueRead for Mdhd { +impl ImmutableBox for Styp { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"styp") + } +} + +impl MutableBox for Styp {} + +impl FieldValueRead for Styp { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "CreationTimeV0" => Ok(FieldValue::Unsigned(u64::from(self.creation_time_v0))), - "ModificationTimeV0" => Ok(FieldValue::Unsigned(u64::from(self.modification_time_v0))), - "CreationTimeV1" => Ok(FieldValue::Unsigned(self.creation_time_v1)), - "ModificationTimeV1" => Ok(FieldValue::Unsigned(self.modification_time_v1)), - "Timescale" => Ok(FieldValue::Unsigned(u64::from(self.timescale))), - "DurationV0" => Ok(FieldValue::Unsigned(u64::from(self.duration_v0))), - "DurationV1" => Ok(FieldValue::Unsigned(self.duration_v1)), - "Pad" => Ok(FieldValue::Boolean(self.pad)), - "Language" => Ok(FieldValue::UnsignedArray( - self.language.iter().copied().map(u64::from).collect(), - )), - "PreDefined" => Ok(FieldValue::Unsigned(u64::from(self.pre_defined))), + "MajorBrand" => Ok(FieldValue::Bytes(self.major_brand.as_bytes().to_vec())), + "MinorVersion" => Ok(FieldValue::Unsigned(u64::from(self.minor_version))), + "CompatibleBrands" => Ok(FieldValue::Bytes(fourcc_vec_to_bytes( + &self.compatible_brands, + ))), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Mdhd { +impl FieldValueWrite for Styp { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("CreationTimeV0", FieldValue::Unsigned(value)) => { - self.creation_time_v0 = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("ModificationTimeV0", FieldValue::Unsigned(value)) => { - self.modification_time_v0 = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("CreationTimeV1", FieldValue::Unsigned(value)) => { - self.creation_time_v1 = value; - Ok(()) - } - ("ModificationTimeV1", FieldValue::Unsigned(value)) => { - self.modification_time_v1 = value; - Ok(()) - } - ("Timescale", FieldValue::Unsigned(value)) => { - self.timescale = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("DurationV0", FieldValue::Unsigned(value)) => { - self.duration_v0 = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("DurationV1", FieldValue::Unsigned(value)) => { - self.duration_v1 = value; - Ok(()) - } - ("Pad", FieldValue::Boolean(value)) => { - self.pad = value; + ("MajorBrand", FieldValue::Bytes(bytes)) => { + self.major_brand = bytes_to_fourcc(field_name, bytes)?; Ok(()) } - ("Language", FieldValue::UnsignedArray(values)) => { - if values.len() != 3 { - return Err(invalid_value( - field_name, - "value must contain exactly 3 elements", - )); - } - self.language = [ - u8_from_unsigned(field_name, values[0])?, - u8_from_unsigned(field_name, values[1])?, - u8_from_unsigned(field_name, values[2])?, - ]; + ("MinorVersion", FieldValue::Unsigned(value)) => { + self.minor_version = u32_from_unsigned(field_name, value)?; Ok(()) } - ("PreDefined", FieldValue::Unsigned(value)) => { - self.pre_defined = u16_from_unsigned(field_name, value)?; + ("CompatibleBrands", FieldValue::Bytes(bytes)) => { + self.compatible_brands = bytes_to_fourcc_vec(field_name, bytes)?; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -1336,193 +2286,51 @@ impl FieldValueWrite for Mdhd { } } -impl CodecBox for Mdhd { +impl CodecBox for Styp { const FIELD_TABLE: FieldTable = FieldTable::new(&[ - codec_field!("Version", 0, with_bit_width(8), as_version_field()), - codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("CreationTimeV0", 2, with_bit_width(32), with_version(0)), - codec_field!("ModificationTimeV0", 3, with_bit_width(32), with_version(0)), - codec_field!("CreationTimeV1", 4, with_bit_width(64), with_version(1)), - codec_field!("ModificationTimeV1", 5, with_bit_width(64), with_version(1)), - codec_field!("Timescale", 6, with_bit_width(32)), - codec_field!("DurationV0", 7, with_bit_width(32), with_version(0)), - codec_field!("DurationV1", 8, with_bit_width(64), with_version(1)), - codec_field!("Pad", 9, with_bit_width(1), as_boolean(), as_hidden()), codec_field!( - "Language", - 10, - with_bit_width(5), - with_length(3), - as_iso639_2() + "MajorBrand", + 0, + with_bit_width(8), + with_length(4), + as_bytes() ), - codec_field!("PreDefined", 11, with_bit_width(16)), + codec_field!("MinorVersion", 1, with_bit_width(32)), + codec_field!("CompatibleBrands", 2, with_bit_width(8), as_bytes()), ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; } -/// Movie header box. +empty_hooks!( + Dref, Url, Urn, Mfhd, Mfro, Prft, Mehd, Mdhd, Tfdt, Tfhd, Trep, Trex, Vmhd, Stsd, Cslg +); + +/// Data reference box that counts child data-entry boxes. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Mvhd { +pub struct Dref { full_box: FullBoxState, - pub creation_time_v0: u32, - pub modification_time_v0: u32, - pub creation_time_v1: u64, - pub modification_time_v1: u64, - pub timescale: u32, - pub duration_v0: u32, - pub duration_v1: u64, - pub rate: i32, - pub volume: i16, - pub matrix: [i32; 9], - pub pre_defined: [i32; 6], - pub next_track_id: u32, -} - -impl_full_box!(Mvhd, *b"mvhd"); - -impl FieldHooks for Mvhd { - fn display_field(&self, name: &'static str) -> Option { - match name { - "Rate" => Some(format_fixed_16_16_signed(self.rate)), - _ => None, - } - } + pub entry_count: u32, } -impl Mvhd { - /// Returns the active movie creation time for the current box version. - pub fn creation_time(&self) -> u64 { - match self.version() { - 0 => u64::from(self.creation_time_v0), - 1 => self.creation_time_v1, - _ => 0, - } - } - - /// Returns the active movie modification time for the current box version. - pub fn modification_time(&self) -> u64 { - match self.version() { - 0 => u64::from(self.modification_time_v0), - 1 => self.modification_time_v1, - _ => 0, - } - } - - /// Returns the active movie duration for the current box version. - pub fn duration(&self) -> u64 { - match self.version() { - 0 => u64::from(self.duration_v0), - 1 => self.duration_v1, - _ => 0, - } - } - - /// Returns the playback rate as a signed 16.16 fixed-point value. - pub fn rate_value(&self) -> f64 { - f64::from(self.rate) / 65536.0 - } - - /// Returns the integer component of the playback rate. - pub fn rate_int(&self) -> i16 { - (self.rate >> 16) as i16 - } -} +impl_full_box!(Dref, *b"dref"); -impl FieldValueRead for Mvhd { +impl FieldValueRead for Dref { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "CreationTimeV0" => Ok(FieldValue::Unsigned(u64::from(self.creation_time_v0))), - "ModificationTimeV0" => Ok(FieldValue::Unsigned(u64::from(self.modification_time_v0))), - "CreationTimeV1" => Ok(FieldValue::Unsigned(self.creation_time_v1)), - "ModificationTimeV1" => Ok(FieldValue::Unsigned(self.modification_time_v1)), - "Timescale" => Ok(FieldValue::Unsigned(u64::from(self.timescale))), - "DurationV0" => Ok(FieldValue::Unsigned(u64::from(self.duration_v0))), - "DurationV1" => Ok(FieldValue::Unsigned(self.duration_v1)), - "Rate" => Ok(FieldValue::Signed(i64::from(self.rate))), - "Volume" => Ok(FieldValue::Signed(i64::from(self.volume))), - "Reserved2" => Ok(FieldValue::Bytes(vec![0; 8])), - "Matrix" => Ok(FieldValue::SignedArray( - self.matrix.iter().copied().map(i64::from).collect(), - )), - "PreDefined" => Ok(FieldValue::SignedArray( - self.pre_defined.iter().copied().map(i64::from).collect(), - )), - "NextTrackID" => Ok(FieldValue::Unsigned(u64::from(self.next_track_id))), + "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Mvhd { +impl FieldValueWrite for Dref { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("CreationTimeV0", FieldValue::Unsigned(value)) => { - self.creation_time_v0 = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("ModificationTimeV0", FieldValue::Unsigned(value)) => { - self.modification_time_v0 = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("CreationTimeV1", FieldValue::Unsigned(value)) => { - self.creation_time_v1 = value; - Ok(()) - } - ("ModificationTimeV1", FieldValue::Unsigned(value)) => { - self.modification_time_v1 = value; - Ok(()) - } - ("Timescale", FieldValue::Unsigned(value)) => { - self.timescale = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("DurationV0", FieldValue::Unsigned(value)) => { - self.duration_v0 = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("DurationV1", FieldValue::Unsigned(value)) => { - self.duration_v1 = value; - Ok(()) - } - ("Rate", FieldValue::Signed(value)) => { - self.rate = i32_from_signed(field_name, value)?; - Ok(()) - } - ("Volume", FieldValue::Signed(value)) => { - self.volume = i16_from_signed(field_name, value)?; - Ok(()) - } - ("Reserved2", FieldValue::Bytes(bytes)) => bytes_to_zeroes(field_name, &bytes, 8), - ("Matrix", FieldValue::SignedArray(values)) => { - if values.len() != 9 { - return Err(invalid_value( - field_name, - "value must contain exactly 9 elements", - )); - } - for (slot, value) in self.matrix.iter_mut().zip(values) { - *slot = i32_from_signed(field_name, value)?; - } - Ok(()) - } - ("PreDefined", FieldValue::SignedArray(values)) => { - if values.len() != 6 { - return Err(invalid_value( - field_name, - "value must contain exactly 6 elements", - )); - } - for (slot, value) in self.pre_defined.iter_mut().zip(values) { - *slot = i32_from_signed(field_name, value)?; - } - Ok(()) - } - ("NextTrackID", FieldValue::Unsigned(value)) => { - self.next_track_id = u32_from_unsigned(field_name, value)?; + ("EntryCount", FieldValue::Unsigned(value)) => { + self.entry_count = u32_from_unsigned(field_name, value)?; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -1530,94 +2338,42 @@ impl FieldValueWrite for Mvhd { } } -impl CodecBox for Mvhd { +impl CodecBox for Dref { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("CreationTimeV0", 2, with_bit_width(32), with_version(0)), - codec_field!("ModificationTimeV0", 3, with_bit_width(32), with_version(0)), - codec_field!("CreationTimeV1", 4, with_bit_width(64), with_version(1)), - codec_field!("ModificationTimeV1", 5, with_bit_width(64), with_version(1)), - codec_field!("Timescale", 6, with_bit_width(32)), - codec_field!("DurationV0", 7, with_bit_width(32), with_version(0)), - codec_field!("DurationV1", 8, with_bit_width(64), with_version(1)), - codec_field!("Rate", 9, with_bit_width(32), as_signed()), - codec_field!("Volume", 10, with_bit_width(16), as_signed()), - codec_field!("Reserved", 11, with_bit_width(16), with_constant("0")), - codec_field!( - "Reserved2", - 12, - with_bit_width(8), - with_length(8), - as_bytes(), - as_hidden() - ), - codec_field!( - "Matrix", - 13, - with_bit_width(32), - with_length(9), - as_signed(), - as_hex() - ), - codec_field!( - "PreDefined", - 14, - with_bit_width(32), - with_length(6), - as_signed() - ), - codec_field!("NextTrackID", 15, with_bit_width(32)), + codec_field!("EntryCount", 2, with_bit_width(32)), ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; } -/// Track fragment decode time box. +/// URL data-entry box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Tfdt { +pub struct Url { full_box: FullBoxState, - pub base_media_decode_time_v0: u32, - pub base_media_decode_time_v1: u64, + pub location: String, } -impl_full_box!(Tfdt, *b"tfdt"); - -impl Tfdt { - /// Returns the active base media decode time for the current box version. - pub fn base_media_decode_time(&self) -> u64 { - match self.version() { - 0 => u64::from(self.base_media_decode_time_v0), - 1 => self.base_media_decode_time_v1, - _ => 0, - } - } -} +impl_full_box!(Url, *b"url "); -impl FieldValueRead for Tfdt { +impl FieldValueRead for Url { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "BaseMediaDecodeTimeV0" => Ok(FieldValue::Unsigned(u64::from( - self.base_media_decode_time_v0, - ))), - "BaseMediaDecodeTimeV1" => Ok(FieldValue::Unsigned(self.base_media_decode_time_v1)), + "Location" => Ok(FieldValue::String(self.location.clone())), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Tfdt { +impl FieldValueWrite for Url { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("BaseMediaDecodeTimeV0", FieldValue::Unsigned(value)) => { - self.base_media_decode_time_v0 = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("BaseMediaDecodeTimeV1", FieldValue::Unsigned(value)) => { - self.base_media_decode_time_v1 = value; + ("Location", FieldValue::String(value)) => { + self.location = value; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -1625,87 +2381,54 @@ impl FieldValueWrite for Tfdt { } } -impl CodecBox for Tfdt { +impl CodecBox for Url { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), codec_field!( - "BaseMediaDecodeTimeV0", + "Location", 2, - with_bit_width(32), - with_version(0) - ), - codec_field!( - "BaseMediaDecodeTimeV1", - 3, - with_bit_width(64), - with_version(1) + with_bit_width(8), + as_string(StringFieldMode::NullTerminated), + with_forbidden_flags(URL_SELF_CONTAINED) ), ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; } -/// Track fragment header box. +/// URN data-entry box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Tfhd { +pub struct Urn { full_box: FullBoxState, - pub track_id: u32, - pub base_data_offset: u64, - pub sample_description_index: u32, - pub default_sample_duration: u32, - pub default_sample_size: u32, - pub default_sample_flags: u32, + pub name: String, + pub location: String, } -impl_full_box!(Tfhd, *b"tfhd"); +impl_full_box!(Urn, *b"urn "); -impl FieldValueRead for Tfhd { +impl FieldValueRead for Urn { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "TrackID" => Ok(FieldValue::Unsigned(u64::from(self.track_id))), - "BaseDataOffset" => Ok(FieldValue::Unsigned(self.base_data_offset)), - "SampleDescriptionIndex" => Ok(FieldValue::Unsigned(u64::from( - self.sample_description_index, - ))), - "DefaultSampleDuration" => Ok(FieldValue::Unsigned(u64::from( - self.default_sample_duration, - ))), - "DefaultSampleSize" => Ok(FieldValue::Unsigned(u64::from(self.default_sample_size))), - "DefaultSampleFlags" => Ok(FieldValue::Unsigned(u64::from(self.default_sample_flags))), + "Name" => Ok(FieldValue::String(self.name.clone())), + "Location" => Ok(FieldValue::String(self.location.clone())), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Tfhd { +impl FieldValueWrite for Urn { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("TrackID", FieldValue::Unsigned(value)) => { - self.track_id = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("BaseDataOffset", FieldValue::Unsigned(value)) => { - self.base_data_offset = value; - Ok(()) - } - ("SampleDescriptionIndex", FieldValue::Unsigned(value)) => { - self.sample_description_index = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("DefaultSampleDuration", FieldValue::Unsigned(value)) => { - self.default_sample_duration = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("DefaultSampleSize", FieldValue::Unsigned(value)) => { - self.default_sample_size = u32_from_unsigned(field_name, value)?; + ("Name", FieldValue::String(value)) => { + self.name = value; Ok(()) } - ("DefaultSampleFlags", FieldValue::Unsigned(value)) => { - self.default_sample_flags = u32_from_unsigned(field_name, value)?; + ("Location", FieldValue::String(value)) => { + self.location = value; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -1713,238 +2436,218 @@ impl FieldValueWrite for Tfhd { } } -impl CodecBox for Tfhd { +impl CodecBox for Urn { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("TrackID", 2, with_bit_width(32)), - codec_field!( - "BaseDataOffset", - 3, - with_bit_width(64), - with_required_flags(TFHD_BASE_DATA_OFFSET_PRESENT) - ), - codec_field!( - "SampleDescriptionIndex", - 4, - with_bit_width(32), - with_required_flags(TFHD_SAMPLE_DESCRIPTION_INDEX_PRESENT) - ), - codec_field!( - "DefaultSampleDuration", - 5, - with_bit_width(32), - with_required_flags(TFHD_DEFAULT_SAMPLE_DURATION_PRESENT) - ), codec_field!( - "DefaultSampleSize", - 6, - with_bit_width(32), - with_required_flags(TFHD_DEFAULT_SAMPLE_SIZE_PRESENT) + "Name", + 2, + with_bit_width(8), + as_string(StringFieldMode::NullTerminated), + with_forbidden_flags(URN_SELF_CONTAINED) ), codec_field!( - "DefaultSampleFlags", - 7, - with_bit_width(32), - with_required_flags(TFHD_DEFAULT_SAMPLE_FLAGS_PRESENT), - as_hex() + "Location", + 3, + with_bit_width(8), + as_string(StringFieldMode::NullTerminated), + with_forbidden_flags(URN_SELF_CONTAINED) ), ]); const SUPPORTED_VERSIONS: &'static [u8] = &[0]; } -/// Track header box. +/// Movie fragment header box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Tkhd { +pub struct Mfhd { full_box: FullBoxState, - pub creation_time_v0: u32, - pub modification_time_v0: u32, - pub creation_time_v1: u64, - pub modification_time_v1: u64, - pub track_id: u32, - pub duration_v0: u32, - pub duration_v1: u64, - pub layer: i16, - pub alternate_group: i16, - pub volume: i16, - pub matrix: [i32; 9], - pub width: u32, - pub height: u32, + pub sequence_number: u32, } -impl FieldHooks for Tkhd { - fn display_field(&self, name: &'static str) -> Option { - match name { - "Width" => Some(format_fixed_16_16_unsigned(self.width)), - "Height" => Some(format_fixed_16_16_unsigned(self.height)), - _ => None, +impl_full_box!(Mfhd, *b"mfhd"); + +impl FieldValueRead for Mfhd { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "SequenceNumber" => Ok(FieldValue::Unsigned(u64::from(self.sequence_number))), + _ => Err(missing_field(field_name)), } } } -impl ImmutableBox for Tkhd { - fn box_type(&self) -> FourCc { - FourCc::from_bytes(*b"tkhd") +impl FieldValueWrite for Mfhd { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("SequenceNumber", FieldValue::Unsigned(value)) => { + self.sequence_number = u32_from_unsigned(field_name, value)?; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } } +} - fn version(&self) -> u8 { - self.full_box.version - } +impl CodecBox for Mfhd { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("SequenceNumber", 2, with_bit_width(32)), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; +} - fn flags(&self) -> u32 { - self.full_box.flags - } +/// Movie fragment random access offset box. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Mfro { + full_box: FullBoxState, + pub size: u32, } -impl MutableBox for Tkhd { - fn set_version(&mut self, version: u8) { - self.full_box.version = version; - } +impl_full_box!(Mfro, *b"mfro"); - fn set_flags(&mut self, flags: u32) { - self.full_box.flags = flags; +impl FieldValueRead for Mfro { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "Size" => Ok(FieldValue::Unsigned(u64::from(self.size))), + _ => Err(missing_field(field_name)), + } } } -impl Tkhd { - /// Returns the active track creation time for the current box version. - pub fn creation_time(&self) -> u64 { - match self.version() { - 0 => u64::from(self.creation_time_v0), - 1 => self.creation_time_v1, - _ => 0, +impl FieldValueWrite for Mfro { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("Size", FieldValue::Unsigned(value)) => { + self.size = u32_from_unsigned(field_name, value)?; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), } } +} - /// Returns the active track modification time for the current box version. - pub fn modification_time(&self) -> u64 { +impl CodecBox for Mfro { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("Size", 2, with_bit_width(32)), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; +} + +/// Producer reference time box. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Prft { + full_box: FullBoxState, + pub reference_track_id: u32, + pub ntp_timestamp: u64, + pub media_time_v0: u32, + pub media_time_v1: u64, +} + +impl_full_box!(Prft, *b"prft"); + +impl Prft { + /// Returns the active media time for the current box version. + pub fn media_time(&self) -> u64 { match self.version() { - 0 => u64::from(self.modification_time_v0), - 1 => self.modification_time_v1, + 0 => u64::from(self.media_time_v0), + 1 => self.media_time_v1, _ => 0, } } - /// Returns the active track duration for the current box version. - pub fn duration(&self) -> u64 { - match self.version() { - 0 => u64::from(self.duration_v0), - 1 => self.duration_v1, - _ => 0, - } + /// Returns the whole-second component of the stored NTP timestamp. + pub fn ntp_seconds(&self) -> u32 { + (self.ntp_timestamp >> 32) as u32 } - /// Returns the track width as an unsigned 16.16 fixed-point value. - pub fn width_value(&self) -> f64 { - f64::from(self.width) / 65536.0 + /// Returns the fractional component of the stored NTP timestamp. + pub fn ntp_fraction(&self) -> u32 { + self.ntp_timestamp as u32 } - /// Returns the integer component of the track width. - pub fn width_int(&self) -> u16 { - (self.width >> 16) as u16 + /// Returns the fractional NTP component converted to nanoseconds. + pub fn ntp_fraction_nanos(&self) -> u32 { + ((u128::from(self.ntp_fraction()) * NANOS_PER_SECOND) / PRFT_NTP_FRACTION_SCALE) as u32 } - /// Returns the track height as an unsigned 16.16 fixed-point value. - pub fn height_value(&self) -> f64 { - f64::from(self.height) / 65536.0 + /// Returns the whole-second UNIX-epoch component represented by the NTP timestamp. + /// + /// Returns `None` when the stored timestamp predates `1970-01-01T00:00:00Z`. + pub fn unix_seconds(&self) -> Option { + u64::from(self.ntp_seconds()).checked_sub(PRFT_NTP_UNIX_EPOCH_OFFSET_SECONDS) } - /// Returns the integer component of the track height. - pub fn height_int(&self) -> u16 { - (self.height >> 16) as u16 + /// Returns the stored NTP timestamp as a UNIX `SystemTime`. + /// + /// Returns `None` when the stored timestamp predates `1970-01-01T00:00:00Z`. + pub fn unix_time(&self) -> Option { + let seconds = self.unix_seconds()?; + UNIX_EPOCH.checked_add(Duration::new(seconds, self.ntp_fraction_nanos())) + } + + /// Returns the known capture-point name for the stored `prft` flags value. + pub fn flag_meaning(&self) -> Option<&'static str> { + Self::known_flag_meaning(self.flags()) + } + + /// Returns the known capture-point name for one `prft` flags value. + pub fn known_flag_meaning(flags: u32) -> Option<&'static str> { + match flags { + PRFT_TIME_ENCODER_INPUT => Some("time_encoder_input"), + PRFT_TIME_ENCODER_OUTPUT => Some("time_encoder_output"), + PRFT_TIME_MOOF_FINALIZED => Some("time_moof_finalized"), + PRFT_TIME_MOOF_WRITTEN => Some("time_moof_written"), + PRFT_TIME_ARBITRARY_CONSISTENT => Some("time_arbitrary_consistent"), + PRFT_TIME_CAPTURED => Some("time_captured"), + _ => None, + } } } -impl FieldValueRead for Tkhd { +impl FieldValueRead for Prft { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "CreationTimeV0" => Ok(FieldValue::Unsigned(u64::from(self.creation_time_v0))), - "ModificationTimeV0" => Ok(FieldValue::Unsigned(u64::from(self.modification_time_v0))), - "CreationTimeV1" => Ok(FieldValue::Unsigned(self.creation_time_v1)), - "ModificationTimeV1" => Ok(FieldValue::Unsigned(self.modification_time_v1)), - "TrackID" => Ok(FieldValue::Unsigned(u64::from(self.track_id))), - "DurationV0" => Ok(FieldValue::Unsigned(u64::from(self.duration_v0))), - "DurationV1" => Ok(FieldValue::Unsigned(self.duration_v1)), - "Reserved1" => Ok(FieldValue::Bytes(vec![0; 8])), - "Layer" => Ok(FieldValue::Signed(i64::from(self.layer))), - "AlternateGroup" => Ok(FieldValue::Signed(i64::from(self.alternate_group))), - "Volume" => Ok(FieldValue::Signed(i64::from(self.volume))), - "Matrix" => Ok(FieldValue::SignedArray( - self.matrix.iter().copied().map(i64::from).collect(), - )), - "Width" => Ok(FieldValue::Unsigned(u64::from(self.width))), - "Height" => Ok(FieldValue::Unsigned(u64::from(self.height))), + "ReferenceTrackID" => Ok(FieldValue::Unsigned(u64::from(self.reference_track_id))), + "NTPTimestamp" => Ok(FieldValue::Unsigned(self.ntp_timestamp)), + "MediaTimeV0" => Ok(FieldValue::Unsigned(u64::from(self.media_time_v0))), + "MediaTimeV1" => Ok(FieldValue::Unsigned(self.media_time_v1)), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Tkhd { +impl FieldValueWrite for Prft { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("CreationTimeV0", FieldValue::Unsigned(value)) => { - self.creation_time_v0 = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("ModificationTimeV0", FieldValue::Unsigned(value)) => { - self.modification_time_v0 = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("CreationTimeV1", FieldValue::Unsigned(value)) => { - self.creation_time_v1 = value; - Ok(()) - } - ("ModificationTimeV1", FieldValue::Unsigned(value)) => { - self.modification_time_v1 = value; - Ok(()) - } - ("TrackID", FieldValue::Unsigned(value)) => { - self.track_id = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("DurationV0", FieldValue::Unsigned(value)) => { - self.duration_v0 = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("DurationV1", FieldValue::Unsigned(value)) => { - self.duration_v1 = value; - Ok(()) - } - ("Reserved1", FieldValue::Bytes(bytes)) => bytes_to_zeroes(field_name, &bytes, 8), - ("Layer", FieldValue::Signed(value)) => { - self.layer = i16_from_signed(field_name, value)?; - Ok(()) - } - ("AlternateGroup", FieldValue::Signed(value)) => { - self.alternate_group = i16_from_signed(field_name, value)?; + ("ReferenceTrackID", FieldValue::Unsigned(value)) => { + self.reference_track_id = u32_from_unsigned(field_name, value)?; Ok(()) } - ("Volume", FieldValue::Signed(value)) => { - self.volume = i16_from_signed(field_name, value)?; - Ok(()) - } - ("Matrix", FieldValue::SignedArray(values)) => { - if values.len() != 9 { - return Err(invalid_value( - field_name, - "value must contain exactly 9 elements", - )); - } - for (slot, value) in self.matrix.iter_mut().zip(values) { - *slot = i32_from_signed(field_name, value)?; - } + ("NTPTimestamp", FieldValue::Unsigned(value)) => { + self.ntp_timestamp = value; Ok(()) } - ("Width", FieldValue::Unsigned(value)) => { - self.width = u32_from_unsigned(field_name, value)?; + ("MediaTimeV0", FieldValue::Unsigned(value)) => { + self.media_time_v0 = u32_from_unsigned(field_name, value)?; Ok(()) } - ("Height", FieldValue::Unsigned(value)) => { - self.height = u32_from_unsigned(field_name, value)?; + ("MediaTimeV1", FieldValue::Unsigned(value)) => { + self.media_time_v1 = value; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -1952,142 +2655,62 @@ impl FieldValueWrite for Tkhd { } } -impl CodecBox for Tkhd { +impl CodecBox for Prft { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("CreationTimeV0", 2, with_bit_width(32), with_version(0)), - codec_field!("ModificationTimeV0", 3, with_bit_width(32), with_version(0)), - codec_field!("CreationTimeV1", 4, with_bit_width(64), with_version(1)), - codec_field!("ModificationTimeV1", 5, with_bit_width(64), with_version(1)), - codec_field!("TrackID", 6, with_bit_width(32)), - codec_field!("Reserved0", 7, with_bit_width(32), with_constant("0")), - codec_field!("DurationV0", 8, with_bit_width(32), with_version(0)), - codec_field!("DurationV1", 9, with_bit_width(64), with_version(1)), - codec_field!( - "Reserved1", - 10, - with_bit_width(8), - with_length(8), - as_bytes(), - as_hidden() - ), - codec_field!("Layer", 11, with_bit_width(16), as_signed()), - codec_field!("AlternateGroup", 12, with_bit_width(16), as_signed()), - codec_field!("Volume", 13, with_bit_width(16), as_signed()), - codec_field!("Reserved2", 14, with_bit_width(16), with_constant("0")), - codec_field!( - "Matrix", - 15, - with_bit_width(32), - with_length(9), - as_signed(), - as_hex() - ), - codec_field!("Width", 16, with_bit_width(32)), - codec_field!("Height", 17, with_bit_width(32)), + codec_field!("ReferenceTrackID", 2, with_bit_width(32)), + codec_field!("NTPTimestamp", 3, with_bit_width(64)), + codec_field!("MediaTimeV0", 4, with_bit_width(32), with_version(0)), + codec_field!("MediaTimeV1", 5, with_bit_width(64), with_version(1)), ]); const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; } -/// Track extension properties box. +/// Movie extends header box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Trep { +pub struct Mehd { full_box: FullBoxState, - pub track_id: u32, + pub fragment_duration_v0: u32, + pub fragment_duration_v1: u64, } -impl_full_box!(Trep, *b"trep"); +impl_full_box!(Mehd, *b"mehd"); -impl FieldValueRead for Trep { - fn field_value(&self, field_name: &'static str) -> Result { - match field_name { - "TrackID" => Ok(FieldValue::Unsigned(u64::from(self.track_id))), - _ => Err(missing_field(field_name)), +impl Mehd { + /// Returns the active fragment duration for the current box version. + pub fn fragment_duration(&self) -> u64 { + match self.version() { + 0 => u64::from(self.fragment_duration_v0), + 1 => self.fragment_duration_v1, + _ => 0, } } } -impl FieldValueWrite for Trep { - fn set_field_value( - &mut self, - field_name: &'static str, - value: FieldValue, - ) -> Result<(), FieldValueError> { - match (field_name, value) { - ("TrackID", FieldValue::Unsigned(value)) => { - self.track_id = u32_from_unsigned(field_name, value)?; - Ok(()) - } - (field_name, value) => Err(unexpected_field(field_name, value)), - } - } -} - -impl CodecBox for Trep { - const FIELD_TABLE: FieldTable = FieldTable::new(&[ - codec_field!("Version", 0, with_bit_width(8), as_version_field()), - codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("TrackID", 2, with_bit_width(32)), - ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0]; -} - -/// Track extends defaults box. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Trex { - full_box: FullBoxState, - pub track_id: u32, - pub default_sample_description_index: u32, - pub default_sample_duration: u32, - pub default_sample_size: u32, - pub default_sample_flags: u32, -} - -impl_full_box!(Trex, *b"trex"); - -impl FieldValueRead for Trex { +impl FieldValueRead for Mehd { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "TrackID" => Ok(FieldValue::Unsigned(u64::from(self.track_id))), - "DefaultSampleDescriptionIndex" => Ok(FieldValue::Unsigned(u64::from( - self.default_sample_description_index, - ))), - "DefaultSampleDuration" => Ok(FieldValue::Unsigned(u64::from( - self.default_sample_duration, - ))), - "DefaultSampleSize" => Ok(FieldValue::Unsigned(u64::from(self.default_sample_size))), - "DefaultSampleFlags" => Ok(FieldValue::Unsigned(u64::from(self.default_sample_flags))), + "FragmentDurationV0" => Ok(FieldValue::Unsigned(u64::from(self.fragment_duration_v0))), + "FragmentDurationV1" => Ok(FieldValue::Unsigned(self.fragment_duration_v1)), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Trex { +impl FieldValueWrite for Mehd { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("TrackID", FieldValue::Unsigned(value)) => { - self.track_id = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("DefaultSampleDescriptionIndex", FieldValue::Unsigned(value)) => { - self.default_sample_description_index = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("DefaultSampleDuration", FieldValue::Unsigned(value)) => { - self.default_sample_duration = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("DefaultSampleSize", FieldValue::Unsigned(value)) => { - self.default_sample_size = u32_from_unsigned(field_name, value)?; + ("FragmentDurationV0", FieldValue::Unsigned(value)) => { + self.fragment_duration_v0 = u32_from_unsigned(field_name, value)?; Ok(()) } - ("DefaultSampleFlags", FieldValue::Unsigned(value)) => { - self.default_sample_flags = u32_from_unsigned(field_name, value)?; + ("FragmentDurationV1", FieldValue::Unsigned(value)) => { + self.fragment_duration_v1 = value; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -2095,100 +2718,239 @@ impl FieldValueWrite for Trex { } } -impl CodecBox for Trex { +impl CodecBox for Mehd { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("TrackID", 2, with_bit_width(32)), - codec_field!("DefaultSampleDescriptionIndex", 3, with_bit_width(32)), - codec_field!("DefaultSampleDuration", 4, with_bit_width(32)), - codec_field!("DefaultSampleSize", 5, with_bit_width(32)), - codec_field!("DefaultSampleFlags", 6, with_bit_width(32), as_hex()), + codec_field!("FragmentDurationV0", 2, with_bit_width(32), with_version(0)), + codec_field!("FragmentDurationV1", 3, with_bit_width(64), with_version(1)), ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0]; + const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; } -/// Video media header box. +/// Media header box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Vmhd { +pub struct Mdhd { full_box: FullBoxState, - pub graphicsmode: u16, - pub opcolor: [u16; 3], + pub creation_time_v0: u32, + pub modification_time_v0: u32, + pub creation_time_v1: u64, + pub modification_time_v1: u64, + pub timescale: u32, + pub duration_v0: u32, + pub duration_v1: u64, + pub pad: bool, + pub language: [u8; 3], + pub pre_defined: u16, } -impl_full_box!(Vmhd, *b"vmhd"); +impl_full_box!(Mdhd, *b"mdhd"); -impl FieldValueRead for Vmhd { +impl Mdhd { + /// Returns the active media creation time for the current box version. + pub fn creation_time(&self) -> u64 { + match self.version() { + 0 => u64::from(self.creation_time_v0), + 1 => self.creation_time_v1, + _ => 0, + } + } + + /// Returns the active media modification time for the current box version. + pub fn modification_time(&self) -> u64 { + match self.version() { + 0 => u64::from(self.modification_time_v0), + 1 => self.modification_time_v1, + _ => 0, + } + } + + /// Returns the active media duration for the current box version. + pub fn duration(&self) -> u64 { + match self.version() { + 0 => u64::from(self.duration_v0), + 1 => self.duration_v1, + _ => 0, + } + } +} + +impl FieldValueRead for Mdhd { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "Graphicsmode" => Ok(FieldValue::Unsigned(u64::from(self.graphicsmode))), - "Opcolor" => Ok(FieldValue::UnsignedArray( - self.opcolor.iter().copied().map(u64::from).collect(), + "CreationTimeV0" => Ok(FieldValue::Unsigned(u64::from(self.creation_time_v0))), + "ModificationTimeV0" => Ok(FieldValue::Unsigned(u64::from(self.modification_time_v0))), + "CreationTimeV1" => Ok(FieldValue::Unsigned(self.creation_time_v1)), + "ModificationTimeV1" => Ok(FieldValue::Unsigned(self.modification_time_v1)), + "Timescale" => Ok(FieldValue::Unsigned(u64::from(self.timescale))), + "DurationV0" => Ok(FieldValue::Unsigned(u64::from(self.duration_v0))), + "DurationV1" => Ok(FieldValue::Unsigned(self.duration_v1)), + "Pad" => Ok(FieldValue::Boolean(self.pad)), + "Language" => Ok(FieldValue::UnsignedArray( + self.language.iter().copied().map(u64::from).collect(), )), + "PreDefined" => Ok(FieldValue::Unsigned(u64::from(self.pre_defined))), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Vmhd { +impl FieldValueWrite for Mdhd { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("Graphicsmode", FieldValue::Unsigned(value)) => { - self.graphicsmode = u16_from_unsigned(field_name, value)?; + ("CreationTimeV0", FieldValue::Unsigned(value)) => { + self.creation_time_v0 = u32_from_unsigned(field_name, value)?; Ok(()) } - ("Opcolor", FieldValue::UnsignedArray(values)) => { + ("ModificationTimeV0", FieldValue::Unsigned(value)) => { + self.modification_time_v0 = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("CreationTimeV1", FieldValue::Unsigned(value)) => { + self.creation_time_v1 = value; + Ok(()) + } + ("ModificationTimeV1", FieldValue::Unsigned(value)) => { + self.modification_time_v1 = value; + Ok(()) + } + ("Timescale", FieldValue::Unsigned(value)) => { + self.timescale = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("DurationV0", FieldValue::Unsigned(value)) => { + self.duration_v0 = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("DurationV1", FieldValue::Unsigned(value)) => { + self.duration_v1 = value; + Ok(()) + } + ("Pad", FieldValue::Boolean(value)) => { + self.pad = value; + Ok(()) + } + ("Language", FieldValue::UnsignedArray(values)) => { if values.len() != 3 { return Err(invalid_value( field_name, "value must contain exactly 3 elements", )); } - self.opcolor = [ - u16_from_unsigned(field_name, values[0])?, - u16_from_unsigned(field_name, values[1])?, - u16_from_unsigned(field_name, values[2])?, + self.language = [ + u8_from_unsigned(field_name, values[0])?, + u8_from_unsigned(field_name, values[1])?, + u8_from_unsigned(field_name, values[2])?, ]; Ok(()) } + ("PreDefined", FieldValue::Unsigned(value)) => { + self.pre_defined = u16_from_unsigned(field_name, value)?; + Ok(()) + } (field_name, value) => Err(unexpected_field(field_name, value)), } } } -impl CodecBox for Vmhd { +impl CodecBox for Mdhd { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("Graphicsmode", 2, with_bit_width(16)), - codec_field!("Opcolor", 3, with_bit_width(16), with_length(3)), + codec_field!("CreationTimeV0", 2, with_bit_width(32), with_version(0)), + codec_field!("ModificationTimeV0", 3, with_bit_width(32), with_version(0)), + codec_field!("CreationTimeV1", 4, with_bit_width(64), with_version(1)), + codec_field!("ModificationTimeV1", 5, with_bit_width(64), with_version(1)), + codec_field!("Timescale", 6, with_bit_width(32)), + codec_field!("DurationV0", 7, with_bit_width(32), with_version(0)), + codec_field!("DurationV1", 8, with_bit_width(64), with_version(1)), + codec_field!("Pad", 9, with_bit_width(1), as_boolean(), as_hidden()), + codec_field!( + "Language", + 10, + with_bit_width(5), + with_length(3), + as_iso639_2() + ), + codec_field!("PreDefined", 11, with_bit_width(16)), ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0]; + const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; } -/// Sound media header box. +fn validate_c_string_value(field_name: &'static str, value: &str) -> Result<(), FieldValueError> { + if value.as_bytes().contains(&0) { + return Err(invalid_value( + field_name, + "value must not contain NUL bytes", + )); + } + Ok(()) +} + +fn decode_c_string(field_name: &'static str, bytes: &[u8]) -> Result { + let end = bytes + .iter() + .position(|byte| *byte == 0) + .unwrap_or(bytes.len()); + String::from_utf8(bytes[..end].to_vec()).map_err(|_| CodecError::InvalidUtf8 { field_name }) +} + +fn parse_required_c_string( + field_name: &'static str, + bytes: &[u8], +) -> Result<(String, usize), FieldValueError> { + let Some(end) = bytes.iter().position(|byte| *byte == 0) else { + return Err(invalid_value(field_name, "string is not NUL-terminated")); + }; + + let value = String::from_utf8(bytes[..end].to_vec()) + .map_err(|_| invalid_value(field_name, "string is not valid UTF-8"))?; + Ok((value, end + 1)) +} + +fn decode_required_c_string( + field_name: &'static str, + bytes: &[u8], +) -> Result<(String, usize), CodecError> { + let Some(end) = bytes.iter().position(|byte| *byte == 0) else { + return Err(invalid_value(field_name, "string is not NUL-terminated").into()); + }; + + let value = String::from_utf8(bytes[..end].to_vec()) + .map_err(|_| CodecError::InvalidUtf8 { field_name })?; + Ok((value, end + 1)) +} + +fn looks_like_missing_elng_full_box_header(bytes: &[u8]) -> bool { + let Some(end) = bytes.iter().position(|byte| *byte == 0) else { + return false; + }; + + end > 0 + && bytes[end..].iter().all(|byte| *byte == 0) + && bytes[..end] + .iter() + .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'-') +} + +/// Extended-language box carried alongside `mdhd` when a track uses a language tag that does not +/// fit the compact ISO-639-2 code stored in the media header. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Smhd { +pub struct Elng { full_box: FullBoxState, - pub balance: i16, + pub extended_language: String, + missing_full_box_header: bool, } -impl FieldHooks for Smhd { - fn display_field(&self, name: &'static str) -> Option { - match name { - "Balance" => Some(format_fixed_8_8_signed(self.balance)), - _ => None, - } - } -} +impl FieldHooks for Elng {} -impl ImmutableBox for Smhd { +impl ImmutableBox for Elng { fn box_type(&self) -> FourCc { - FourCc::from_bytes(*b"smhd") + FourCc::from_bytes(*b"elng") } fn version(&self) -> u8 { @@ -2200,46 +2962,37 @@ impl ImmutableBox for Smhd { } } -impl MutableBox for Smhd { +impl MutableBox for Elng { fn set_version(&mut self, version: u8) { self.full_box.version = version; + self.missing_full_box_header = false; } fn set_flags(&mut self, flags: u32) { self.full_box.flags = flags; + self.missing_full_box_header = false; } } -impl Smhd { - /// Returns the balance as a signed 8.8 fixed-point value. - pub fn balance_value(&self) -> f32 { - f32::from(self.balance) / 256.0 - } - - /// Returns the integer component of the balance. - pub fn balance_int(&self) -> i8 { - (self.balance >> 8) as i8 - } -} - -impl FieldValueRead for Smhd { +impl FieldValueRead for Elng { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "Balance" => Ok(FieldValue::Signed(i64::from(self.balance))), + "ExtendedLanguage" => Ok(FieldValue::String(self.extended_language.clone())), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Smhd { +impl FieldValueWrite for Elng { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("Balance", FieldValue::Signed(value)) => { - self.balance = i16_from_signed(field_name, value)?; + ("ExtendedLanguage", FieldValue::String(value)) => { + validate_c_string_value(field_name, &value)?; + self.extended_language = value; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -2247,414 +3000,341 @@ impl FieldValueWrite for Smhd { } } -impl CodecBox for Smhd { +impl CodecBox for Elng { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("Balance", 2, with_bit_width(16), as_signed()), - codec_field!("Reserved", 3, with_bit_width(16), with_constant("0")), + codec_field!( + "ExtendedLanguage", + 2, + with_bit_width(8), + as_string(StringFieldMode::NullTerminated) + ), ]); const SUPPORTED_VERSIONS: &'static [u8] = &[0]; -} - -/// Sample description box. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Stsd { - full_box: FullBoxState, - pub entry_count: u32, -} -impl_full_box!(Stsd, *b"stsd"); + fn custom_marshal(&self, writer: &mut dyn Write) -> Result, CodecError> { + validate_c_string_value("ExtendedLanguage", &self.extended_language)?; + if self.version() != 0 { + return Err(CodecError::UnsupportedVersion { + box_type: self.box_type(), + version: self.version(), + }); + } + if self.flags() != 0 { + return Err(invalid_value("Flags", "non-zero flags are not supported").into()); + } -impl FieldValueRead for Stsd { - fn field_value(&self, field_name: &'static str) -> Result { - match field_name { - "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), - _ => Err(missing_field(field_name)), + let mut payload = Vec::with_capacity(4 + self.extended_language.len() + 1); + if !self.missing_full_box_header { + payload.push(self.version()); + push_uint("Flags", &mut payload, 3, u64::from(self.flags()))?; } + payload.extend_from_slice(self.extended_language.as_bytes()); + payload.push(0); + writer.write_all(&payload)?; + Ok(Some(payload.len() as u64)) } -} -impl FieldValueWrite for Stsd { - fn set_field_value( + fn custom_unmarshal( &mut self, - field_name: &'static str, - value: FieldValue, - ) -> Result<(), FieldValueError> { - match (field_name, value) { - ("EntryCount", FieldValue::Unsigned(value)) => { - self.entry_count = u32_from_unsigned(field_name, value)?; - Ok(()) - } - (field_name, value) => Err(unexpected_field(field_name, value)), + reader: &mut dyn ReadSeek, + payload_size: u64, + ) -> Result, CodecError> { + let payload_len = usize::try_from(payload_size) + .map_err(|_| invalid_value("Payload", "payload is too large to decode"))?; + let payload = read_exact_vec_untrusted(reader, payload_len).map_err(CodecError::Io)?; + + if (payload.len() < 4 || !payload.starts_with(&[0, 0, 0, 0])) + && looks_like_missing_elng_full_box_header(&payload) + { + self.full_box = FullBoxState::default(); + self.extended_language = decode_c_string("ExtendedLanguage", &payload)?; + self.missing_full_box_header = true; + return Ok(Some(payload_size)); } - } -} -impl CodecBox for Stsd { - const FIELD_TABLE: FieldTable = FieldTable::new(&[ - codec_field!("Version", 0, with_bit_width(8), as_version_field()), - codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("EntryCount", 2, with_bit_width(32)), - ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0]; + if payload.len() < 4 { + return Err(invalid_value("Payload", "payload is too short").into()); + } + + let version = payload[0]; + if version != 0 { + return Err(CodecError::UnsupportedVersion { + box_type: self.box_type(), + version, + }); + } + let flags = read_uint(&payload, 1, 3) as u32; + if flags != 0 { + return Err(invalid_value("Flags", "non-zero flags are not supported").into()); + } + + self.full_box = FullBoxState { version, flags }; + self.extended_language = decode_c_string("ExtendedLanguage", &payload[4..])?; + self.missing_full_box_header = false; + Ok(Some(payload_size)) + } } -/// Composition-to-decode timeline shift box. +/// Movie header box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Cslg { +pub struct Mvhd { full_box: FullBoxState, - pub composition_to_dts_shift_v0: i32, - pub least_decode_to_display_delta_v0: i32, - pub greatest_decode_to_display_delta_v0: i32, - pub composition_start_time_v0: i32, - pub composition_end_time_v0: i32, - pub composition_to_dts_shift_v1: i64, - pub least_decode_to_display_delta_v1: i64, - pub greatest_decode_to_display_delta_v1: i64, - pub composition_start_time_v1: i64, - pub composition_end_time_v1: i64, + pub creation_time_v0: u32, + pub modification_time_v0: u32, + pub creation_time_v1: u64, + pub modification_time_v1: u64, + pub timescale: u32, + pub duration_v0: u32, + pub duration_v1: u64, + pub rate: i32, + pub volume: i16, + pub matrix: [i32; 9], + pub pre_defined: [i32; 6], + pub next_track_id: u32, } -impl_full_box!(Cslg, *b"cslg"); +impl_full_box!(Mvhd, *b"mvhd"); -impl Cslg { - /// Returns the active composition-to-decode shift for the current box version. - pub fn composition_to_dts_shift(&self) -> i64 { - match self.version() { - 0 => i64::from(self.composition_to_dts_shift_v0), - 1 => self.composition_to_dts_shift_v1, - _ => 0, +impl FieldHooks for Mvhd { + fn display_field(&self, name: &'static str) -> Option { + match name { + "Rate" => Some(format_fixed_16_16_signed(self.rate)), + _ => None, } } +} - /// Returns the active least decode-to-display delta for the current box version. - pub fn least_decode_to_display_delta(&self) -> i64 { +impl Mvhd { + /// Returns the active movie creation time for the current box version. + pub fn creation_time(&self) -> u64 { match self.version() { - 0 => i64::from(self.least_decode_to_display_delta_v0), - 1 => self.least_decode_to_display_delta_v1, + 0 => u64::from(self.creation_time_v0), + 1 => self.creation_time_v1, _ => 0, } } - /// Returns the active greatest decode-to-display delta for the current box version. - pub fn greatest_decode_to_display_delta(&self) -> i64 { + /// Returns the active movie modification time for the current box version. + pub fn modification_time(&self) -> u64 { match self.version() { - 0 => i64::from(self.greatest_decode_to_display_delta_v0), - 1 => self.greatest_decode_to_display_delta_v1, + 0 => u64::from(self.modification_time_v0), + 1 => self.modification_time_v1, _ => 0, } } - /// Returns the active composition start time for the current box version. - pub fn composition_start_time(&self) -> i64 { + /// Returns the active movie duration for the current box version. + pub fn duration(&self) -> u64 { match self.version() { - 0 => i64::from(self.composition_start_time_v0), - 1 => self.composition_start_time_v1, + 0 => u64::from(self.duration_v0), + 1 => self.duration_v1, _ => 0, } } - /// Returns the active composition end time for the current box version. - pub fn composition_end_time(&self) -> i64 { - match self.version() { - 0 => i64::from(self.composition_end_time_v0), - 1 => self.composition_end_time_v1, - _ => 0, - } + /// Returns the playback rate as a signed 16.16 fixed-point value. + pub fn rate_value(&self) -> f64 { + f64::from(self.rate) / 65536.0 + } + + /// Returns the integer component of the playback rate. + pub fn rate_int(&self) -> i16 { + (self.rate >> 16) as i16 } } -impl FieldValueRead for Cslg { +impl FieldValueRead for Mvhd { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "CompositionToDTSShiftV0" => Ok(FieldValue::Signed(i64::from( - self.composition_to_dts_shift_v0, - ))), - "LeastDecodeToDisplayDeltaV0" => Ok(FieldValue::Signed(i64::from( - self.least_decode_to_display_delta_v0, - ))), - "GreatestDecodeToDisplayDeltaV0" => Ok(FieldValue::Signed(i64::from( - self.greatest_decode_to_display_delta_v0, - ))), - "CompositionStartTimeV0" => Ok(FieldValue::Signed(i64::from( - self.composition_start_time_v0, - ))), - "CompositionEndTimeV0" => { - Ok(FieldValue::Signed(i64::from(self.composition_end_time_v0))) - } - "CompositionToDTSShiftV1" => Ok(FieldValue::Signed(self.composition_to_dts_shift_v1)), - "LeastDecodeToDisplayDeltaV1" => { - Ok(FieldValue::Signed(self.least_decode_to_display_delta_v1)) - } - "GreatestDecodeToDisplayDeltaV1" => { - Ok(FieldValue::Signed(self.greatest_decode_to_display_delta_v1)) - } - "CompositionStartTimeV1" => Ok(FieldValue::Signed(self.composition_start_time_v1)), - "CompositionEndTimeV1" => Ok(FieldValue::Signed(self.composition_end_time_v1)), + "CreationTimeV0" => Ok(FieldValue::Unsigned(u64::from(self.creation_time_v0))), + "ModificationTimeV0" => Ok(FieldValue::Unsigned(u64::from(self.modification_time_v0))), + "CreationTimeV1" => Ok(FieldValue::Unsigned(self.creation_time_v1)), + "ModificationTimeV1" => Ok(FieldValue::Unsigned(self.modification_time_v1)), + "Timescale" => Ok(FieldValue::Unsigned(u64::from(self.timescale))), + "DurationV0" => Ok(FieldValue::Unsigned(u64::from(self.duration_v0))), + "DurationV1" => Ok(FieldValue::Unsigned(self.duration_v1)), + "Rate" => Ok(FieldValue::Signed(i64::from(self.rate))), + "Volume" => Ok(FieldValue::Signed(i64::from(self.volume))), + "Reserved2" => Ok(FieldValue::Bytes(vec![0; 8])), + "Matrix" => Ok(FieldValue::SignedArray( + self.matrix.iter().copied().map(i64::from).collect(), + )), + "PreDefined" => Ok(FieldValue::SignedArray( + self.pre_defined.iter().copied().map(i64::from).collect(), + )), + "NextTrackID" => Ok(FieldValue::Unsigned(u64::from(self.next_track_id))), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Cslg { +impl FieldValueWrite for Mvhd { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("CompositionToDTSShiftV0", FieldValue::Signed(value)) => { - self.composition_to_dts_shift_v0 = i32_from_signed(field_name, value)?; + ("CreationTimeV0", FieldValue::Unsigned(value)) => { + self.creation_time_v0 = u32_from_unsigned(field_name, value)?; Ok(()) } - ("LeastDecodeToDisplayDeltaV0", FieldValue::Signed(value)) => { - self.least_decode_to_display_delta_v0 = i32_from_signed(field_name, value)?; + ("ModificationTimeV0", FieldValue::Unsigned(value)) => { + self.modification_time_v0 = u32_from_unsigned(field_name, value)?; Ok(()) } - ("GreatestDecodeToDisplayDeltaV0", FieldValue::Signed(value)) => { - self.greatest_decode_to_display_delta_v0 = i32_from_signed(field_name, value)?; + ("CreationTimeV1", FieldValue::Unsigned(value)) => { + self.creation_time_v1 = value; Ok(()) } - ("CompositionStartTimeV0", FieldValue::Signed(value)) => { - self.composition_start_time_v0 = i32_from_signed(field_name, value)?; + ("ModificationTimeV1", FieldValue::Unsigned(value)) => { + self.modification_time_v1 = value; Ok(()) } - ("CompositionEndTimeV0", FieldValue::Signed(value)) => { - self.composition_end_time_v0 = i32_from_signed(field_name, value)?; + ("Timescale", FieldValue::Unsigned(value)) => { + self.timescale = u32_from_unsigned(field_name, value)?; Ok(()) } - ("CompositionToDTSShiftV1", FieldValue::Signed(value)) => { - self.composition_to_dts_shift_v1 = i64_from_signed(field_name, value)?; + ("DurationV0", FieldValue::Unsigned(value)) => { + self.duration_v0 = u32_from_unsigned(field_name, value)?; Ok(()) } - ("LeastDecodeToDisplayDeltaV1", FieldValue::Signed(value)) => { - self.least_decode_to_display_delta_v1 = i64_from_signed(field_name, value)?; + ("DurationV1", FieldValue::Unsigned(value)) => { + self.duration_v1 = value; Ok(()) } - ("GreatestDecodeToDisplayDeltaV1", FieldValue::Signed(value)) => { - self.greatest_decode_to_display_delta_v1 = i64_from_signed(field_name, value)?; + ("Rate", FieldValue::Signed(value)) => { + self.rate = i32_from_signed(field_name, value)?; Ok(()) } - ("CompositionStartTimeV1", FieldValue::Signed(value)) => { - self.composition_start_time_v1 = i64_from_signed(field_name, value)?; + ("Volume", FieldValue::Signed(value)) => { + self.volume = i16_from_signed(field_name, value)?; Ok(()) } - ("CompositionEndTimeV1", FieldValue::Signed(value)) => { - self.composition_end_time_v1 = i64_from_signed(field_name, value)?; + ("Reserved2", FieldValue::Bytes(bytes)) => bytes_to_zeroes(field_name, &bytes, 8), + ("Matrix", FieldValue::SignedArray(values)) => { + if values.len() != 9 { + return Err(invalid_value( + field_name, + "value must contain exactly 9 elements", + )); + } + for (slot, value) in self.matrix.iter_mut().zip(values) { + *slot = i32_from_signed(field_name, value)?; + } Ok(()) } - (field_name, value) => Err(unexpected_field(field_name, value)), + ("PreDefined", FieldValue::SignedArray(values)) => { + if values.len() != 6 { + return Err(invalid_value( + field_name, + "value must contain exactly 6 elements", + )); + } + for (slot, value) in self.pre_defined.iter_mut().zip(values) { + *slot = i32_from_signed(field_name, value)?; + } + Ok(()) + } + ("NextTrackID", FieldValue::Unsigned(value)) => { + self.next_track_id = u32_from_unsigned(field_name, value)?; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), } } } -impl CodecBox for Cslg { +impl CodecBox for Mvhd { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("CreationTimeV0", 2, with_bit_width(32), with_version(0)), + codec_field!("ModificationTimeV0", 3, with_bit_width(32), with_version(0)), + codec_field!("CreationTimeV1", 4, with_bit_width(64), with_version(1)), + codec_field!("ModificationTimeV1", 5, with_bit_width(64), with_version(1)), + codec_field!("Timescale", 6, with_bit_width(32)), + codec_field!("DurationV0", 7, with_bit_width(32), with_version(0)), + codec_field!("DurationV1", 8, with_bit_width(64), with_version(1)), + codec_field!("Rate", 9, with_bit_width(32), as_signed()), + codec_field!("Volume", 10, with_bit_width(16), as_signed()), + codec_field!("Reserved", 11, with_bit_width(16), with_constant("0")), codec_field!( - "CompositionToDTSShiftV0", - 2, - with_bit_width(32), - with_version(0), - as_signed() - ), - codec_field!( - "LeastDecodeToDisplayDeltaV0", - 3, - with_bit_width(32), - with_version(0), - as_signed() - ), - codec_field!( - "GreatestDecodeToDisplayDeltaV0", - 4, - with_bit_width(32), - with_version(0), - as_signed() + "Reserved2", + 12, + with_bit_width(8), + with_length(8), + as_bytes(), + as_hidden() ), codec_field!( - "CompositionStartTimeV0", - 5, + "Matrix", + 13, with_bit_width(32), - with_version(0), - as_signed() + with_length(9), + as_signed(), + as_hex() ), codec_field!( - "CompositionEndTimeV0", - 6, + "PreDefined", + 14, with_bit_width(32), - with_version(0), - as_signed() - ), - codec_field!( - "CompositionToDTSShiftV1", - 7, - with_bit_width(64), - with_version(1), - as_signed() - ), - codec_field!( - "LeastDecodeToDisplayDeltaV1", - 8, - with_bit_width(64), - with_version(1), - as_signed() - ), - codec_field!( - "GreatestDecodeToDisplayDeltaV1", - 9, - with_bit_width(64), - with_version(1), - as_signed() - ), - codec_field!( - "CompositionStartTimeV1", - 10, - with_bit_width(64), - with_version(1), - as_signed() - ), - codec_field!( - "CompositionEndTimeV1", - 11, - with_bit_width(64), - with_version(1), + with_length(6), as_signed() ), + codec_field!("NextTrackID", 15, with_bit_width(32)), ]); const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; } -/// One composition-offset run. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct CttsEntry { - pub sample_count: u32, - pub sample_offset_v0: u32, - pub sample_offset_v1: i32, -} - -/// Composition time to sample box. +/// Track fragment decode time box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Ctts { +pub struct Tfdt { full_box: FullBoxState, - pub entry_count: u32, - pub entries: Vec, -} - -impl FieldHooks for Ctts { - fn field_length(&self, name: &'static str) -> Option { - match name { - "Entries" => usize::try_from(self.entry_count) - .ok() - .and_then(|count| field_len_bytes(count, 8)), - _ => None, - } - } - - fn display_field(&self, name: &'static str) -> Option { - match name { - "Entries" => Some(render_array(self.entries.iter().map( - |entry| match self.version() { - 0 => format!( - "{{SampleCount={} SampleOffsetV0={}}}", - entry.sample_count, entry.sample_offset_v0 - ), - 1 => format!( - "{{SampleCount={} SampleOffsetV1={}}}", - entry.sample_count, entry.sample_offset_v1 - ), - _ => String::from("{}"), - }, - ))), - _ => None, - } - } -} - -impl ImmutableBox for Ctts { - fn box_type(&self) -> FourCc { - FourCc::from_bytes(*b"ctts") - } - - fn version(&self) -> u8 { - self.full_box.version - } - - fn flags(&self) -> u32 { - self.full_box.flags - } + pub base_media_decode_time_v0: u32, + pub base_media_decode_time_v1: u64, } -impl MutableBox for Ctts { - fn set_version(&mut self, version: u8) { - self.full_box.version = version; - } - - fn set_flags(&mut self, flags: u32) { - self.full_box.flags = flags; - } -} +impl_full_box!(Tfdt, *b"tfdt"); -impl Ctts { - /// Returns the active sample offset for `index`. - pub fn sample_offset(&self, index: usize) -> i64 { +impl Tfdt { + /// Returns the active base media decode time for the current box version. + pub fn base_media_decode_time(&self) -> u64 { match self.version() { - 0 => i64::from(self.entries[index].sample_offset_v0), - 1 => i64::from(self.entries[index].sample_offset_v1), + 0 => u64::from(self.base_media_decode_time_v0), + 1 => self.base_media_decode_time_v1, _ => 0, } } } -impl FieldValueRead for Ctts { +impl FieldValueRead for Tfdt { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), - "Entries" => { - let mut bytes = Vec::with_capacity(self.entries.len() * 8); - for entry in &self.entries { - bytes.extend_from_slice(&entry.sample_count.to_be_bytes()); - match self.version() { - 0 => bytes.extend_from_slice(&entry.sample_offset_v0.to_be_bytes()), - 1 => bytes.extend_from_slice(&entry.sample_offset_v1.to_be_bytes()), - _ => {} - } - } - Ok(FieldValue::Bytes(bytes)) - } + "BaseMediaDecodeTimeV0" => Ok(FieldValue::Unsigned(u64::from( + self.base_media_decode_time_v0, + ))), + "BaseMediaDecodeTimeV1" => Ok(FieldValue::Unsigned(self.base_media_decode_time_v1)), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Ctts { +impl FieldValueWrite for Tfdt { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("EntryCount", FieldValue::Unsigned(value)) => { - self.entry_count = u32_from_unsigned(field_name, value)?; + ("BaseMediaDecodeTimeV0", FieldValue::Unsigned(value)) => { + self.base_media_decode_time_v0 = u32_from_unsigned(field_name, value)?; Ok(()) } - ("Entries", FieldValue::Bytes(bytes)) => { - self.entries = - parse_fixed_chunks(field_name, &bytes, 8, |chunk| match self.version() { - 0 => CttsEntry { - sample_count: read_u32(chunk, 0), - sample_offset_v0: read_u32(chunk, 4), - ..CttsEntry::default() - }, - 1 => CttsEntry { - sample_count: read_u32(chunk, 0), - sample_offset_v1: read_i32(chunk, 4), - ..CttsEntry::default() - }, - _ => CttsEntry::default(), - })?; + ("BaseMediaDecodeTimeV1", FieldValue::Unsigned(value)) => { + self.base_media_decode_time_v1 = value; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -2662,79 +3342,166 @@ impl FieldValueWrite for Ctts { } } -impl CodecBox for Ctts { +impl CodecBox for Tfdt { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("EntryCount", 2, with_bit_width(32)), codec_field!( - "Entries", + "BaseMediaDecodeTimeV0", + 2, + with_bit_width(32), + with_version(0) + ), + codec_field!( + "BaseMediaDecodeTimeV1", 3, - with_bit_width(8), - with_dynamic_length(), - as_bytes() + with_bit_width(64), + with_version(1) ), ]); const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; } -/// One edit-list entry. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct ElstEntry { - pub segment_duration_v0: u32, - pub media_time_v0: i32, - pub segment_duration_v1: u64, - pub media_time_v1: i64, - pub media_rate_integer: i16, -} - -/// Edit list box. +/// Track fragment header box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Elst { +pub struct Tfhd { full_box: FullBoxState, - pub entry_count: u32, - pub entries: Vec, + pub track_id: u32, + pub base_data_offset: u64, + pub sample_description_index: u32, + pub default_sample_duration: u32, + pub default_sample_size: u32, + pub default_sample_flags: u32, } -impl FieldHooks for Elst { - fn field_length(&self, name: &'static str) -> Option { - match name { - "Entries" => match self.version() { - 0 => usize::try_from(self.entry_count) - .ok() - .and_then(|count| field_len_bytes(count, 12)), - 1 => usize::try_from(self.entry_count) - .ok() - .and_then(|count| field_len_bytes(count, 20)), - _ => Some(0), - }, - _ => None, +impl_full_box!(Tfhd, *b"tfhd"); + +impl FieldValueRead for Tfhd { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "TrackID" => Ok(FieldValue::Unsigned(u64::from(self.track_id))), + "BaseDataOffset" => Ok(FieldValue::Unsigned(self.base_data_offset)), + "SampleDescriptionIndex" => Ok(FieldValue::Unsigned(u64::from( + self.sample_description_index, + ))), + "DefaultSampleDuration" => Ok(FieldValue::Unsigned(u64::from( + self.default_sample_duration, + ))), + "DefaultSampleSize" => Ok(FieldValue::Unsigned(u64::from(self.default_sample_size))), + "DefaultSampleFlags" => Ok(FieldValue::Unsigned(u64::from(self.default_sample_flags))), + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Tfhd { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("TrackID", FieldValue::Unsigned(value)) => { + self.track_id = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("BaseDataOffset", FieldValue::Unsigned(value)) => { + self.base_data_offset = value; + Ok(()) + } + ("SampleDescriptionIndex", FieldValue::Unsigned(value)) => { + self.sample_description_index = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("DefaultSampleDuration", FieldValue::Unsigned(value)) => { + self.default_sample_duration = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("DefaultSampleSize", FieldValue::Unsigned(value)) => { + self.default_sample_size = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("DefaultSampleFlags", FieldValue::Unsigned(value)) => { + self.default_sample_flags = u32_from_unsigned(field_name, value)?; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), } } +} + +impl CodecBox for Tfhd { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("TrackID", 2, with_bit_width(32)), + codec_field!( + "BaseDataOffset", + 3, + with_bit_width(64), + with_required_flags(TFHD_BASE_DATA_OFFSET_PRESENT) + ), + codec_field!( + "SampleDescriptionIndex", + 4, + with_bit_width(32), + with_required_flags(TFHD_SAMPLE_DESCRIPTION_INDEX_PRESENT) + ), + codec_field!( + "DefaultSampleDuration", + 5, + with_bit_width(32), + with_required_flags(TFHD_DEFAULT_SAMPLE_DURATION_PRESENT) + ), + codec_field!( + "DefaultSampleSize", + 6, + with_bit_width(32), + with_required_flags(TFHD_DEFAULT_SAMPLE_SIZE_PRESENT) + ), + codec_field!( + "DefaultSampleFlags", + 7, + with_bit_width(32), + with_required_flags(TFHD_DEFAULT_SAMPLE_FLAGS_PRESENT), + as_hex() + ), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; +} + +/// Track header box. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Tkhd { + full_box: FullBoxState, + pub creation_time_v0: u32, + pub modification_time_v0: u32, + pub creation_time_v1: u64, + pub modification_time_v1: u64, + pub track_id: u32, + pub duration_v0: u32, + pub duration_v1: u64, + pub layer: i16, + pub alternate_group: i16, + pub volume: i16, + pub matrix: [i32; 9], + pub width: u32, + pub height: u32, +} +impl FieldHooks for Tkhd { fn display_field(&self, name: &'static str) -> Option { match name { - "Entries" => Some(render_array(self.entries.iter().map( - |entry| match self.version() { - 0 => format!( - "{{SegmentDurationV0={} MediaTimeV0={} MediaRateInteger={}}}", - entry.segment_duration_v0, entry.media_time_v0, entry.media_rate_integer - ), - 1 => format!( - "{{SegmentDurationV1={} MediaTimeV1={} MediaRateInteger={}}}", - entry.segment_duration_v1, entry.media_time_v1, entry.media_rate_integer - ), - _ => String::from("{}"), - }, - ))), + "Width" => Some(format_fixed_16_16_unsigned(self.width)), + "Height" => Some(format_fixed_16_16_unsigned(self.height)), _ => None, } } } -impl ImmutableBox for Elst { +impl ImmutableBox for Tkhd { fn box_type(&self) -> FourCc { - FourCc::from_bytes(*b"elst") + FourCc::from_bytes(*b"tkhd") } fn version(&self) -> u8 { @@ -2746,7 +3513,7 @@ impl ImmutableBox for Elst { } } -impl MutableBox for Elst { +impl MutableBox for Tkhd { fn set_version(&mut self, version: u8) { self.full_box.version = version; } @@ -2756,159 +3523,380 @@ impl MutableBox for Elst { } } -impl Elst { - /// Returns the active segment duration for `index`. - pub fn segment_duration(&self, index: usize) -> u64 { +impl Tkhd { + /// Returns the active track creation time for the current box version. + pub fn creation_time(&self) -> u64 { match self.version() { - 0 => u64::from(self.entries[index].segment_duration_v0), - 1 => self.entries[index].segment_duration_v1, + 0 => u64::from(self.creation_time_v0), + 1 => self.creation_time_v1, _ => 0, } } - /// Returns the active media time for `index`. - pub fn media_time(&self, index: usize) -> i64 { + /// Returns the active track modification time for the current box version. + pub fn modification_time(&self) -> u64 { match self.version() { - 0 => i64::from(self.entries[index].media_time_v0), - 1 => self.entries[index].media_time_v1, + 0 => u64::from(self.modification_time_v0), + 1 => self.modification_time_v1, + _ => 0, + } + } + + /// Returns the active track duration for the current box version. + pub fn duration(&self) -> u64 { + match self.version() { + 0 => u64::from(self.duration_v0), + 1 => self.duration_v1, _ => 0, } } + + /// Returns the track width as an unsigned 16.16 fixed-point value. + pub fn width_value(&self) -> f64 { + f64::from(self.width) / 65536.0 + } + + /// Returns the integer component of the track width. + pub fn width_int(&self) -> u16 { + (self.width >> 16) as u16 + } + + /// Returns the track height as an unsigned 16.16 fixed-point value. + pub fn height_value(&self) -> f64 { + f64::from(self.height) / 65536.0 + } + + /// Returns the integer component of the track height. + pub fn height_int(&self) -> u16 { + (self.height >> 16) as u16 + } } -impl FieldValueRead for Elst { +impl FieldValueRead for Tkhd { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), - "Entries" => { - let mut bytes = Vec::new(); - for entry in &self.entries { - match self.version() { - 0 => { - bytes.extend_from_slice(&entry.segment_duration_v0.to_be_bytes()); - bytes.extend_from_slice(&entry.media_time_v0.to_be_bytes()); - } - 1 => { - bytes.extend_from_slice(&entry.segment_duration_v1.to_be_bytes()); - bytes.extend_from_slice(&entry.media_time_v1.to_be_bytes()); - } - _ => {} - } - bytes.extend_from_slice(&entry.media_rate_integer.to_be_bytes()); - bytes.extend_from_slice(&0_i16.to_be_bytes()); - } - Ok(FieldValue::Bytes(bytes)) - } + "CreationTimeV0" => Ok(FieldValue::Unsigned(u64::from(self.creation_time_v0))), + "ModificationTimeV0" => Ok(FieldValue::Unsigned(u64::from(self.modification_time_v0))), + "CreationTimeV1" => Ok(FieldValue::Unsigned(self.creation_time_v1)), + "ModificationTimeV1" => Ok(FieldValue::Unsigned(self.modification_time_v1)), + "TrackID" => Ok(FieldValue::Unsigned(u64::from(self.track_id))), + "DurationV0" => Ok(FieldValue::Unsigned(u64::from(self.duration_v0))), + "DurationV1" => Ok(FieldValue::Unsigned(self.duration_v1)), + "Reserved1" => Ok(FieldValue::Bytes(vec![0; 8])), + "Layer" => Ok(FieldValue::Signed(i64::from(self.layer))), + "AlternateGroup" => Ok(FieldValue::Signed(i64::from(self.alternate_group))), + "Volume" => Ok(FieldValue::Signed(i64::from(self.volume))), + "Matrix" => Ok(FieldValue::SignedArray( + self.matrix.iter().copied().map(i64::from).collect(), + )), + "Width" => Ok(FieldValue::Unsigned(u64::from(self.width))), + "Height" => Ok(FieldValue::Unsigned(u64::from(self.height))), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Elst { +impl FieldValueWrite for Tkhd { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("EntryCount", FieldValue::Unsigned(value)) => { - self.entry_count = u32_from_unsigned(field_name, value)?; + ("CreationTimeV0", FieldValue::Unsigned(value)) => { + self.creation_time_v0 = u32_from_unsigned(field_name, value)?; Ok(()) } - ("Entries", FieldValue::Bytes(bytes)) => { - self.entries = match self.version() { - 0 => parse_fixed_chunks(field_name, &bytes, 12, |chunk| ElstEntry { - segment_duration_v0: read_u32(chunk, 0), - media_time_v0: read_i32(chunk, 4), - media_rate_integer: read_i16(chunk, 8), - ..ElstEntry::default() - })?, - 1 => parse_fixed_chunks(field_name, &bytes, 20, |chunk| ElstEntry { - segment_duration_v1: read_u64(chunk, 0), - media_time_v1: read_i64(chunk, 8), - media_rate_integer: read_i16(chunk, 16), - ..ElstEntry::default() - })?, - _ => Vec::new(), - }; - for chunk in bytes.chunks_exact(match self.version() { - 0 => 12, - 1 => 20, - _ => 1, - }) { - let offset = chunk.len() - 2; - if read_i16(chunk, offset) != 0 { - return Err(invalid_value( - field_name, - "media rate fraction must be zero", - )); - } + ("ModificationTimeV0", FieldValue::Unsigned(value)) => { + self.modification_time_v0 = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("CreationTimeV1", FieldValue::Unsigned(value)) => { + self.creation_time_v1 = value; + Ok(()) + } + ("ModificationTimeV1", FieldValue::Unsigned(value)) => { + self.modification_time_v1 = value; + Ok(()) + } + ("TrackID", FieldValue::Unsigned(value)) => { + self.track_id = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("DurationV0", FieldValue::Unsigned(value)) => { + self.duration_v0 = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("DurationV1", FieldValue::Unsigned(value)) => { + self.duration_v1 = value; + Ok(()) + } + ("Reserved1", FieldValue::Bytes(bytes)) => bytes_to_zeroes(field_name, &bytes, 8), + ("Layer", FieldValue::Signed(value)) => { + self.layer = i16_from_signed(field_name, value)?; + Ok(()) + } + ("AlternateGroup", FieldValue::Signed(value)) => { + self.alternate_group = i16_from_signed(field_name, value)?; + Ok(()) + } + ("Volume", FieldValue::Signed(value)) => { + self.volume = i16_from_signed(field_name, value)?; + Ok(()) + } + ("Matrix", FieldValue::SignedArray(values)) => { + if values.len() != 9 { + return Err(invalid_value( + field_name, + "value must contain exactly 9 elements", + )); + } + for (slot, value) in self.matrix.iter_mut().zip(values) { + *slot = i32_from_signed(field_name, value)?; } Ok(()) } + ("Width", FieldValue::Unsigned(value)) => { + self.width = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("Height", FieldValue::Unsigned(value)) => { + self.height = u32_from_unsigned(field_name, value)?; + Ok(()) + } (field_name, value) => Err(unexpected_field(field_name, value)), } } } -impl CodecBox for Elst { +impl CodecBox for Tkhd { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("EntryCount", 2, with_bit_width(32)), + codec_field!("CreationTimeV0", 2, with_bit_width(32), with_version(0)), + codec_field!("ModificationTimeV0", 3, with_bit_width(32), with_version(0)), + codec_field!("CreationTimeV1", 4, with_bit_width(64), with_version(1)), + codec_field!("ModificationTimeV1", 5, with_bit_width(64), with_version(1)), + codec_field!("TrackID", 6, with_bit_width(32)), + codec_field!("Reserved0", 7, with_bit_width(32), with_constant("0")), + codec_field!("DurationV0", 8, with_bit_width(32), with_version(0)), + codec_field!("DurationV1", 9, with_bit_width(64), with_version(1)), codec_field!( - "Entries", - 3, + "Reserved1", + 10, with_bit_width(8), - with_dynamic_length(), - as_bytes() + with_length(8), + as_bytes(), + as_hidden() + ), + codec_field!("Layer", 11, with_bit_width(16), as_signed()), + codec_field!("AlternateGroup", 12, with_bit_width(16), as_signed()), + codec_field!("Volume", 13, with_bit_width(16), as_signed()), + codec_field!("Reserved2", 14, with_bit_width(16), with_constant("0")), + codec_field!( + "Matrix", + 15, + with_bit_width(32), + with_length(9), + as_signed(), + as_hex() ), + codec_field!("Width", 16, with_bit_width(32)), + codec_field!("Height", 17, with_bit_width(32)), ]); const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; } -/// 64-bit chunk offset box. +/// One level-assignment record carried by [`Leva`]. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Co64 { +pub struct LevaLevel { + 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, +} + +/// Level-assignment box used by track-extension property paths. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Leva { full_box: FullBoxState, - pub entry_count: u32, - pub chunk_offset: Vec, + pub level_count: u8, + pub levels: Vec, } -impl_full_box!(Co64, *b"co64"); +fn format_leva_levels(levels: &[LevaLevel]) -> String { + render_array(levels.iter().map(|level| match level.assignment_type { + 0 => format!( + "{{TrackID={} PaddingFlag={} AssignmentType={} GroupingType=0x{:08x}}}", + level.track_id, level.padding_flag, level.assignment_type, level.grouping_type + ), + 1 => format!( + "{{TrackID={} PaddingFlag={} AssignmentType={} GroupingType=0x{:08x} GroupingTypeParameter={}}}", + level.track_id, + level.padding_flag, + level.assignment_type, + level.grouping_type, + level.grouping_type_parameter + ), + 4 => format!( + "{{TrackID={} PaddingFlag={} AssignmentType={} SubTrackID={}}}", + level.track_id, level.padding_flag, level.assignment_type, level.sub_track_id + ), + _ => format!( + "{{TrackID={} PaddingFlag={} AssignmentType={}}}", + level.track_id, level.padding_flag, level.assignment_type + ), + })) +} -impl FieldHooks for Co64 { - fn field_length(&self, name: &'static str) -> Option { +fn encode_leva_levels( + field_name: &'static str, + levels: &[LevaLevel], +) -> Result, FieldValueError> { + let mut bytes = Vec::new(); + for level in levels { + if level.assignment_type > 4 { + return Err(invalid_value( + field_name, + "assignment type uses a reserved layout", + )); + } + + bytes.extend_from_slice(&level.track_id.to_be_bytes()); + bytes.push((u8::from(level.padding_flag) << 7) | level.assignment_type); + match level.assignment_type { + 0 => bytes.extend_from_slice(&level.grouping_type.to_be_bytes()), + 1 => { + bytes.extend_from_slice(&level.grouping_type.to_be_bytes()); + bytes.extend_from_slice(&level.grouping_type_parameter.to_be_bytes()); + } + 2 | 3 => {} + 4 => bytes.extend_from_slice(&level.sub_track_id.to_be_bytes()), + _ => unreachable!(), + } + } + Ok(bytes) +} + +fn parse_leva_levels( + field_name: &'static str, + level_count: u8, + bytes: &[u8], +) -> Result, FieldValueError> { + let mut levels = Vec::with_capacity(untrusted_prealloc_hint(usize::from(level_count))); + let mut offset = 0usize; + + for _ in 0..level_count { + if bytes.len().saturating_sub(offset) < 5 { + return Err(invalid_value(field_name, "level payload is truncated")); + } + + let track_id = read_u32(bytes, offset); + let assignment_header = bytes[offset + 4]; + offset += 5; + + let padding_flag = assignment_header & 0x80 != 0; + let assignment_type = assignment_header & 0x7f; + let mut level = LevaLevel { + track_id, + padding_flag, + assignment_type, + ..LevaLevel::default() + }; + + match assignment_type { + 0 => { + if bytes.len().saturating_sub(offset) < 4 { + return Err(invalid_value( + field_name, + "grouping type payload is truncated", + )); + } + level.grouping_type = read_u32(bytes, offset); + offset += 4; + } + 1 => { + if bytes.len().saturating_sub(offset) < 8 { + return Err(invalid_value( + field_name, + "grouping type parameter payload is truncated", + )); + } + level.grouping_type = read_u32(bytes, offset); + level.grouping_type_parameter = read_u32(bytes, offset + 4); + offset += 8; + } + 2 | 3 => {} + 4 => { + if bytes.len().saturating_sub(offset) < 4 { + return Err(invalid_value(field_name, "sub-track payload is truncated")); + } + level.sub_track_id = read_u32(bytes, offset); + offset += 4; + } + _ => { + return Err(invalid_value( + field_name, + "assignment type uses a reserved layout", + )); + } + } + + levels.push(level); + } + + if offset != bytes.len() { + return Err(invalid_value( + field_name, + "level payload length does not match the level count", + )); + } + + Ok(levels) +} + +impl FieldHooks for Leva { + fn display_field(&self, name: &'static str) -> Option { match name { - "ChunkOffset" => Some(self.entry_count), + "Levels" => Some(format_leva_levels(&self.levels)), _ => None, } } } -impl FieldValueRead for Co64 { +impl_full_box!(Leva, *b"leva"); + +impl FieldValueRead for Leva { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), - "ChunkOffset" => Ok(FieldValue::UnsignedArray(self.chunk_offset.clone())), + "LevelCount" => Ok(FieldValue::Unsigned(u64::from(self.level_count))), + "Levels" => { + require_count(field_name, u32::from(self.level_count), self.levels.len())?; + Ok(FieldValue::Bytes(encode_leva_levels( + field_name, + &self.levels, + )?)) + } _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Co64 { +impl FieldValueWrite for Leva { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("EntryCount", FieldValue::Unsigned(value)) => { - self.entry_count = u32_from_unsigned(field_name, value)?; + ("LevelCount", FieldValue::Unsigned(value)) => { + self.level_count = u8_from_unsigned(field_name, value)?; Ok(()) } - ("ChunkOffset", FieldValue::UnsignedArray(values)) => { - self.chunk_offset = values; + ("Levels", FieldValue::Bytes(bytes)) => { + self.levels = parse_leva_levels(field_name, self.level_count, &bytes)?; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -2916,62 +3904,83 @@ impl FieldValueWrite for Co64 { } } -impl CodecBox for Co64 { +impl CodecBox for Leva { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("EntryCount", 2, with_bit_width(32)), - codec_field!("ChunkOffset", 3, with_bit_width(64), with_dynamic_length()), + codec_field!("LevelCount", 2, with_bit_width(8)), + codec_field!("Levels", 3, with_bit_width(8), as_bytes()), ]); const SUPPORTED_VERSIONS: &'static [u8] = &[0]; } -/// 32-bit chunk offset box. +track_id_list_box!( + Cdsc, + *b"cdsc", + "Track-link type box for content-description links." +); +track_id_list_box!( + Dpnd, + *b"dpnd", + "Track-link type box for track-dependency links." +); +track_id_list_box!(Font, *b"font", "Track-link type box for font links."); +track_id_list_box!(Hind, *b"hind", "Track-link type box for hint dependencies."); +track_id_list_box!(Hint, *b"hint", "Track-link type box for hint-track links."); +track_id_list_box!( + Ipir, + *b"ipir", + "Track-link type box for auxiliary-picture links." +); +track_id_list_box!( + Mpod, + *b"mpod", + "Track-link type box for metadata-pod links." +); +track_id_list_box!(Subt, *b"subt", "Track-link type box for subtitle links."); +track_id_list_box!( + Sync, + *b"sync", + "Track-link type box for synchronized-track links." +); +track_id_list_box!( + Vdep, + *b"vdep", + "Track-link type box for video-dependency links." +); +track_id_list_box!( + Vplx, + *b"vplx", + "Track-link type box for video-multiplex links." +); + +/// Track extension properties box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Stco { +pub struct Trep { full_box: FullBoxState, - pub entry_count: u32, - pub chunk_offset: Vec, + pub track_id: u32, } -impl_full_box!(Stco, *b"stco"); - -impl FieldHooks for Stco { - fn field_length(&self, name: &'static str) -> Option { - match name { - "ChunkOffset" => Some(self.entry_count), - _ => None, - } - } -} +impl_full_box!(Trep, *b"trep"); -impl FieldValueRead for Stco { +impl FieldValueRead for Trep { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), - "ChunkOffset" => Ok(FieldValue::UnsignedArray(self.chunk_offset.clone())), + "TrackID" => Ok(FieldValue::Unsigned(u64::from(self.track_id))), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Stco { +impl FieldValueWrite for Trep { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("EntryCount", FieldValue::Unsigned(value)) => { - self.entry_count = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("ChunkOffset", FieldValue::UnsignedArray(values)) => { - let mut offsets = Vec::with_capacity(values.len()); - for value in values { - offsets.push(u64::from(u32_from_unsigned(field_name, value)?)); - } - self.chunk_offset = offsets; + ("TrackID", FieldValue::Unsigned(value)) => { + self.track_id = u32_from_unsigned(field_name, value)?; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -2979,114 +3988,70 @@ impl FieldValueWrite for Stco { } } -impl CodecBox for Stco { +impl CodecBox for Trep { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("EntryCount", 2, with_bit_width(32)), - codec_field!("ChunkOffset", 3, with_bit_width(32), with_dynamic_length()), + codec_field!("TrackID", 2, with_bit_width(32)), ]); const SUPPORTED_VERSIONS: &'static [u8] = &[0]; } -/// One sample-to-chunk entry. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct StscEntry { - pub first_chunk: u32, - pub samples_per_chunk: u32, - pub sample_description_index: u32, -} - -/// Sample-to-chunk box. +/// Track extends defaults box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Stsc { +pub struct Trex { full_box: FullBoxState, - pub entry_count: u32, - pub entries: Vec, + pub track_id: u32, + pub default_sample_description_index: u32, + pub default_sample_duration: u32, + pub default_sample_size: u32, + pub default_sample_flags: u32, } -impl FieldHooks for Stsc { - fn field_length(&self, name: &'static str) -> Option { - match name { - "Entries" => usize::try_from(self.entry_count) - .ok() - .and_then(|count| field_len_bytes(count, 12)), - _ => None, - } - } - - fn display_field(&self, name: &'static str) -> Option { - match name { - "Entries" => Some(render_array(self.entries.iter().map(|entry| { - format!( - "{{FirstChunk={} SamplesPerChunk={} SampleDescriptionIndex={}}}", - entry.first_chunk, entry.samples_per_chunk, entry.sample_description_index - ) - }))), - _ => None, - } - } -} - -impl ImmutableBox for Stsc { - fn box_type(&self) -> FourCc { - FourCc::from_bytes(*b"stsc") - } - - fn version(&self) -> u8 { - self.full_box.version - } - - fn flags(&self) -> u32 { - self.full_box.flags - } -} - -impl MutableBox for Stsc { - fn set_version(&mut self, version: u8) { - self.full_box.version = version; - } - - fn set_flags(&mut self, flags: u32) { - self.full_box.flags = flags; - } -} +impl_full_box!(Trex, *b"trex"); -impl FieldValueRead for Stsc { +impl FieldValueRead for Trex { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), - "Entries" => { - let mut bytes = Vec::with_capacity(self.entries.len() * 12); - for entry in &self.entries { - bytes.extend_from_slice(&entry.first_chunk.to_be_bytes()); - bytes.extend_from_slice(&entry.samples_per_chunk.to_be_bytes()); - bytes.extend_from_slice(&entry.sample_description_index.to_be_bytes()); - } - Ok(FieldValue::Bytes(bytes)) - } + "TrackID" => Ok(FieldValue::Unsigned(u64::from(self.track_id))), + "DefaultSampleDescriptionIndex" => Ok(FieldValue::Unsigned(u64::from( + self.default_sample_description_index, + ))), + "DefaultSampleDuration" => Ok(FieldValue::Unsigned(u64::from( + self.default_sample_duration, + ))), + "DefaultSampleSize" => Ok(FieldValue::Unsigned(u64::from(self.default_sample_size))), + "DefaultSampleFlags" => Ok(FieldValue::Unsigned(u64::from(self.default_sample_flags))), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Stsc { +impl FieldValueWrite for Trex { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("EntryCount", FieldValue::Unsigned(value)) => { - self.entry_count = u32_from_unsigned(field_name, value)?; + ("TrackID", FieldValue::Unsigned(value)) => { + self.track_id = u32_from_unsigned(field_name, value)?; Ok(()) } - ("Entries", FieldValue::Bytes(bytes)) => { - self.entries = parse_fixed_chunks(field_name, &bytes, 12, |chunk| StscEntry { - first_chunk: read_u32(chunk, 0), - samples_per_chunk: read_u32(chunk, 4), - sample_description_index: read_u32(chunk, 8), - })?; + ("DefaultSampleDescriptionIndex", FieldValue::Unsigned(value)) => { + self.default_sample_description_index = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("DefaultSampleDuration", FieldValue::Unsigned(value)) => { + self.default_sample_duration = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("DefaultSampleSize", FieldValue::Unsigned(value)) => { + self.default_sample_size = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("DefaultSampleFlags", FieldValue::Unsigned(value)) => { + self.default_sample_flags = u32_from_unsigned(field_name, value)?; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -3094,68 +4059,64 @@ impl FieldValueWrite for Stsc { } } -impl CodecBox for Stsc { +impl CodecBox for Trex { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("EntryCount", 2, with_bit_width(32)), - codec_field!( - "Entries", - 3, - with_bit_width(8), - with_dynamic_length(), - as_bytes() - ), + codec_field!("TrackID", 2, with_bit_width(32)), + codec_field!("DefaultSampleDescriptionIndex", 3, with_bit_width(32)), + codec_field!("DefaultSampleDuration", 4, with_bit_width(32)), + codec_field!("DefaultSampleSize", 5, with_bit_width(32)), + codec_field!("DefaultSampleFlags", 6, with_bit_width(32), as_hex()), ]); const SUPPORTED_VERSIONS: &'static [u8] = &[0]; } -/// Sync sample box. +/// Video media header box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Stss { +pub struct Vmhd { full_box: FullBoxState, - pub entry_count: u32, - pub sample_number: Vec, + pub graphicsmode: u16, + pub opcolor: [u16; 3], } -impl_full_box!(Stss, *b"stss"); - -impl FieldHooks for Stss { - fn field_length(&self, name: &'static str) -> Option { - match name { - "SampleNumber" => Some(self.entry_count), - _ => None, - } - } -} +impl_full_box!(Vmhd, *b"vmhd"); -impl FieldValueRead for Stss { +impl FieldValueRead for Vmhd { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), - "SampleNumber" => Ok(FieldValue::UnsignedArray(self.sample_number.clone())), + "Graphicsmode" => Ok(FieldValue::Unsigned(u64::from(self.graphicsmode))), + "Opcolor" => Ok(FieldValue::UnsignedArray( + self.opcolor.iter().copied().map(u64::from).collect(), + )), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Stss { +impl FieldValueWrite for Vmhd { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("EntryCount", FieldValue::Unsigned(value)) => { - self.entry_count = u32_from_unsigned(field_name, value)?; + ("Graphicsmode", FieldValue::Unsigned(value)) => { + self.graphicsmode = u16_from_unsigned(field_name, value)?; Ok(()) } - ("SampleNumber", FieldValue::UnsignedArray(values)) => { - let mut numbers = Vec::with_capacity(values.len()); - for value in values { - numbers.push(u64::from(u32_from_unsigned(field_name, value)?)); + ("Opcolor", FieldValue::UnsignedArray(values)) => { + if values.len() != 3 { + return Err(invalid_value( + field_name, + "value must contain exactly 3 elements", + )); } - self.sample_number = numbers; + self.opcolor = [ + u16_from_unsigned(field_name, values[0])?, + u16_from_unsigned(field_name, values[1])?, + u16_from_unsigned(field_name, values[2])?, + ]; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -3163,47 +4124,35 @@ impl FieldValueWrite for Stss { } } -impl CodecBox for Stss { +impl CodecBox for Vmhd { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("EntryCount", 2, with_bit_width(32)), - codec_field!("SampleNumber", 3, with_bit_width(32), with_dynamic_length()), + codec_field!("Graphicsmode", 2, with_bit_width(16)), + codec_field!("Opcolor", 3, with_bit_width(16), with_length(3)), ]); const SUPPORTED_VERSIONS: &'static [u8] = &[0]; } -/// Sample size box. +/// Sound media header box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Stsz { +pub struct Smhd { full_box: FullBoxState, - pub sample_size: u32, - pub sample_count: u32, - pub entry_size: Vec, + pub balance: i16, } -impl FieldHooks for Stsz { - fn field_length(&self, name: &'static str) -> Option { +impl FieldHooks for Smhd { + fn display_field(&self, name: &'static str) -> Option { match name { - "EntrySize" => { - if self.sample_size == 0 { - Some(self.sample_count) - } else { - Some(0) - } - } + "Balance" => Some(format_fixed_8_8_signed(self.balance)), _ => None, } } - - fn display_field(&self, _name: &'static str) -> Option { - None - } } -impl ImmutableBox for Stsz { +impl ImmutableBox for Smhd { fn box_type(&self) -> FourCc { - FourCc::from_bytes(*b"stsz") + FourCc::from_bytes(*b"smhd") } fn version(&self) -> u8 { @@ -3215,7 +4164,7 @@ impl ImmutableBox for Stsz { } } -impl MutableBox for Stsz { +impl MutableBox for Smhd { fn set_version(&mut self, version: u8) { self.full_box.version = version; } @@ -3225,38 +4174,36 @@ impl MutableBox for Stsz { } } -impl FieldValueRead for Stsz { +impl Smhd { + /// Returns the balance as a signed 8.8 fixed-point value. + pub fn balance_value(&self) -> f32 { + f32::from(self.balance) / 256.0 + } + + /// Returns the integer component of the balance. + pub fn balance_int(&self) -> i8 { + (self.balance >> 8) as i8 + } +} + +impl FieldValueRead for Smhd { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "SampleSize" => Ok(FieldValue::Unsigned(u64::from(self.sample_size))), - "SampleCount" => Ok(FieldValue::Unsigned(u64::from(self.sample_count))), - "EntrySize" => Ok(FieldValue::UnsignedArray(self.entry_size.clone())), + "Balance" => Ok(FieldValue::Signed(i64::from(self.balance))), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Stsz { +impl FieldValueWrite for Smhd { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("SampleSize", FieldValue::Unsigned(value)) => { - self.sample_size = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("SampleCount", FieldValue::Unsigned(value)) => { - self.sample_count = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("EntrySize", FieldValue::UnsignedArray(values)) => { - let mut entry_size = Vec::with_capacity(values.len()); - for value in values { - entry_size.push(u64::from(u32_from_unsigned(field_name, value)?)); - } - self.entry_size = entry_size; + ("Balance", FieldValue::Signed(value)) => { + self.balance = i16_from_signed(field_name, value)?; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -3264,97 +4211,55 @@ impl FieldValueWrite for Stsz { } } -impl CodecBox for Stsz { +impl CodecBox for Smhd { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("SampleSize", 2, with_bit_width(32)), - codec_field!("SampleCount", 3, with_bit_width(32)), - codec_field!("EntrySize", 4, with_bit_width(32), with_dynamic_length()), + codec_field!("Balance", 2, with_bit_width(16), as_signed()), + codec_field!("Reserved", 3, with_bit_width(16), with_constant("0")), ]); const SUPPORTED_VERSIONS: &'static [u8] = &[0]; } -/// One time-to-sample entry. +/// Subtitle media header box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct SttsEntry { - pub sample_count: u32, - pub sample_delta: u32, +pub struct Sthd { + full_box: FullBoxState, } -/// Time to sample box. +impl_full_box!(Sthd, *b"sthd"); +empty_hooks!(Sthd); +empty_full_box_codec!(Sthd); + +/// Null media header box that carries no additional media-header fields. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Stts { +pub struct Nmhd { full_box: FullBoxState, - pub entry_count: u32, - pub entries: Vec, } -impl FieldHooks for Stts { - fn field_length(&self, name: &'static str) -> Option { - match name { - "Entries" => usize::try_from(self.entry_count) - .ok() - .and_then(|count| field_len_bytes(count, 8)), - _ => None, - } - } +impl_full_box!(Nmhd, *b"nmhd"); +empty_hooks!(Nmhd); +empty_full_box_codec!(Nmhd); - fn display_field(&self, name: &'static str) -> Option { - match name { - "Entries" => Some(render_array(self.entries.iter().map(|entry| { - format!( - "{{SampleCount={} SampleDelta={}}}", - entry.sample_count, entry.sample_delta - ) - }))), - _ => None, - } - } -} - -impl ImmutableBox for Stts { - fn box_type(&self) -> FourCc { - FourCc::from_bytes(*b"stts") - } - - fn version(&self) -> u8 { - self.full_box.version - } - - fn flags(&self) -> u32 { - self.full_box.flags - } +/// Sample description box. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Stsd { + full_box: FullBoxState, + pub entry_count: u32, } -impl MutableBox for Stts { - fn set_version(&mut self, version: u8) { - self.full_box.version = version; - } - - fn set_flags(&mut self, flags: u32) { - self.full_box.flags = flags; - } -} +impl_full_box!(Stsd, *b"stsd"); -impl FieldValueRead for Stts { +impl FieldValueRead for Stsd { fn field_value(&self, field_name: &'static str) -> Result { match field_name { "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), - "Entries" => { - let mut bytes = Vec::with_capacity(self.entries.len() * 8); - for entry in &self.entries { - bytes.extend_from_slice(&entry.sample_count.to_be_bytes()); - bytes.extend_from_slice(&entry.sample_delta.to_be_bytes()); - } - Ok(FieldValue::Bytes(bytes)) - } _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Stts { +impl FieldValueWrite for Stsd { fn set_field_value( &mut self, field_name: &'static str, @@ -3365,246 +4270,162 @@ impl FieldValueWrite for Stts { self.entry_count = u32_from_unsigned(field_name, value)?; Ok(()) } - ("Entries", FieldValue::Bytes(bytes)) => { - self.entries = parse_fixed_chunks(field_name, &bytes, 8, |chunk| SttsEntry { - sample_count: read_u32(chunk, 0), - sample_delta: read_u32(chunk, 4), - })?; - Ok(()) - } (field_name, value) => Err(unexpected_field(field_name, value)), } } } -impl CodecBox for Stts { +impl CodecBox for Stsd { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), codec_field!("EntryCount", 2, with_bit_width(32)), - codec_field!( - "Entries", - 3, - with_bit_width(8), - with_dynamic_length(), - as_bytes() - ), ]); const SUPPORTED_VERSIONS: &'static [u8] = &[0]; } -/// One track-run sample entry. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct TrunEntry { - pub sample_duration: u32, - pub sample_size: u32, - pub sample_flags: u32, - pub sample_composition_time_offset_v0: u32, - pub sample_composition_time_offset_v1: i32, -} - -/// Track run box. +/// Composition-to-decode timeline shift box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Trun { +pub struct Cslg { full_box: FullBoxState, - pub sample_count: u32, - pub data_offset: i32, - pub first_sample_flags: u32, - pub entries: Vec, + pub composition_to_dts_shift_v0: i32, + pub least_decode_to_display_delta_v0: i32, + pub greatest_decode_to_display_delta_v0: i32, + pub composition_start_time_v0: i32, + pub composition_end_time_v0: i32, + pub composition_to_dts_shift_v1: i64, + pub least_decode_to_display_delta_v1: i64, + pub greatest_decode_to_display_delta_v1: i64, + pub composition_start_time_v1: i64, + pub composition_end_time_v1: i64, } -impl FieldHooks for Trun { - fn field_length(&self, name: &'static str) -> Option { - match name { - "Entries" => { - let mut bytes_per_entry = 0usize; - if self.flags() & TRUN_SAMPLE_DURATION_PRESENT != 0 { - bytes_per_entry += 4; - } - if self.flags() & TRUN_SAMPLE_SIZE_PRESENT != 0 { - bytes_per_entry += 4; - } - if self.flags() & TRUN_SAMPLE_FLAGS_PRESENT != 0 { - bytes_per_entry += 4; - } - if self.flags() & TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT != 0 { - bytes_per_entry += 4; - } - usize::try_from(self.sample_count) - .ok() - .and_then(|count| field_len_bytes(count, bytes_per_entry)) - } - _ => None, - } - } +impl_full_box!(Cslg, *b"cslg"); - fn display_field(&self, name: &'static str) -> Option { - match name { - "Entries" => Some(render_array(self.entries.iter().map(|entry| { - let mut fields = Vec::new(); - if self.flags() & TRUN_SAMPLE_DURATION_PRESENT != 0 { - fields.push(format!("SampleDuration={}", entry.sample_duration)); - } - if self.flags() & TRUN_SAMPLE_SIZE_PRESENT != 0 { - fields.push(format!("SampleSize={}", entry.sample_size)); - } - if self.flags() & TRUN_SAMPLE_FLAGS_PRESENT != 0 { - fields.push(format!("SampleFlags=0x{:x}", entry.sample_flags)); - } - if self.flags() & TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT != 0 { - if self.version() == 0 { - fields.push(format!( - "SampleCompositionTimeOffsetV0={}", - entry.sample_composition_time_offset_v0 - )); - } else { - fields.push(format!( - "SampleCompositionTimeOffsetV1={}", - entry.sample_composition_time_offset_v1 - )); - } - } - format!("{{{}}}", fields.join(" ")) - }))), - _ => None, +impl Cslg { + /// Returns the active composition-to-decode shift for the current box version. + pub fn composition_to_dts_shift(&self) -> i64 { + match self.version() { + 0 => i64::from(self.composition_to_dts_shift_v0), + 1 => self.composition_to_dts_shift_v1, + _ => 0, } } -} - -impl ImmutableBox for Trun { - fn box_type(&self) -> FourCc { - FourCc::from_bytes(*b"trun") - } - - fn version(&self) -> u8 { - self.full_box.version - } - fn flags(&self) -> u32 { - self.full_box.flags + /// Returns the active least decode-to-display delta for the current box version. + pub fn least_decode_to_display_delta(&self) -> i64 { + match self.version() { + 0 => i64::from(self.least_decode_to_display_delta_v0), + 1 => self.least_decode_to_display_delta_v1, + _ => 0, + } } -} -impl MutableBox for Trun { - fn set_version(&mut self, version: u8) { - self.full_box.version = version; + /// Returns the active greatest decode-to-display delta for the current box version. + pub fn greatest_decode_to_display_delta(&self) -> i64 { + match self.version() { + 0 => i64::from(self.greatest_decode_to_display_delta_v0), + 1 => self.greatest_decode_to_display_delta_v1, + _ => 0, + } } - fn set_flags(&mut self, flags: u32) { - self.full_box.flags = flags; + /// Returns the active composition start time for the current box version. + pub fn composition_start_time(&self) -> i64 { + match self.version() { + 0 => i64::from(self.composition_start_time_v0), + 1 => self.composition_start_time_v1, + _ => 0, + } } -} -impl Trun { - /// Returns the active composition time offset for `index`. - pub fn sample_composition_time_offset(&self, index: usize) -> i64 { + /// Returns the active composition end time for the current box version. + pub fn composition_end_time(&self) -> i64 { match self.version() { - 0 => i64::from(self.entries[index].sample_composition_time_offset_v0), - 1 => i64::from(self.entries[index].sample_composition_time_offset_v1), + 0 => i64::from(self.composition_end_time_v0), + 1 => self.composition_end_time_v1, _ => 0, } } } -impl FieldValueRead for Trun { +impl FieldValueRead for Cslg { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "SampleCount" => Ok(FieldValue::Unsigned(u64::from(self.sample_count))), - "DataOffset" => Ok(FieldValue::Signed(i64::from(self.data_offset))), - "FirstSampleFlags" => Ok(FieldValue::Unsigned(u64::from(self.first_sample_flags))), - "Entries" => { - let mut bytes = Vec::new(); - for entry in &self.entries { - if self.flags() & TRUN_SAMPLE_DURATION_PRESENT != 0 { - bytes.extend_from_slice(&entry.sample_duration.to_be_bytes()); - } - if self.flags() & TRUN_SAMPLE_SIZE_PRESENT != 0 { - bytes.extend_from_slice(&entry.sample_size.to_be_bytes()); - } - if self.flags() & TRUN_SAMPLE_FLAGS_PRESENT != 0 { - bytes.extend_from_slice(&entry.sample_flags.to_be_bytes()); - } - if self.flags() & TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT != 0 { - if self.version() == 0 { - bytes.extend_from_slice( - &entry.sample_composition_time_offset_v0.to_be_bytes(), - ); - } else { - bytes.extend_from_slice( - &entry.sample_composition_time_offset_v1.to_be_bytes(), - ); - } - } - } - Ok(FieldValue::Bytes(bytes)) + "CompositionToDTSShiftV0" => Ok(FieldValue::Signed(i64::from( + self.composition_to_dts_shift_v0, + ))), + "LeastDecodeToDisplayDeltaV0" => Ok(FieldValue::Signed(i64::from( + self.least_decode_to_display_delta_v0, + ))), + "GreatestDecodeToDisplayDeltaV0" => Ok(FieldValue::Signed(i64::from( + self.greatest_decode_to_display_delta_v0, + ))), + "CompositionStartTimeV0" => Ok(FieldValue::Signed(i64::from( + self.composition_start_time_v0, + ))), + "CompositionEndTimeV0" => { + Ok(FieldValue::Signed(i64::from(self.composition_end_time_v0))) } - _ => Err(missing_field(field_name)), - } - } -} - -impl FieldValueWrite for Trun { + "CompositionToDTSShiftV1" => Ok(FieldValue::Signed(self.composition_to_dts_shift_v1)), + "LeastDecodeToDisplayDeltaV1" => { + Ok(FieldValue::Signed(self.least_decode_to_display_delta_v1)) + } + "GreatestDecodeToDisplayDeltaV1" => { + Ok(FieldValue::Signed(self.greatest_decode_to_display_delta_v1)) + } + "CompositionStartTimeV1" => Ok(FieldValue::Signed(self.composition_start_time_v1)), + "CompositionEndTimeV1" => Ok(FieldValue::Signed(self.composition_end_time_v1)), + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Cslg { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("SampleCount", FieldValue::Unsigned(value)) => { - self.sample_count = u32_from_unsigned(field_name, value)?; + ("CompositionToDTSShiftV0", FieldValue::Signed(value)) => { + self.composition_to_dts_shift_v0 = i32_from_signed(field_name, value)?; Ok(()) } - ("DataOffset", FieldValue::Signed(value)) => { - self.data_offset = i32_from_signed(field_name, value)?; + ("LeastDecodeToDisplayDeltaV0", FieldValue::Signed(value)) => { + self.least_decode_to_display_delta_v0 = i32_from_signed(field_name, value)?; Ok(()) } - ("FirstSampleFlags", FieldValue::Unsigned(value)) => { - self.first_sample_flags = u32_from_unsigned(field_name, value)?; + ("GreatestDecodeToDisplayDeltaV0", FieldValue::Signed(value)) => { + self.greatest_decode_to_display_delta_v0 = i32_from_signed(field_name, value)?; Ok(()) } - ("Entries", FieldValue::Bytes(bytes)) => { - let mut bytes_per_entry = 0usize; - if self.flags() & TRUN_SAMPLE_DURATION_PRESENT != 0 { - bytes_per_entry += 4; - } - if self.flags() & TRUN_SAMPLE_SIZE_PRESENT != 0 { - bytes_per_entry += 4; - } - if self.flags() & TRUN_SAMPLE_FLAGS_PRESENT != 0 { - bytes_per_entry += 4; - } - if self.flags() & TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT != 0 { - bytes_per_entry += 4; - } - - self.entries = if bytes_per_entry == 0 { - Vec::new() - } else { - parse_fixed_chunks(field_name, &bytes, bytes_per_entry, |chunk| { - let mut offset = 0; - let mut entry = TrunEntry::default(); - if self.flags() & TRUN_SAMPLE_DURATION_PRESENT != 0 { - entry.sample_duration = read_u32(chunk, offset); - offset += 4; - } - if self.flags() & TRUN_SAMPLE_SIZE_PRESENT != 0 { - entry.sample_size = read_u32(chunk, offset); - offset += 4; - } - if self.flags() & TRUN_SAMPLE_FLAGS_PRESENT != 0 { - entry.sample_flags = read_u32(chunk, offset); - offset += 4; - } - if self.flags() & TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT != 0 { - if self.version() == 0 { - entry.sample_composition_time_offset_v0 = read_u32(chunk, offset); - } else { - entry.sample_composition_time_offset_v1 = read_i32(chunk, offset); - } - } - entry - })? - }; + ("CompositionStartTimeV0", FieldValue::Signed(value)) => { + self.composition_start_time_v0 = i32_from_signed(field_name, value)?; + Ok(()) + } + ("CompositionEndTimeV0", FieldValue::Signed(value)) => { + self.composition_end_time_v0 = i32_from_signed(field_name, value)?; + Ok(()) + } + ("CompositionToDTSShiftV1", FieldValue::Signed(value)) => { + self.composition_to_dts_shift_v1 = i64_from_signed(field_name, value)?; + Ok(()) + } + ("LeastDecodeToDisplayDeltaV1", FieldValue::Signed(value)) => { + self.least_decode_to_display_delta_v1 = i64_from_signed(field_name, value)?; + Ok(()) + } + ("GreatestDecodeToDisplayDeltaV1", FieldValue::Signed(value)) => { + self.greatest_decode_to_display_delta_v1 = i64_from_signed(field_name, value)?; + Ok(()) + } + ("CompositionStartTimeV1", FieldValue::Signed(value)) => { + self.composition_start_time_v1 = i64_from_signed(field_name, value)?; + Ok(()) + } + ("CompositionEndTimeV1", FieldValue::Signed(value)) => { + self.composition_end_time_v1 = i64_from_signed(field_name, value)?; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -3612,59 +4433,133 @@ impl FieldValueWrite for Trun { } } -impl CodecBox for Trun { +impl CodecBox for Cslg { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("SampleCount", 2, with_bit_width(32)), codec_field!( - "DataOffset", + "CompositionToDTSShiftV0", + 2, + with_bit_width(32), + with_version(0), + as_signed() + ), + codec_field!( + "LeastDecodeToDisplayDeltaV0", 3, with_bit_width(32), - as_signed(), - with_required_flags(TRUN_DATA_OFFSET_PRESENT) + with_version(0), + as_signed() ), codec_field!( - "FirstSampleFlags", + "GreatestDecodeToDisplayDeltaV0", 4, with_bit_width(32), - with_required_flags(TRUN_FIRST_SAMPLE_FLAGS_PRESENT), - as_hex() + with_version(0), + as_signed() ), codec_field!( - "Entries", + "CompositionStartTimeV0", 5, - with_bit_width(8), - with_dynamic_length(), - as_bytes() + with_bit_width(32), + with_version(0), + as_signed() + ), + codec_field!( + "CompositionEndTimeV0", + 6, + with_bit_width(32), + with_version(0), + as_signed() + ), + codec_field!( + "CompositionToDTSShiftV1", + 7, + with_bit_width(64), + with_version(1), + as_signed() + ), + codec_field!( + "LeastDecodeToDisplayDeltaV1", + 8, + with_bit_width(64), + with_version(1), + as_signed() + ), + codec_field!( + "GreatestDecodeToDisplayDeltaV1", + 9, + with_bit_width(64), + with_version(1), + as_signed() + ), + codec_field!( + "CompositionStartTimeV1", + 10, + with_bit_width(64), + with_version(1), + as_signed() + ), + codec_field!( + "CompositionEndTimeV1", + 11, + with_bit_width(64), + with_version(1), + as_signed() ), ]); const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; } -simple_container_box!(Schi, *b"schi"); -simple_container_box!(Sinf, *b"sinf"); -simple_container_box!(Wave, *b"wave"); +/// One composition-offset run. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct CttsEntry { + pub sample_count: u32, + pub sample_offset_v0: u32, + pub sample_offset_v1: i32, +} -/// Metadata box. +/// Composition time to sample box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Meta { +pub struct Ctts { full_box: FullBoxState, - quicktime_headerless: bool, + pub entry_count: u32, + pub entries: Vec, } -impl FieldHooks for Meta { - fn field_enabled(&self, name: &'static str) -> Option { +impl FieldHooks for Ctts { + fn field_length(&self, name: &'static str) -> Option { match name { - "Version" | "Flags" => Some(!self.quicktime_headerless), + "Entries" => usize::try_from(self.entry_count) + .ok() + .and_then(|count| field_len_bytes(count, 8)), + _ => None, + } + } + + fn display_field(&self, name: &'static str) -> Option { + match name { + "Entries" => Some(render_array(self.entries.iter().map( + |entry| match self.version() { + 0 => format!( + "{{SampleCount={} SampleOffsetV0={}}}", + entry.sample_count, entry.sample_offset_v0 + ), + 1 => format!( + "{{SampleCount={} SampleOffsetV1={}}}", + entry.sample_count, entry.sample_offset_v1 + ), + _ => String::from("{}"), + }, + ))), _ => None, } } } -impl ImmutableBox for Meta { +impl ImmutableBox for Ctts { fn box_type(&self) -> FourCc { - FourCc::from_bytes(*b"meta") + FourCc::from_bytes(*b"ctts") } fn version(&self) -> u8 { @@ -3676,179 +4571,263 @@ impl ImmutableBox for Meta { } } -impl MutableBox for Meta { +impl MutableBox for Ctts { fn set_version(&mut self, version: u8) { - self.quicktime_headerless = false; self.full_box.version = version; } fn set_flags(&mut self, flags: u32) { - self.quicktime_headerless = false; self.full_box.flags = flags; } +} - fn before_unmarshal( - &mut self, - reader: &mut dyn ReadSeek, - payload_size: u64, - ) -> Result<(), CodecError> { - self.quicktime_headerless = false; - if payload_size < 4 { - return Ok(()); - } - - // Headerless metadata starts directly with the first child box type instead of the full-box prefix. - let start = reader.stream_position()?; - let mut prefix = [0_u8; 4]; - reader.read_exact(&mut prefix)?; - reader.seek(SeekFrom::Start(start))?; - - if prefix.iter().any(|byte| *byte != 0) { - self.quicktime_headerless = true; - self.full_box.version = 0; - self.full_box.flags = 0; +impl Ctts { + /// Returns the active sample offset for `index`. + pub fn sample_offset(&self, index: usize) -> i64 { + match self.version() { + 0 => i64::from(self.entries[index].sample_offset_v0), + 1 => i64::from(self.entries[index].sample_offset_v1), + _ => 0, } - - Ok(()) } } -impl Meta { - /// Returns `true` when the payload omits the normal full-box header bytes. - pub fn is_quicktime_headerless(&self) -> bool { - self.quicktime_headerless - } -} - -impl FieldValueRead for Meta { +impl FieldValueRead for Ctts { fn field_value(&self, field_name: &'static str) -> Result { - Err(missing_field(field_name)) + match field_name { + "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), + "Entries" => { + let mut bytes = Vec::with_capacity(self.entries.len() * 8); + for entry in &self.entries { + bytes.extend_from_slice(&entry.sample_count.to_be_bytes()); + match self.version() { + 0 => bytes.extend_from_slice(&entry.sample_offset_v0.to_be_bytes()), + 1 => bytes.extend_from_slice(&entry.sample_offset_v1.to_be_bytes()), + _ => {} + } + } + Ok(FieldValue::Bytes(bytes)) + } + _ => Err(missing_field(field_name)), + } } } -impl FieldValueWrite for Meta { +impl FieldValueWrite for Ctts { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { - Err(unexpected_field(field_name, value)) + match (field_name, value) { + ("EntryCount", FieldValue::Unsigned(value)) => { + self.entry_count = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("Entries", FieldValue::Bytes(bytes)) => { + self.entries = + parse_fixed_chunks(field_name, &bytes, 8, |chunk| match self.version() { + 0 => CttsEntry { + sample_count: read_u32(chunk, 0), + sample_offset_v0: read_u32(chunk, 4), + ..CttsEntry::default() + }, + 1 => CttsEntry { + sample_count: read_u32(chunk, 0), + sample_offset_v1: read_i32(chunk, 4), + ..CttsEntry::default() + }, + _ => CttsEntry::default(), + })?; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } } } -impl CodecBox for Meta { +impl CodecBox for Ctts { const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("EntryCount", 2, with_bit_width(32)), codec_field!( - "Version", - 0, + "Entries", + 3, with_bit_width(8), - as_version_field(), - with_dynamic_presence() - ), - codec_field!( - "Flags", - 1, - with_bit_width(24), - as_flags_field(), - with_dynamic_presence() + with_dynamic_length(), + as_bytes() ), ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0]; + const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; } -/// Handler reference box. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Hdlr { - full_box: FullBoxState, - pub pre_defined: u32, - pub handler_type: FourCc, - pub reserved: [u8; 12], - pub name: String, +/// One edit-list entry. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ElstEntry { + pub segment_duration_v0: u32, + pub media_time_v0: i32, + pub segment_duration_v1: u64, + pub media_time_v1: i64, + pub media_rate_integer: i16, } -impl Default for Hdlr { - fn default() -> Self { - Self { - full_box: FullBoxState::default(), - pre_defined: 0, - handler_type: FourCc::ANY, - reserved: [0; 12], - name: String::new(), - } - } +/// Edit list box. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Elst { + full_box: FullBoxState, + pub entry_count: u32, + pub entries: Vec, } -impl FieldHooks for Hdlr { - fn is_pascal_string( - &self, - name: &'static str, - _data: &[u8], - remaining_bytes: u64, - ) -> Option { +impl FieldHooks for Elst { + fn field_length(&self, name: &'static str) -> Option { match name { - // Some files store the handler name as a Pascal string and consume the last payload byte with the length prefix. - "Name" => Some(self.pre_defined != 0 && remaining_bytes == 0), + "Entries" => match self.version() { + 0 => usize::try_from(self.entry_count) + .ok() + .and_then(|count| field_len_bytes(count, 12)), + 1 => usize::try_from(self.entry_count) + .ok() + .and_then(|count| field_len_bytes(count, 20)), + _ => Some(0), + }, _ => None, } } - fn consume_remaining_bytes_after_string(&self, name: &'static str) -> Option { + fn display_field(&self, name: &'static str) -> Option { match name { - // Handler names may be padded after the visible terminator, so keep consuming the declared field payload. - "Name" => Some(true), + "Entries" => Some(render_array(self.entries.iter().map( + |entry| match self.version() { + 0 => format!( + "{{SegmentDurationV0={} MediaTimeV0={} MediaRateInteger={}}}", + entry.segment_duration_v0, entry.media_time_v0, entry.media_rate_integer + ), + 1 => format!( + "{{SegmentDurationV1={} MediaTimeV1={} MediaRateInteger={}}}", + entry.segment_duration_v1, entry.media_time_v1, entry.media_rate_integer + ), + _ => String::from("{}"), + }, + ))), _ => None, } } +} - fn display_field(&self, name: &'static str) -> Option { - match name { - "HandlerType" => Some(quoted_fourcc(self.handler_type)), - _ => None, - } +impl ImmutableBox for Elst { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"elst") + } + + fn version(&self) -> u8 { + self.full_box.version + } + + fn flags(&self) -> u32 { + self.full_box.flags } } -impl_full_box!(Hdlr, *b"hdlr"); +impl MutableBox for Elst { + fn set_version(&mut self, version: u8) { + self.full_box.version = version; + } -impl FieldValueRead for Hdlr { + fn set_flags(&mut self, flags: u32) { + self.full_box.flags = flags; + } +} + +impl Elst { + /// Returns the active segment duration for `index`. + pub fn segment_duration(&self, index: usize) -> u64 { + match self.version() { + 0 => u64::from(self.entries[index].segment_duration_v0), + 1 => self.entries[index].segment_duration_v1, + _ => 0, + } + } + + /// Returns the active media time for `index`. + pub fn media_time(&self, index: usize) -> i64 { + match self.version() { + 0 => i64::from(self.entries[index].media_time_v0), + 1 => self.entries[index].media_time_v1, + _ => 0, + } + } +} + +impl FieldValueRead for Elst { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "PreDefined" => Ok(FieldValue::Unsigned(u64::from(self.pre_defined))), - "HandlerType" => Ok(FieldValue::Bytes(self.handler_type.as_bytes().to_vec())), - "Reserved" => Ok(FieldValue::Bytes(self.reserved.to_vec())), - "Name" => Ok(FieldValue::String(self.name.clone())), + "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), + "Entries" => { + let mut bytes = Vec::new(); + for entry in &self.entries { + match self.version() { + 0 => { + bytes.extend_from_slice(&entry.segment_duration_v0.to_be_bytes()); + bytes.extend_from_slice(&entry.media_time_v0.to_be_bytes()); + } + 1 => { + bytes.extend_from_slice(&entry.segment_duration_v1.to_be_bytes()); + bytes.extend_from_slice(&entry.media_time_v1.to_be_bytes()); + } + _ => {} + } + bytes.extend_from_slice(&entry.media_rate_integer.to_be_bytes()); + bytes.extend_from_slice(&0_i16.to_be_bytes()); + } + Ok(FieldValue::Bytes(bytes)) + } _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Hdlr { +impl FieldValueWrite for Elst { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("PreDefined", FieldValue::Unsigned(value)) => { - self.pre_defined = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("HandlerType", FieldValue::Bytes(bytes)) => { - self.handler_type = bytes_to_fourcc(field_name, bytes)?; + ("EntryCount", FieldValue::Unsigned(value)) => { + self.entry_count = u32_from_unsigned(field_name, value)?; Ok(()) } - ("Reserved", FieldValue::Bytes(bytes)) => { - if bytes.len() != 12 { - return Err(invalid_value( - field_name, - "value must contain exactly 12 bytes", - )); + ("Entries", FieldValue::Bytes(bytes)) => { + self.entries = match self.version() { + 0 => parse_fixed_chunks(field_name, &bytes, 12, |chunk| ElstEntry { + segment_duration_v0: read_u32(chunk, 0), + media_time_v0: read_i32(chunk, 4), + media_rate_integer: read_i16(chunk, 8), + ..ElstEntry::default() + })?, + 1 => parse_fixed_chunks(field_name, &bytes, 20, |chunk| ElstEntry { + segment_duration_v1: read_u64(chunk, 0), + media_time_v1: read_i64(chunk, 8), + media_rate_integer: read_i16(chunk, 16), + ..ElstEntry::default() + })?, + _ => Vec::new(), + }; + for chunk in bytes.chunks_exact(match self.version() { + 0 => 12, + 1 => 20, + _ => 1, + }) { + let offset = chunk.len() - 2; + if read_i16(chunk, offset) != 0 { + return Err(invalid_value( + field_name, + "media rate fraction must be zero", + )); + } } - self.reserved.copy_from_slice(&bytes); - Ok(()) - } - ("Name", FieldValue::String(value)) => { - self.name = value; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -3856,133 +4835,64 @@ impl FieldValueWrite for Hdlr { } } -impl CodecBox for Hdlr { +impl CodecBox for Elst { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("PreDefined", 2, with_bit_width(32)), + codec_field!("EntryCount", 2, with_bit_width(32)), codec_field!( - "HandlerType", + "Entries", 3, with_bit_width(8), - with_length(4), + with_dynamic_length(), as_bytes() ), - codec_field!( - "Reserved", - 4, - with_bit_width(8), - with_length(12), - as_bytes(), - as_hidden() - ), - codec_field!( - "Name", - 5, - with_bit_width(8), - as_string(StringFieldMode::PascalCompatible) - ), ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0]; + const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; } -/// Auxiliary information offsets box. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Saio { +/// 64-bit chunk offset box. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Co64 { full_box: FullBoxState, - pub aux_info_type: FourCc, - pub aux_info_type_parameter: u32, pub entry_count: u32, - pub offset_v0: Vec, - pub offset_v1: Vec, + pub chunk_offset: Vec, } -impl Default for Saio { - fn default() -> Self { - Self { - full_box: FullBoxState::default(), - aux_info_type: FourCc::ANY, - aux_info_type_parameter: 0, - entry_count: 0, - offset_v0: Vec::new(), - offset_v1: Vec::new(), - } - } -} +impl_full_box!(Co64, *b"co64"); -impl FieldHooks for Saio { +impl FieldHooks for Co64 { fn field_length(&self, name: &'static str) -> Option { match name { - "OffsetV0" | "OffsetV1" => Some(self.entry_count), - _ => None, - } - } - - fn display_field(&self, name: &'static str) -> Option { - match name { - "AuxInfoType" => Some(quoted_fourcc(self.aux_info_type)), + "ChunkOffset" => Some(self.entry_count), _ => None, } } } -impl_full_box!(Saio, *b"saio"); - -impl Saio { - /// Returns the active auxiliary information offset at `index`. - pub fn offset(&self, index: usize) -> u64 { - match self.version() { - 0 => self.offset_v0[index], - 1 => self.offset_v1[index], - _ => 0, - } - } -} - -impl FieldValueRead for Saio { +impl FieldValueRead for Co64 { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "AuxInfoType" => Ok(FieldValue::Bytes(self.aux_info_type.as_bytes().to_vec())), - "AuxInfoTypeParameter" => Ok(FieldValue::Unsigned(u64::from( - self.aux_info_type_parameter, - ))), "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), - "OffsetV0" => Ok(FieldValue::UnsignedArray(self.offset_v0.clone())), - "OffsetV1" => Ok(FieldValue::UnsignedArray(self.offset_v1.clone())), + "ChunkOffset" => Ok(FieldValue::UnsignedArray(self.chunk_offset.clone())), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Saio { +impl FieldValueWrite for Co64 { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("AuxInfoType", FieldValue::Bytes(bytes)) => { - self.aux_info_type = bytes_to_fourcc(field_name, bytes)?; - Ok(()) - } - ("AuxInfoTypeParameter", FieldValue::Unsigned(value)) => { - self.aux_info_type_parameter = u32_from_unsigned(field_name, value)?; - Ok(()) - } ("EntryCount", FieldValue::Unsigned(value)) => { self.entry_count = u32_from_unsigned(field_name, value)?; Ok(()) } - ("OffsetV0", FieldValue::UnsignedArray(values)) => { - let mut offsets = Vec::with_capacity(values.len()); - for value in values { - offsets.push(u64::from(u32_from_unsigned(field_name, value)?)); - } - self.offset_v0 = offsets; - Ok(()) - } - ("OffsetV1", FieldValue::UnsignedArray(values)) => { - self.offset_v1 = values; + ("ChunkOffset", FieldValue::UnsignedArray(values)) => { + self.chunk_offset = values; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -3990,145 +4900,62 @@ impl FieldValueWrite for Saio { } } -impl CodecBox for Saio { +impl CodecBox for Co64 { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!( - "AuxInfoType", - 2, - with_bit_width(8), - with_length(4), - as_bytes(), - with_required_flags(AUX_INFO_TYPE_PRESENT) - ), - codec_field!( - "AuxInfoTypeParameter", - 3, - with_bit_width(32), - as_hex(), - with_required_flags(AUX_INFO_TYPE_PRESENT) - ), - codec_field!("EntryCount", 4, with_bit_width(32)), - codec_field!( - "OffsetV0", - 5, - with_bit_width(32), - with_dynamic_length(), - with_version(0) - ), - codec_field!( - "OffsetV1", - 6, - with_bit_width(64), - with_dynamic_length(), - with_version(1) - ), + codec_field!("EntryCount", 2, with_bit_width(32)), + codec_field!("ChunkOffset", 3, with_bit_width(64), with_dynamic_length()), ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; } -/// Auxiliary information sizes box. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Saiz { +/// 32-bit chunk offset box. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Stco { full_box: FullBoxState, - pub aux_info_type: FourCc, - pub aux_info_type_parameter: u32, - pub default_sample_info_size: u8, - pub sample_count: u32, - pub sample_info_size: Vec, + pub entry_count: u32, + pub chunk_offset: Vec, } -impl Default for Saiz { - fn default() -> Self { - Self { - full_box: FullBoxState::default(), - aux_info_type: FourCc::ANY, - aux_info_type_parameter: 0, - default_sample_info_size: 0, - sample_count: 0, - sample_info_size: Vec::new(), - } - } -} +impl_full_box!(Stco, *b"stco"); -impl FieldHooks for Saiz { +impl FieldHooks for Stco { fn field_length(&self, name: &'static str) -> Option { match name { - "SampleInfoSize" => Some(self.sample_count), - _ => None, - } - } - - fn field_enabled(&self, name: &'static str) -> Option { - match name { - "SampleInfoSize" => Some(self.default_sample_info_size == 0), - _ => None, - } - } - - fn display_field(&self, name: &'static str) -> Option { - match name { - "AuxInfoType" => Some(quoted_fourcc(self.aux_info_type)), + "ChunkOffset" => Some(self.entry_count), _ => None, } } } -impl_full_box!(Saiz, *b"saiz"); - -impl FieldValueRead for Saiz { +impl FieldValueRead for Stco { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "AuxInfoType" => Ok(FieldValue::Bytes(self.aux_info_type.as_bytes().to_vec())), - "AuxInfoTypeParameter" => Ok(FieldValue::Unsigned(u64::from( - self.aux_info_type_parameter, - ))), - "DefaultSampleInfoSize" => Ok(FieldValue::Unsigned(u64::from( - self.default_sample_info_size, - ))), - "SampleCount" => Ok(FieldValue::Unsigned(u64::from(self.sample_count))), - "SampleInfoSize" => Ok(FieldValue::UnsignedArray( - self.sample_info_size - .iter() - .copied() - .map(u64::from) - .collect(), - )), + "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), + "ChunkOffset" => Ok(FieldValue::UnsignedArray(self.chunk_offset.clone())), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Saiz { +impl FieldValueWrite for Stco { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("AuxInfoType", FieldValue::Bytes(bytes)) => { - self.aux_info_type = bytes_to_fourcc(field_name, bytes)?; - Ok(()) - } - ("AuxInfoTypeParameter", FieldValue::Unsigned(value)) => { - self.aux_info_type_parameter = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("DefaultSampleInfoSize", FieldValue::Unsigned(value)) => { - self.default_sample_info_size = u8_from_unsigned(field_name, value)?; - Ok(()) - } - ("SampleCount", FieldValue::Unsigned(value)) => { - self.sample_count = u32_from_unsigned(field_name, value)?; + ("EntryCount", FieldValue::Unsigned(value)) => { + self.entry_count = u32_from_unsigned(field_name, value)?; Ok(()) } - ("SampleInfoSize", FieldValue::UnsignedArray(values)) => { - let mut sizes = Vec::with_capacity(values.len()); + ("ChunkOffset", FieldValue::UnsignedArray(values)) => { + let mut offsets = Vec::with_capacity(values.len()); for value in values { - sizes.push(u8_from_unsigned(field_name, value)?); + offsets.push(u64::from(u32_from_unsigned(field_name, value)?)); } - self.sample_info_size = sizes; + self.chunk_offset = offsets; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -4136,62 +4963,48 @@ impl FieldValueWrite for Saiz { } } -impl CodecBox for Saiz { +impl CodecBox for Stco { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!( - "AuxInfoType", - 2, - with_bit_width(8), - with_length(4), - as_bytes(), - with_required_flags(AUX_INFO_TYPE_PRESENT) - ), - codec_field!( - "AuxInfoTypeParameter", - 3, - with_bit_width(32), - as_hex(), - with_required_flags(AUX_INFO_TYPE_PRESENT) - ), - codec_field!("DefaultSampleInfoSize", 4, with_bit_width(8)), - codec_field!("SampleCount", 5, with_bit_width(32)), - codec_field!( - "SampleInfoSize", - 6, - with_bit_width(8), - with_dynamic_length(), - with_dynamic_presence() - ), + codec_field!("EntryCount", 2, with_bit_width(32)), + codec_field!("ChunkOffset", 3, with_bit_width(32), with_dynamic_length()), ]); const SUPPORTED_VERSIONS: &'static [u8] = &[0]; } -/// One sample-to-group entry. +/// One sample-to-chunk entry. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct SbgpEntry { - pub sample_count: u32, - pub group_description_index: u32, +pub struct StscEntry { + pub first_chunk: u32, + pub samples_per_chunk: u32, + pub sample_description_index: u32, } -/// Sample-to-group box. +/// Sample-to-chunk box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Sbgp { +pub struct Stsc { full_box: FullBoxState, - pub grouping_type: u32, - pub grouping_type_parameter: u32, pub entry_count: u32, - pub entries: Vec, + pub entries: Vec, } -impl FieldHooks for Sbgp { +impl FieldHooks for Stsc { + fn field_length(&self, name: &'static str) -> Option { + match name { + "Entries" => usize::try_from(self.entry_count) + .ok() + .and_then(|count| field_len_bytes(count, 12)), + _ => None, + } + } + fn display_field(&self, name: &'static str) -> Option { match name { "Entries" => Some(render_array(self.entries.iter().map(|entry| { format!( - "{{SampleCount={} GroupDescriptionIndex={}}}", - entry.sample_count, entry.group_description_index + "{{FirstChunk={} SamplesPerChunk={} SampleDescriptionIndex={}}}", + entry.first_chunk, entry.samples_per_chunk, entry.sample_description_index ) }))), _ => None, @@ -4199,21 +5012,40 @@ impl FieldHooks for Sbgp { } } -impl_full_box!(Sbgp, *b"sbgp"); +impl ImmutableBox for Stsc { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"stsc") + } -impl FieldValueRead for Sbgp { + fn version(&self) -> u8 { + self.full_box.version + } + + fn flags(&self) -> u32 { + self.full_box.flags + } +} + +impl MutableBox for Stsc { + fn set_version(&mut self, version: u8) { + self.full_box.version = version; + } + + fn set_flags(&mut self, flags: u32) { + self.full_box.flags = flags; + } +} + +impl FieldValueRead for Stsc { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "GroupingType" => Ok(FieldValue::Unsigned(u64::from(self.grouping_type))), - "GroupingTypeParameter" => Ok(FieldValue::Unsigned(u64::from( - self.grouping_type_parameter, - ))), "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), "Entries" => { - let mut bytes = Vec::with_capacity(self.entries.len() * 8); + let mut bytes = Vec::with_capacity(self.entries.len() * 12); for entry in &self.entries { - bytes.extend_from_slice(&entry.sample_count.to_be_bytes()); - bytes.extend_from_slice(&entry.group_description_index.to_be_bytes()); + bytes.extend_from_slice(&entry.first_chunk.to_be_bytes()); + bytes.extend_from_slice(&entry.samples_per_chunk.to_be_bytes()); + bytes.extend_from_slice(&entry.sample_description_index.to_be_bytes()); } Ok(FieldValue::Bytes(bytes)) } @@ -4222,39 +5054,22 @@ impl FieldValueRead for Sbgp { } } -impl FieldValueWrite for Sbgp { +impl FieldValueWrite for Stsc { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("GroupingType", FieldValue::Unsigned(value)) => { - self.grouping_type = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("GroupingTypeParameter", FieldValue::Unsigned(value)) => { - self.grouping_type_parameter = u32_from_unsigned(field_name, value)?; - Ok(()) - } ("EntryCount", FieldValue::Unsigned(value)) => { self.entry_count = u32_from_unsigned(field_name, value)?; Ok(()) } ("Entries", FieldValue::Bytes(bytes)) => { - let expected_len = field_len_from_count(self.entry_count, 8) - .map(|len| len as usize) - .unwrap_or(0); - if bytes.len() != expected_len { - return Err(invalid_value( - field_name, - "entry payload length does not match the entry count", - )); - } - - self.entries = parse_fixed_chunks(field_name, &bytes, 8, |chunk| SbgpEntry { - sample_count: read_u32(chunk, 0), - group_description_index: read_u32(chunk, 4), + self.entries = parse_fixed_chunks(field_name, &bytes, 12, |chunk| StscEntry { + first_chunk: read_u32(chunk, 0), + samples_per_chunk: read_u32(chunk, 4), + sample_description_index: read_u32(chunk, 8), })?; Ok(()) } @@ -4263,111 +5078,68 @@ impl FieldValueWrite for Sbgp { } } -impl CodecBox for Sbgp { +impl CodecBox for Stsc { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("GroupingType", 2, with_bit_width(32)), + codec_field!("EntryCount", 2, with_bit_width(32)), codec_field!( - "GroupingTypeParameter", + "Entries", 3, - with_bit_width(32), - with_version(1) + with_bit_width(8), + with_dynamic_length(), + as_bytes() ), - codec_field!("EntryCount", 4, with_bit_width(32)), - codec_field!("Entries", 5, with_bit_width(8), as_bytes()), ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; -} - -/// One packed sample dependency entry. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct SdtpSampleElem { - pub is_leading: u8, - pub sample_depends_on: u8, - pub sample_is_depended_on: u8, - pub sample_has_redundancy: u8, -} - -fn encode_sdtp_sample( - field_name: &'static str, - sample: &SdtpSampleElem, -) -> Result { - if sample.is_leading > 0x03 - || sample.sample_depends_on > 0x03 - || sample.sample_is_depended_on > 0x03 - || sample.sample_has_redundancy > 0x03 - { - return Err(invalid_value( - field_name, - "sample dependency fields must fit in 2 bits", - )); - } - - Ok((sample.is_leading << 6) - | (sample.sample_depends_on << 4) - | (sample.sample_is_depended_on << 2) - | sample.sample_has_redundancy) + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; } -/// Sample dependency type box. +/// Sync sample box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Sdtp { +pub struct Stss { full_box: FullBoxState, - pub samples: Vec, + pub entry_count: u32, + pub sample_number: Vec, } -impl FieldHooks for Sdtp { - fn display_field(&self, name: &'static str) -> Option { +impl_full_box!(Stss, *b"stss"); + +impl FieldHooks for Stss { + fn field_length(&self, name: &'static str) -> Option { match name { - "Samples" => Some(render_array(self.samples.iter().map(|sample| { - format!( - "{{IsLeading=0x{:x} SampleDependsOn=0x{:x} SampleIsDependedOn=0x{:x} SampleHasRedundancy=0x{:x}}}", - sample.is_leading, - sample.sample_depends_on, - sample.sample_is_depended_on, - sample.sample_has_redundancy - ) - }))), + "SampleNumber" => Some(self.entry_count), _ => None, } } } -impl_full_box!(Sdtp, *b"sdtp"); - -impl FieldValueRead for Sdtp { +impl FieldValueRead for Stss { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "Samples" => { - let mut bytes = Vec::with_capacity(self.samples.len()); - for sample in &self.samples { - bytes.push(encode_sdtp_sample(field_name, sample)?); - } - Ok(FieldValue::Bytes(bytes)) - } + "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), + "SampleNumber" => Ok(FieldValue::UnsignedArray(self.sample_number.clone())), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Sdtp { +impl FieldValueWrite for Stss { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("Samples", FieldValue::Bytes(bytes)) => { - self.samples = bytes - .into_iter() - .map(|sample| SdtpSampleElem { - is_leading: (sample >> 6) & 0x03, - sample_depends_on: (sample >> 4) & 0x03, - sample_is_depended_on: (sample >> 2) & 0x03, - sample_has_redundancy: sample & 0x03, - }) - .collect(); + ("EntryCount", FieldValue::Unsigned(value)) => { + self.entry_count = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("SampleNumber", FieldValue::UnsignedArray(values)) => { + let mut numbers = Vec::with_capacity(values.len()); + for value in values { + numbers.push(u64::from(u32_from_unsigned(field_name, value)?)); + } + self.sample_number = numbers; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -4375,702 +5147,3492 @@ impl FieldValueWrite for Sdtp { } } -impl CodecBox for Sdtp { +impl CodecBox for Stss { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("Samples", 2, with_bit_width(8), as_bytes()), + codec_field!("EntryCount", 2, with_bit_width(32)), + codec_field!("SampleNumber", 3, with_bit_width(32), with_dynamic_length()), ]); const SUPPORTED_VERSIONS: &'static [u8] = &[0]; } -/// Length-prefixed roll-distance description. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct RollDistanceWithLength { - pub description_length: u32, - pub roll_distance: i16, -} - -/// Optional alternative-startup sample counts. +/// Sample size box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct AlternativeStartupEntryOpt { - pub num_output_samples: u16, - pub num_total_samples: u16, +pub struct Stsz { + full_box: FullBoxState, + pub sample_size: u32, + pub sample_count: u32, + pub entry_size: Vec, } -/// Alternative-startup group description payload. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct AlternativeStartupEntry { - pub roll_count: u16, - pub first_output_sample: u16, - pub sample_offset: Vec, - pub opts: Vec, -} +impl FieldHooks for Stsz { + fn field_length(&self, name: &'static str) -> Option { + match name { + "EntrySize" => { + if self.sample_size == 0 { + Some(self.sample_count) + } else { + Some(0) + } + } + _ => None, + } + } -/// Length-prefixed alternative-startup description. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct AlternativeStartupEntryL { - pub description_length: u32, - pub alternative_startup_entry: AlternativeStartupEntry, + fn display_field(&self, _name: &'static str) -> Option { + None + } } -/// Visual random-access group description payload. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct VisualRandomAccessEntry { - pub num_leading_samples_known: bool, - pub num_leading_samples: u8, +impl ImmutableBox for Stsz { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"stsz") + } + + fn version(&self) -> u8 { + self.full_box.version + } + + fn flags(&self) -> u32 { + self.full_box.flags + } } -/// Length-prefixed visual random-access description. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct VisualRandomAccessEntryL { - pub description_length: u32, - pub visual_random_access_entry: VisualRandomAccessEntry, +impl MutableBox for Stsz { + fn set_version(&mut self, version: u8) { + self.full_box.version = version; + } + + fn set_flags(&mut self, flags: u32) { + self.full_box.flags = flags; + } } -/// Temporal-level group description payload. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct TemporalLevelEntry { - pub level_independently_decodable: bool, +impl FieldValueRead for Stsz { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "SampleSize" => Ok(FieldValue::Unsigned(u64::from(self.sample_size))), + "SampleCount" => Ok(FieldValue::Unsigned(u64::from(self.sample_count))), + "EntrySize" => Ok(FieldValue::UnsignedArray(self.entry_size.clone())), + _ => Err(missing_field(field_name)), + } + } } -/// Length-prefixed temporal-level description. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct TemporalLevelEntryL { - pub description_length: u32, - pub temporal_level_entry: TemporalLevelEntry, +impl FieldValueWrite for Stsz { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("SampleSize", FieldValue::Unsigned(value)) => { + self.sample_size = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("SampleCount", FieldValue::Unsigned(value)) => { + self.sample_count = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("EntrySize", FieldValue::UnsignedArray(values)) => { + let mut entry_size = Vec::with_capacity(values.len()); + for value in values { + entry_size.push(u64::from(u32_from_unsigned(field_name, value)?)); + } + self.entry_size = entry_size; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } } -fn format_alternative_startup_opts(opts: &[AlternativeStartupEntryOpt]) -> String { - render_array(opts.iter().map(|opt| { - format!( - "{{NumOutputSamples={} NumTotalSamples={}}}", - opt.num_output_samples, opt.num_total_samples - ) - })) +impl CodecBox for Stsz { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("SampleSize", 2, with_bit_width(32)), + codec_field!("SampleCount", 3, with_bit_width(32)), + codec_field!("EntrySize", 4, with_bit_width(32), with_dynamic_length()), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; } -fn format_alternative_startup_entry(entry: &AlternativeStartupEntry) -> String { - format!( - "{{RollCount={} FirstOutputSample={} SampleOffset={} Opts={}}}", - entry.roll_count, - entry.first_output_sample, - render_array(entry.sample_offset.iter().map(|offset| offset.to_string())), - format_alternative_startup_opts(&entry.opts) - ) +/// One time-to-sample entry. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SttsEntry { + pub sample_count: u32, + pub sample_delta: u32, } -fn encode_alternative_startup_entry( - field_name: &'static str, - entry: &AlternativeStartupEntry, -) -> Result, FieldValueError> { - require_count( - field_name, - u32::from(entry.roll_count), - entry.sample_offset.len(), - )?; +/// Time to sample box. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Stts { + full_box: FullBoxState, + pub entry_count: u32, + pub entries: Vec, +} - let mut bytes = Vec::with_capacity(4 + (entry.sample_offset.len() + entry.opts.len()) * 4); - bytes.extend_from_slice(&entry.roll_count.to_be_bytes()); - bytes.extend_from_slice(&entry.first_output_sample.to_be_bytes()); - for sample_offset in &entry.sample_offset { - bytes.extend_from_slice(&sample_offset.to_be_bytes()); +impl FieldHooks for Stts { + fn field_length(&self, name: &'static str) -> Option { + match name { + "Entries" => usize::try_from(self.entry_count) + .ok() + .and_then(|count| field_len_bytes(count, 8)), + _ => None, + } } - for opt in &entry.opts { - bytes.extend_from_slice(&opt.num_output_samples.to_be_bytes()); - bytes.extend_from_slice(&opt.num_total_samples.to_be_bytes()); + + fn display_field(&self, name: &'static str) -> Option { + match name { + "Entries" => Some(render_array(self.entries.iter().map(|entry| { + format!( + "{{SampleCount={} SampleDelta={}}}", + entry.sample_count, entry.sample_delta + ) + }))), + _ => None, + } } - Ok(bytes) } -fn parse_alternative_startup_entry( - field_name: &'static str, - bytes: &[u8], -) -> Result { - if bytes.len() < 4 { - return Err(invalid_value( - field_name, - "alternative startup entry is too short", - )); +impl ImmutableBox for Stts { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"stts") } - let roll_count = read_u16(bytes, 0); - let sample_offset_count = usize::from(roll_count); - let sample_offset_bytes = sample_offset_count - .checked_mul(4) - .ok_or_else(|| invalid_value(field_name, "alternative startup entry is too large"))?; - let minimum_len = 4_usize - .checked_add(sample_offset_bytes) - .ok_or_else(|| invalid_value(field_name, "alternative startup entry is too large"))?; - if bytes.len() < minimum_len { - return Err(invalid_value( - field_name, - "alternative startup entry is shorter than its roll count requires", - )); + fn version(&self) -> u8 { + self.full_box.version } - let trailing_len = bytes.len() - minimum_len; - if !trailing_len.is_multiple_of(4) { - return Err(invalid_value( - field_name, - "alternative startup entry options do not align to 4 bytes", - )); + fn flags(&self) -> u32 { + self.full_box.flags } +} - let mut sample_offset = Vec::with_capacity(untrusted_prealloc_hint(sample_offset_count)); - let mut offset = 4; - for _ in 0..sample_offset_count { - sample_offset.push(read_u32(bytes, offset)); - offset += 4; +impl MutableBox for Stts { + fn set_version(&mut self, version: u8) { + self.full_box.version = version; } - let mut opts = Vec::with_capacity(untrusted_prealloc_hint(trailing_len / 4)); - while offset < bytes.len() { - opts.push(AlternativeStartupEntryOpt { - num_output_samples: read_u16(bytes, offset), - num_total_samples: read_u16(bytes, offset + 2), - }); - offset += 4; + fn set_flags(&mut self, flags: u32) { + self.full_box.flags = flags; } - - Ok(AlternativeStartupEntry { - roll_count, - first_output_sample: read_u16(bytes, 2), - sample_offset, - opts, - }) } -fn encode_visual_random_access_entry( - field_name: &'static str, - entry: &VisualRandomAccessEntry, -) -> Result { - if entry.num_leading_samples > 0x7f { - return Err(invalid_value( - field_name, - "num leading samples does not fit in 7 bits", - )); +impl FieldValueRead for Stts { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), + "Entries" => { + let mut bytes = Vec::with_capacity(self.entries.len() * 8); + for entry in &self.entries { + bytes.extend_from_slice(&entry.sample_count.to_be_bytes()); + bytes.extend_from_slice(&entry.sample_delta.to_be_bytes()); + } + Ok(FieldValue::Bytes(bytes)) + } + _ => Err(missing_field(field_name)), + } } - - Ok((u8::from(entry.num_leading_samples_known) << 7) | entry.num_leading_samples) } -fn parse_visual_random_access_entry(byte: u8) -> VisualRandomAccessEntry { - VisualRandomAccessEntry { - num_leading_samples_known: byte & 0x80 != 0, - num_leading_samples: byte & 0x7f, +impl FieldValueWrite for Stts { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("EntryCount", FieldValue::Unsigned(value)) => { + self.entry_count = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("Entries", FieldValue::Bytes(bytes)) => { + self.entries = parse_fixed_chunks(field_name, &bytes, 8, |chunk| SttsEntry { + sample_count: read_u32(chunk, 0), + sample_delta: read_u32(chunk, 4), + })?; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } } } -fn encode_temporal_level_entry(entry: &TemporalLevelEntry) -> u8 { - u8::from(entry.level_independently_decodable) << 7 +impl CodecBox for Stts { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("EntryCount", 2, with_bit_width(32)), + codec_field!( + "Entries", + 3, + with_bit_width(8), + with_dynamic_length(), + as_bytes() + ), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; } -fn parse_temporal_level_entry( - field_name: &'static str, - byte: u8, -) -> Result { - if byte & 0x7f != 0 { - return Err(invalid_value( - field_name, - "temporal level entry reserved bits must be zero", - )); - } - - Ok(TemporalLevelEntry { - level_independently_decodable: byte & 0x80 != 0, - }) +/// One track-run sample entry. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TrunEntry { + pub sample_duration: u32, + pub sample_size: u32, + pub sample_flags: u32, + pub sample_composition_time_offset_v0: u32, + pub sample_composition_time_offset_v1: i32, } -/// Sample group description box. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Sgpd { +/// Track run box. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Trun { full_box: FullBoxState, - pub grouping_type: FourCc, - pub default_length: u32, - pub default_sample_description_index: u32, - pub entry_count: u32, - pub roll_distances: Vec, - pub roll_distances_l: Vec, - pub alternative_startup_entries: Vec, - pub alternative_startup_entries_l: Vec, - pub visual_random_access_entries: Vec, - pub visual_random_access_entries_l: Vec, - pub temporal_level_entries: Vec, - pub temporal_level_entries_l: Vec, - pub unsupported: Vec, + pub sample_count: u32, + pub data_offset: i32, + pub first_sample_flags: u32, + pub entries: Vec, } -impl Default for Sgpd { - fn default() -> Self { - Self { - full_box: FullBoxState::default(), - grouping_type: FourCc::ANY, - default_length: 0, - default_sample_description_index: 0, - entry_count: 0, - roll_distances: Vec::new(), - roll_distances_l: Vec::new(), - alternative_startup_entries: Vec::new(), - alternative_startup_entries_l: Vec::new(), - visual_random_access_entries: Vec::new(), - visual_random_access_entries_l: Vec::new(), - temporal_level_entries: Vec::new(), - temporal_level_entries_l: Vec::new(), - unsupported: Vec::new(), +impl FieldHooks for Trun { + fn field_length(&self, name: &'static str) -> Option { + match name { + "Entries" => { + let mut bytes_per_entry = 0usize; + if self.flags() & TRUN_SAMPLE_DURATION_PRESENT != 0 { + bytes_per_entry += 4; + } + if self.flags() & TRUN_SAMPLE_SIZE_PRESENT != 0 { + bytes_per_entry += 4; + } + if self.flags() & TRUN_SAMPLE_FLAGS_PRESENT != 0 { + bytes_per_entry += 4; + } + if self.flags() & TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT != 0 { + bytes_per_entry += 4; + } + usize::try_from(self.sample_count) + .ok() + .and_then(|count| field_len_bytes(count, bytes_per_entry)) + } + _ => None, } } -} - -impl Sgpd { - fn no_default_length(&self) -> bool { - self.version() == 1 && self.default_length == 0 - } - fn is_roll_grouping_type(&self) -> bool { - *self.grouping_type.as_bytes() == *b"roll" || *self.grouping_type.as_bytes() == *b"prol" + fn display_field(&self, name: &'static str) -> Option { + match name { + "Entries" => Some(render_array(self.entries.iter().map(|entry| { + let mut fields = Vec::new(); + if self.flags() & TRUN_SAMPLE_DURATION_PRESENT != 0 { + fields.push(format!("SampleDuration={}", entry.sample_duration)); + } + if self.flags() & TRUN_SAMPLE_SIZE_PRESENT != 0 { + fields.push(format!("SampleSize={}", entry.sample_size)); + } + if self.flags() & TRUN_SAMPLE_FLAGS_PRESENT != 0 { + fields.push(format!("SampleFlags=0x{:x}", entry.sample_flags)); + } + if self.flags() & TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT != 0 { + if self.version() == 0 { + fields.push(format!( + "SampleCompositionTimeOffsetV0={}", + entry.sample_composition_time_offset_v0 + )); + } else { + fields.push(format!( + "SampleCompositionTimeOffsetV1={}", + entry.sample_composition_time_offset_v1 + )); + } + } + format!("{{{}}}", fields.join(" ")) + }))), + _ => None, + } } +} - fn is_alternative_startup_grouping_type(&self) -> bool { - *self.grouping_type.as_bytes() == *b"alst" +impl ImmutableBox for Trun { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"trun") } - fn is_visual_random_access_grouping_type(&self) -> bool { - *self.grouping_type.as_bytes() == *b"rap " + fn version(&self) -> u8 { + self.full_box.version } - fn is_temporal_level_grouping_type(&self) -> bool { - *self.grouping_type.as_bytes() == *b"tele" + fn flags(&self) -> u32 { + self.full_box.flags } } -impl FieldHooks for Sgpd { - fn field_enabled(&self, name: &'static str) -> Option { - // The active payload shape depends on both the grouping type and whether version 1 uses per-entry lengths. - let no_default_length = self.no_default_length(); - let roll_distances = self.is_roll_grouping_type(); - let alternative_startup_entries = self.is_alternative_startup_grouping_type(); - let visual_random_access_entries = self.is_visual_random_access_grouping_type(); - let temporal_level_entries = self.is_temporal_level_grouping_type(); +impl MutableBox for Trun { + fn set_version(&mut self, version: u8) { + self.full_box.version = version; + } - match name { - "RollDistances" => Some(roll_distances && !no_default_length), - "RollDistancesL" => Some(roll_distances && no_default_length), - "AlternativeStartupEntries" => Some(alternative_startup_entries && !no_default_length), - "AlternativeStartupEntriesL" => Some(alternative_startup_entries && no_default_length), - "VisualRandomAccessEntries" => Some(visual_random_access_entries && !no_default_length), - "VisualRandomAccessEntriesL" => Some(visual_random_access_entries && no_default_length), - "TemporalLevelEntries" => Some(temporal_level_entries && !no_default_length), - "TemporalLevelEntriesL" => Some(temporal_level_entries && no_default_length), - "Unsupported" => Some( - !roll_distances - && !alternative_startup_entries - && !visual_random_access_entries - && !temporal_level_entries, - ), - _ => None, - } + fn set_flags(&mut self, flags: u32) { + self.full_box.flags = flags; } +} - fn display_field(&self, name: &'static str) -> Option { - match name { - "GroupingType" => Some(quoted_fourcc(self.grouping_type)), - "RollDistances" => Some(render_array( - self.roll_distances.iter().map(|distance| distance.to_string()), - )), - "RollDistancesL" => Some(render_array(self.roll_distances_l.iter().map(|entry| { - format!( - "{{DescriptionLength={} RollDistance={}}}", - entry.description_length, entry.roll_distance - ) - }))), - "AlternativeStartupEntries" => Some(render_array( - self.alternative_startup_entries - .iter() - .map(format_alternative_startup_entry), - )), - "AlternativeStartupEntriesL" => Some(render_array( - self.alternative_startup_entries_l.iter().map(|entry| { - format!( - "{{DescriptionLength={} {}}}", - entry.description_length, - format_alternative_startup_entry(&entry.alternative_startup_entry) - .trim_start_matches('{') - .trim_end_matches('}') - ) - }), - )), - "VisualRandomAccessEntries" => Some(render_array( - self.visual_random_access_entries.iter().map(|entry| { - format!( - "{{NumLeadingSamplesKnown={} NumLeadingSamples=0x{:x}}}", - entry.num_leading_samples_known, entry.num_leading_samples - ) - }), - )), - "VisualRandomAccessEntriesL" => Some(render_array( - self.visual_random_access_entries_l.iter().map(|entry| { - format!( - "{{DescriptionLength={} NumLeadingSamplesKnown={} NumLeadingSamples=0x{:x}}}", - entry.description_length, - entry.visual_random_access_entry.num_leading_samples_known, - entry.visual_random_access_entry.num_leading_samples - ) - }), - )), - "TemporalLevelEntries" => Some(render_array( - self.temporal_level_entries.iter().map(|entry| { - format!( - "{{LevelIndependentlyDecodable={}}}", - entry.level_independently_decodable - ) - }), - )), - "TemporalLevelEntriesL" => Some(render_array( - self.temporal_level_entries_l.iter().map(|entry| { - format!( - "{{DescriptionLength={} LevelIndependentlyDecodable={}}}", - entry.description_length, - entry.temporal_level_entry.level_independently_decodable - ) - }), - )), - _ => None, +impl Trun { + /// Returns the active composition time offset for `index`. + pub fn sample_composition_time_offset(&self, index: usize) -> i64 { + match self.version() { + 0 => i64::from(self.entries[index].sample_composition_time_offset_v0), + 1 => i64::from(self.entries[index].sample_composition_time_offset_v1), + _ => 0, } } } -impl_full_box!(Sgpd, *b"sgpd"); - -impl FieldValueRead for Sgpd { +impl FieldValueRead for Trun { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "GroupingType" => Ok(FieldValue::Bytes(self.grouping_type.as_bytes().to_vec())), - "DefaultLength" => Ok(FieldValue::Unsigned(u64::from(self.default_length))), - "DefaultSampleDescriptionIndex" => Ok(FieldValue::Unsigned(u64::from( - self.default_sample_description_index, - ))), - "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), - "RollDistances" => { - require_count(field_name, self.entry_count, self.roll_distances.len())?; - let mut bytes = Vec::with_capacity(self.roll_distances.len() * 2); - for roll_distance in &self.roll_distances { - bytes.extend_from_slice(&roll_distance.to_be_bytes()); - } - Ok(FieldValue::Bytes(bytes)) - } - "RollDistancesL" => { - require_count(field_name, self.entry_count, self.roll_distances_l.len())?; - let mut bytes = Vec::with_capacity(self.roll_distances_l.len() * 6); - for entry in &self.roll_distances_l { - bytes.extend_from_slice(&entry.description_length.to_be_bytes()); - bytes.extend_from_slice(&entry.roll_distance.to_be_bytes()); - } - Ok(FieldValue::Bytes(bytes)) - } - "AlternativeStartupEntries" => { - require_count( - field_name, - self.entry_count, - self.alternative_startup_entries.len(), - )?; + "SampleCount" => Ok(FieldValue::Unsigned(u64::from(self.sample_count))), + "DataOffset" => Ok(FieldValue::Signed(i64::from(self.data_offset))), + "FirstSampleFlags" => Ok(FieldValue::Unsigned(u64::from(self.first_sample_flags))), + "Entries" => { let mut bytes = Vec::new(); - for entry in &self.alternative_startup_entries { - let encoded = encode_alternative_startup_entry(field_name, entry)?; - if self.default_length != 0 && encoded.len() != self.default_length as usize { - return Err(invalid_value( - field_name, - "alternative startup entry does not match the default length", - )); + for entry in &self.entries { + if self.flags() & TRUN_SAMPLE_DURATION_PRESENT != 0 { + bytes.extend_from_slice(&entry.sample_duration.to_be_bytes()); } - bytes.extend_from_slice(&encoded); - } - Ok(FieldValue::Bytes(bytes)) - } - "AlternativeStartupEntriesL" => { - require_count( - field_name, - self.entry_count, - self.alternative_startup_entries_l.len(), - )?; - let mut bytes = Vec::new(); - for entry in &self.alternative_startup_entries_l { - let encoded = encode_alternative_startup_entry( - field_name, - &entry.alternative_startup_entry, - )?; - if encoded.len() != entry.description_length as usize { - return Err(invalid_value( - field_name, - "alternative startup entry length does not match the description length", - )); + if self.flags() & TRUN_SAMPLE_SIZE_PRESENT != 0 { + bytes.extend_from_slice(&entry.sample_size.to_be_bytes()); + } + if self.flags() & TRUN_SAMPLE_FLAGS_PRESENT != 0 { + bytes.extend_from_slice(&entry.sample_flags.to_be_bytes()); + } + if self.flags() & TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT != 0 { + if self.version() == 0 { + bytes.extend_from_slice( + &entry.sample_composition_time_offset_v0.to_be_bytes(), + ); + } else { + bytes.extend_from_slice( + &entry.sample_composition_time_offset_v1.to_be_bytes(), + ); + } } - bytes.extend_from_slice(&entry.description_length.to_be_bytes()); - bytes.extend_from_slice(&encoded); } Ok(FieldValue::Bytes(bytes)) } - "VisualRandomAccessEntries" => { - require_count( - field_name, - self.entry_count, - self.visual_random_access_entries.len(), - )?; - let mut bytes = Vec::with_capacity(self.visual_random_access_entries.len()); - for entry in &self.visual_random_access_entries { - bytes.push(encode_visual_random_access_entry(field_name, entry)?); - } - Ok(FieldValue::Bytes(bytes)) + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Trun { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("SampleCount", FieldValue::Unsigned(value)) => { + self.sample_count = u32_from_unsigned(field_name, value)?; + Ok(()) } - "VisualRandomAccessEntriesL" => { - require_count( - field_name, - self.entry_count, - self.visual_random_access_entries_l.len(), - )?; - let mut bytes = Vec::new(); - for entry in &self.visual_random_access_entries_l { - if entry.description_length != 1 { - return Err(invalid_value( - field_name, - "visual random access entries with explicit lengths must be 1 byte", - )); - } - bytes.extend_from_slice(&entry.description_length.to_be_bytes()); - bytes.push(encode_visual_random_access_entry( - field_name, - &entry.visual_random_access_entry, - )?); - } - Ok(FieldValue::Bytes(bytes)) + ("DataOffset", FieldValue::Signed(value)) => { + self.data_offset = i32_from_signed(field_name, value)?; + Ok(()) } - "TemporalLevelEntries" => { - require_count( - field_name, - self.entry_count, - self.temporal_level_entries.len(), - )?; - Ok(FieldValue::Bytes( - self.temporal_level_entries - .iter() - .map(encode_temporal_level_entry) - .collect(), - )) + ("FirstSampleFlags", FieldValue::Unsigned(value)) => { + self.first_sample_flags = u32_from_unsigned(field_name, value)?; + Ok(()) } - "TemporalLevelEntriesL" => { - require_count( - field_name, - self.entry_count, - self.temporal_level_entries_l.len(), - )?; - let mut bytes = Vec::new(); - for entry in &self.temporal_level_entries_l { - if entry.description_length != 1 { - return Err(invalid_value( - field_name, - "temporal level entries with explicit lengths must be 1 byte", - )); + ("Entries", FieldValue::Bytes(bytes)) => { + let mut bytes_per_entry = 0usize; + if self.flags() & TRUN_SAMPLE_DURATION_PRESENT != 0 { + bytes_per_entry += 4; + } + if self.flags() & TRUN_SAMPLE_SIZE_PRESENT != 0 { + bytes_per_entry += 4; + } + if self.flags() & TRUN_SAMPLE_FLAGS_PRESENT != 0 { + bytes_per_entry += 4; + } + if self.flags() & TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT != 0 { + bytes_per_entry += 4; + } + + self.entries = if bytes_per_entry == 0 { + Vec::new() + } else { + parse_fixed_chunks(field_name, &bytes, bytes_per_entry, |chunk| { + let mut offset = 0; + let mut entry = TrunEntry::default(); + if self.flags() & TRUN_SAMPLE_DURATION_PRESENT != 0 { + entry.sample_duration = read_u32(chunk, offset); + offset += 4; + } + if self.flags() & TRUN_SAMPLE_SIZE_PRESENT != 0 { + entry.sample_size = read_u32(chunk, offset); + offset += 4; + } + if self.flags() & TRUN_SAMPLE_FLAGS_PRESENT != 0 { + entry.sample_flags = read_u32(chunk, offset); + offset += 4; + } + if self.flags() & TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT != 0 { + if self.version() == 0 { + entry.sample_composition_time_offset_v0 = read_u32(chunk, offset); + } else { + entry.sample_composition_time_offset_v1 = read_i32(chunk, offset); + } + } + entry + })? + }; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Trun { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("SampleCount", 2, with_bit_width(32)), + codec_field!( + "DataOffset", + 3, + with_bit_width(32), + as_signed(), + with_required_flags(TRUN_DATA_OFFSET_PRESENT) + ), + codec_field!( + "FirstSampleFlags", + 4, + with_bit_width(32), + with_required_flags(TRUN_FIRST_SAMPLE_FLAGS_PRESENT), + as_hex() + ), + codec_field!( + "Entries", + 5, + with_bit_width(8), + with_dynamic_length(), + as_bytes() + ), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; +} + +simple_container_box!(Schi, *b"schi"); +simple_container_box!(Sinf, *b"sinf"); +simple_container_box!(Wave, *b"wave"); + +/// Metadata box. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Meta { + full_box: FullBoxState, + quicktime_headerless: bool, +} + +impl FieldHooks for Meta { + fn field_enabled(&self, name: &'static str) -> Option { + match name { + "Version" | "Flags" => Some(!self.quicktime_headerless), + _ => None, + } + } +} + +impl ImmutableBox for Meta { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"meta") + } + + fn version(&self) -> u8 { + self.full_box.version + } + + fn flags(&self) -> u32 { + self.full_box.flags + } +} + +impl MutableBox for Meta { + fn set_version(&mut self, version: u8) { + self.quicktime_headerless = false; + self.full_box.version = version; + } + + fn set_flags(&mut self, flags: u32) { + self.quicktime_headerless = false; + self.full_box.flags = flags; + } + + fn before_unmarshal( + &mut self, + reader: &mut dyn ReadSeek, + payload_size: u64, + ) -> Result<(), CodecError> { + self.quicktime_headerless = false; + if payload_size < 4 { + return Ok(()); + } + + // Headerless metadata starts directly with the first child box type instead of the full-box prefix. + let start = reader.stream_position()?; + let mut prefix = [0_u8; 4]; + reader.read_exact(&mut prefix)?; + reader.seek(SeekFrom::Start(start))?; + + if prefix.iter().any(|byte| *byte != 0) { + self.quicktime_headerless = true; + self.full_box.version = 0; + self.full_box.flags = 0; + } + + Ok(()) + } +} + +impl Meta { + /// Returns `true` when the payload omits the normal full-box header bytes. + pub fn is_quicktime_headerless(&self) -> bool { + self.quicktime_headerless + } +} + +impl FieldValueRead for Meta { + fn field_value(&self, field_name: &'static str) -> Result { + Err(missing_field(field_name)) + } +} + +impl FieldValueWrite for Meta { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + Err(unexpected_field(field_name, value)) + } +} + +impl CodecBox for Meta { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!( + "Version", + 0, + with_bit_width(8), + as_version_field(), + with_dynamic_presence() + ), + codec_field!( + "Flags", + 1, + with_bit_width(24), + as_flags_field(), + with_dynamic_presence() + ), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; +} + +/// Track-kind metadata box that stores a scheme URI and value string. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Kind { + full_box: FullBoxState, + pub scheme_uri: String, + pub value: String, +} + +impl FieldHooks for Kind {} + +impl_full_box!(Kind, *b"kind"); + +impl FieldValueRead for Kind { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "SchemeURI" => Ok(FieldValue::String(self.scheme_uri.clone())), + "Value" => Ok(FieldValue::String(self.value.clone())), + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Kind { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("SchemeURI", FieldValue::String(value)) => { + validate_c_string_value(field_name, &value)?; + self.scheme_uri = value; + Ok(()) + } + ("Value", FieldValue::String(value)) => { + validate_c_string_value(field_name, &value)?; + self.value = value; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Kind { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!( + "SchemeURI", + 2, + with_bit_width(8), + as_string(StringFieldMode::NullTerminated) + ), + codec_field!( + "Value", + 3, + with_bit_width(8), + as_string(StringFieldMode::NullTerminated) + ), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; + + fn custom_marshal(&self, writer: &mut dyn Write) -> Result, CodecError> { + validate_c_string_value("SchemeURI", &self.scheme_uri)?; + validate_c_string_value("Value", &self.value)?; + if self.version() != 0 { + return Err(CodecError::UnsupportedVersion { + box_type: self.box_type(), + version: self.version(), + }); + } + + let mut payload = Vec::with_capacity(6 + self.scheme_uri.len() + self.value.len()); + payload.push(self.version()); + push_uint("Flags", &mut payload, 3, u64::from(self.flags()))?; + payload.extend_from_slice(self.scheme_uri.as_bytes()); + payload.push(0); + payload.extend_from_slice(self.value.as_bytes()); + payload.push(0); + writer.write_all(&payload)?; + Ok(Some(payload.len() as u64)) + } + + fn custom_unmarshal( + &mut self, + reader: &mut dyn ReadSeek, + payload_size: u64, + ) -> Result, CodecError> { + let payload_len = usize::try_from(payload_size) + .map_err(|_| invalid_value("Payload", "payload is too large to decode"))?; + let payload = read_exact_vec_untrusted(reader, payload_len).map_err(CodecError::Io)?; + + if payload.len() < 6 { + return Err(invalid_value("Payload", "payload is too short").into()); + } + + let version = payload[0]; + if version != 0 { + return Err(CodecError::UnsupportedVersion { + box_type: self.box_type(), + version, + }); + } + + let (scheme_uri, scheme_len) = decode_required_c_string("SchemeURI", &payload[4..])?; + let value_offset = 4 + scheme_len; + let (value, value_len) = decode_required_c_string("Value", &payload[value_offset..])?; + if value_offset + value_len != payload.len() { + return Err(invalid_value("Payload", "payload has trailing bytes").into()); + } + + self.full_box = FullBoxState { + version, + flags: read_uint(&payload, 1, 3) as u32, + }; + self.scheme_uri = scheme_uri; + self.value = value; + Ok(Some(payload_size)) + } +} + +/// MIME metadata box that preserves whether the payload omitted the trailing NUL byte. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Mime { + full_box: FullBoxState, + pub content_type: String, + pub lacks_zero_termination: bool, +} + +impl FieldHooks for Mime { + fn field_enabled(&self, name: &'static str) -> Option { + match name { + "LacksZeroTermination" => Some(self.lacks_zero_termination), + _ => None, + } + } +} + +impl_full_box!(Mime, *b"mime"); + +impl FieldValueRead for Mime { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "ContentType" => Ok(FieldValue::String(self.content_type.clone())), + "LacksZeroTermination" => Ok(FieldValue::Boolean(self.lacks_zero_termination)), + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Mime { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("ContentType", FieldValue::String(value)) => { + validate_c_string_value(field_name, &value)?; + self.content_type = value; + Ok(()) + } + ("LacksZeroTermination", FieldValue::Boolean(value)) => { + self.lacks_zero_termination = value; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Mime { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!( + "ContentType", + 2, + with_bit_width(8), + as_string(StringFieldMode::NullTerminated) + ), + codec_field!( + "LacksZeroTermination", + 3, + with_bit_width(1), + as_boolean(), + with_dynamic_presence() + ), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; + + fn custom_marshal(&self, writer: &mut dyn Write) -> Result, CodecError> { + validate_c_string_value("ContentType", &self.content_type)?; + if self.version() != 0 { + return Err(CodecError::UnsupportedVersion { + box_type: self.box_type(), + version: self.version(), + }); + } + if self.lacks_zero_termination && self.content_type.is_empty() { + return Err( + invalid_value("ContentType", "non-terminated payload must not be empty").into(), + ); + } + + let mut payload = Vec::with_capacity( + 4 + self.content_type.len() + usize::from(!self.lacks_zero_termination), + ); + payload.push(self.version()); + push_uint("Flags", &mut payload, 3, u64::from(self.flags()))?; + payload.extend_from_slice(self.content_type.as_bytes()); + if !self.lacks_zero_termination { + payload.push(0); + } + writer.write_all(&payload)?; + Ok(Some(payload.len() as u64)) + } + + fn custom_unmarshal( + &mut self, + reader: &mut dyn ReadSeek, + payload_size: u64, + ) -> Result, CodecError> { + let payload_len = usize::try_from(payload_size) + .map_err(|_| invalid_value("Payload", "payload is too large to decode"))?; + let payload = read_exact_vec_untrusted(reader, payload_len).map_err(CodecError::Io)?; + + if payload.len() < 5 { + return Err(invalid_value("Payload", "payload is too short").into()); + } + + let version = payload[0]; + if version != 0 { + return Err(CodecError::UnsupportedVersion { + box_type: self.box_type(), + version, + }); + } + + let content_bytes = if payload.last() == Some(&0) { + self.lacks_zero_termination = false; + &payload[4..payload.len() - 1] + } else { + self.lacks_zero_termination = true; + &payload[4..] + }; + + if content_bytes.contains(&0) { + return Err(invalid_value("ContentType", "value must not contain NUL bytes").into()); + } + + self.full_box = FullBoxState { + version, + flags: read_uint(&payload, 1, 3) as u32, + }; + self.content_type = + String::from_utf8(content_bytes.to_vec()).map_err(|_| CodecError::InvalidUtf8 { + field_name: "ContentType", + })?; + Ok(Some(payload_size)) + } +} + +/// Handler reference box. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Hdlr { + full_box: FullBoxState, + pub pre_defined: u32, + pub handler_type: FourCc, + pub reserved: [u8; 12], + pub name: String, +} + +impl Default for Hdlr { + fn default() -> Self { + Self { + full_box: FullBoxState::default(), + pre_defined: 0, + handler_type: FourCc::ANY, + reserved: [0; 12], + name: String::new(), + } + } +} + +impl FieldHooks for Hdlr { + fn is_pascal_string( + &self, + name: &'static str, + _data: &[u8], + remaining_bytes: u64, + ) -> Option { + match name { + // Some files store the handler name as a Pascal string and consume the last payload byte with the length prefix. + "Name" => Some(self.pre_defined != 0 && remaining_bytes == 0), + _ => None, + } + } + + fn consume_remaining_bytes_after_string(&self, name: &'static str) -> Option { + match name { + // Handler names may be padded after the visible terminator, so keep consuming the declared field payload. + "Name" => Some(true), + _ => None, + } + } + + fn display_field(&self, name: &'static str) -> Option { + match name { + "HandlerType" => Some(quoted_fourcc(self.handler_type)), + _ => None, + } + } +} + +impl_full_box!(Hdlr, *b"hdlr"); + +impl FieldValueRead for Hdlr { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "PreDefined" => Ok(FieldValue::Unsigned(u64::from(self.pre_defined))), + "HandlerType" => Ok(FieldValue::Bytes(self.handler_type.as_bytes().to_vec())), + "Reserved" => Ok(FieldValue::Bytes(self.reserved.to_vec())), + "Name" => Ok(FieldValue::String(self.name.clone())), + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Hdlr { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("PreDefined", FieldValue::Unsigned(value)) => { + self.pre_defined = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("HandlerType", FieldValue::Bytes(bytes)) => { + self.handler_type = bytes_to_fourcc(field_name, bytes)?; + Ok(()) + } + ("Reserved", FieldValue::Bytes(bytes)) => { + if bytes.len() != 12 { + return Err(invalid_value( + field_name, + "value must contain exactly 12 bytes", + )); + } + self.reserved.copy_from_slice(&bytes); + Ok(()) + } + ("Name", FieldValue::String(value)) => { + self.name = value; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Hdlr { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("PreDefined", 2, with_bit_width(32)), + codec_field!( + "HandlerType", + 3, + with_bit_width(8), + with_length(4), + as_bytes() + ), + codec_field!( + "Reserved", + 4, + with_bit_width(8), + with_length(12), + as_bytes(), + as_hidden() + ), + codec_field!( + "Name", + 5, + with_bit_width(8), + as_string(StringFieldMode::PascalCompatible) + ), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; +} + +/// Auxiliary information offsets box. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Saio { + full_box: FullBoxState, + pub aux_info_type: FourCc, + pub aux_info_type_parameter: u32, + pub entry_count: u32, + pub offset_v0: Vec, + pub offset_v1: Vec, +} + +impl Default for Saio { + fn default() -> Self { + Self { + full_box: FullBoxState::default(), + aux_info_type: FourCc::ANY, + aux_info_type_parameter: 0, + entry_count: 0, + offset_v0: Vec::new(), + offset_v1: Vec::new(), + } + } +} + +impl FieldHooks for Saio { + fn field_length(&self, name: &'static str) -> Option { + match name { + "OffsetV0" | "OffsetV1" => Some(self.entry_count), + _ => None, + } + } + + fn display_field(&self, name: &'static str) -> Option { + match name { + "AuxInfoType" => Some(quoted_fourcc(self.aux_info_type)), + _ => None, + } + } +} + +impl_full_box!(Saio, *b"saio"); + +impl Saio { + /// Returns the active auxiliary information offset at `index`. + pub fn offset(&self, index: usize) -> u64 { + match self.version() { + 0 => self.offset_v0[index], + 1 => self.offset_v1[index], + _ => 0, + } + } +} + +impl FieldValueRead for Saio { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "AuxInfoType" => Ok(FieldValue::Bytes(self.aux_info_type.as_bytes().to_vec())), + "AuxInfoTypeParameter" => Ok(FieldValue::Unsigned(u64::from( + self.aux_info_type_parameter, + ))), + "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), + "OffsetV0" => Ok(FieldValue::UnsignedArray(self.offset_v0.clone())), + "OffsetV1" => Ok(FieldValue::UnsignedArray(self.offset_v1.clone())), + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Saio { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("AuxInfoType", FieldValue::Bytes(bytes)) => { + self.aux_info_type = bytes_to_fourcc(field_name, bytes)?; + Ok(()) + } + ("AuxInfoTypeParameter", FieldValue::Unsigned(value)) => { + self.aux_info_type_parameter = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("EntryCount", FieldValue::Unsigned(value)) => { + self.entry_count = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("OffsetV0", FieldValue::UnsignedArray(values)) => { + let mut offsets = Vec::with_capacity(values.len()); + for value in values { + offsets.push(u64::from(u32_from_unsigned(field_name, value)?)); + } + self.offset_v0 = offsets; + Ok(()) + } + ("OffsetV1", FieldValue::UnsignedArray(values)) => { + self.offset_v1 = values; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Saio { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!( + "AuxInfoType", + 2, + with_bit_width(8), + with_length(4), + as_bytes(), + with_required_flags(AUX_INFO_TYPE_PRESENT) + ), + codec_field!( + "AuxInfoTypeParameter", + 3, + with_bit_width(32), + as_hex(), + with_required_flags(AUX_INFO_TYPE_PRESENT) + ), + codec_field!("EntryCount", 4, with_bit_width(32)), + codec_field!( + "OffsetV0", + 5, + with_bit_width(32), + with_dynamic_length(), + with_version(0) + ), + codec_field!( + "OffsetV1", + 6, + with_bit_width(64), + with_dynamic_length(), + with_version(1) + ), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; +} + +/// Auxiliary information sizes box. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Saiz { + full_box: FullBoxState, + pub aux_info_type: FourCc, + pub aux_info_type_parameter: u32, + pub default_sample_info_size: u8, + pub sample_count: u32, + pub sample_info_size: Vec, +} + +impl Default for Saiz { + fn default() -> Self { + Self { + full_box: FullBoxState::default(), + aux_info_type: FourCc::ANY, + aux_info_type_parameter: 0, + default_sample_info_size: 0, + sample_count: 0, + sample_info_size: Vec::new(), + } + } +} + +impl FieldHooks for Saiz { + fn field_length(&self, name: &'static str) -> Option { + match name { + "SampleInfoSize" => Some(self.sample_count), + _ => None, + } + } + + fn field_enabled(&self, name: &'static str) -> Option { + match name { + "SampleInfoSize" => Some(self.default_sample_info_size == 0), + _ => None, + } + } + + fn display_field(&self, name: &'static str) -> Option { + match name { + "AuxInfoType" => Some(quoted_fourcc(self.aux_info_type)), + _ => None, + } + } +} + +impl_full_box!(Saiz, *b"saiz"); + +impl FieldValueRead for Saiz { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "AuxInfoType" => Ok(FieldValue::Bytes(self.aux_info_type.as_bytes().to_vec())), + "AuxInfoTypeParameter" => Ok(FieldValue::Unsigned(u64::from( + self.aux_info_type_parameter, + ))), + "DefaultSampleInfoSize" => Ok(FieldValue::Unsigned(u64::from( + self.default_sample_info_size, + ))), + "SampleCount" => Ok(FieldValue::Unsigned(u64::from(self.sample_count))), + "SampleInfoSize" => Ok(FieldValue::UnsignedArray( + self.sample_info_size + .iter() + .copied() + .map(u64::from) + .collect(), + )), + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Saiz { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("AuxInfoType", FieldValue::Bytes(bytes)) => { + self.aux_info_type = bytes_to_fourcc(field_name, bytes)?; + Ok(()) + } + ("AuxInfoTypeParameter", FieldValue::Unsigned(value)) => { + self.aux_info_type_parameter = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("DefaultSampleInfoSize", FieldValue::Unsigned(value)) => { + self.default_sample_info_size = u8_from_unsigned(field_name, value)?; + Ok(()) + } + ("SampleCount", FieldValue::Unsigned(value)) => { + self.sample_count = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("SampleInfoSize", FieldValue::UnsignedArray(values)) => { + let mut sizes = Vec::with_capacity(values.len()); + for value in values { + sizes.push(u8_from_unsigned(field_name, value)?); + } + self.sample_info_size = sizes; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Saiz { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!( + "AuxInfoType", + 2, + with_bit_width(8), + with_length(4), + as_bytes(), + with_required_flags(AUX_INFO_TYPE_PRESENT) + ), + codec_field!( + "AuxInfoTypeParameter", + 3, + with_bit_width(32), + as_hex(), + with_required_flags(AUX_INFO_TYPE_PRESENT) + ), + codec_field!("DefaultSampleInfoSize", 4, with_bit_width(8)), + codec_field!("SampleCount", 5, with_bit_width(32)), + codec_field!( + "SampleInfoSize", + 6, + with_bit_width(8), + with_dynamic_length(), + with_dynamic_presence() + ), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; +} + +/// One sample-to-group entry. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SbgpEntry { + pub sample_count: u32, + pub group_description_index: u32, +} + +/// Sample-to-group box. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Sbgp { + full_box: FullBoxState, + pub grouping_type: u32, + pub grouping_type_parameter: u32, + pub entry_count: u32, + pub entries: Vec, +} + +impl FieldHooks for Sbgp { + fn display_field(&self, name: &'static str) -> Option { + match name { + "Entries" => Some(render_array(self.entries.iter().map(|entry| { + format!( + "{{SampleCount={} GroupDescriptionIndex={}}}", + entry.sample_count, entry.group_description_index + ) + }))), + _ => None, + } + } +} + +impl_full_box!(Sbgp, *b"sbgp"); + +impl FieldValueRead for Sbgp { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "GroupingType" => Ok(FieldValue::Unsigned(u64::from(self.grouping_type))), + "GroupingTypeParameter" => Ok(FieldValue::Unsigned(u64::from( + self.grouping_type_parameter, + ))), + "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), + "Entries" => { + let mut bytes = Vec::with_capacity(self.entries.len() * 8); + for entry in &self.entries { + bytes.extend_from_slice(&entry.sample_count.to_be_bytes()); + bytes.extend_from_slice(&entry.group_description_index.to_be_bytes()); + } + Ok(FieldValue::Bytes(bytes)) + } + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Sbgp { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("GroupingType", FieldValue::Unsigned(value)) => { + self.grouping_type = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("GroupingTypeParameter", FieldValue::Unsigned(value)) => { + self.grouping_type_parameter = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("EntryCount", FieldValue::Unsigned(value)) => { + self.entry_count = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("Entries", FieldValue::Bytes(bytes)) => { + let expected_len = field_len_from_count(self.entry_count, 8) + .map(|len| len as usize) + .unwrap_or(0); + if bytes.len() != expected_len { + return Err(invalid_value( + field_name, + "entry payload length does not match the entry count", + )); + } + + self.entries = parse_fixed_chunks(field_name, &bytes, 8, |chunk| SbgpEntry { + sample_count: read_u32(chunk, 0), + group_description_index: read_u32(chunk, 4), + })?; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Sbgp { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("GroupingType", 2, with_bit_width(32)), + codec_field!( + "GroupingTypeParameter", + 3, + with_bit_width(32), + with_version(1) + ), + codec_field!("EntryCount", 4, with_bit_width(32)), + codec_field!("Entries", 5, with_bit_width(8), as_bytes()), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; +} + +/// One subsample record carried by [`Subs`]. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SubsSample { + pub subsample_size: u32, + pub subsample_priority: u8, + pub discardable: u8, + pub codec_specific_parameters: u32, +} + +/// One sample entry inside [`Subs`]. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SubsEntry { + pub sample_delta: u32, + pub subsample_count: u16, + pub subsamples: Vec, +} + +/// Subsample-information box. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Subs { + full_box: FullBoxState, + pub entry_count: u32, + pub entries: Vec, +} + +fn format_subs_entries(entries: &[SubsEntry]) -> String { + render_array(entries.iter().map(|entry| { + format!( + "{{SampleDelta={} SubsampleCount={} Subsamples={}}}", + entry.sample_delta, + entry.subsample_count, + render_array(entry.subsamples.iter().map(|subsample| { + format!( + "{{SubsampleSize={} SubsamplePriority={} Discardable={} CodecSpecificParameters={}}}", + subsample.subsample_size, + subsample.subsample_priority, + subsample.discardable, + subsample.codec_specific_parameters + ) + })) + ) + })) +} + +fn encode_subs_entries( + field_name: &'static str, + version: u8, + entries: &[SubsEntry], +) -> Result, FieldValueError> { + let mut bytes = Vec::new(); + + for entry in entries { + require_count( + field_name, + u32::from(entry.subsample_count), + entry.subsamples.len(), + )?; + bytes.extend_from_slice(&entry.sample_delta.to_be_bytes()); + bytes.extend_from_slice(&entry.subsample_count.to_be_bytes()); + + for subsample in &entry.subsamples { + if version == 0 { + let subsample_size = u16::try_from(subsample.subsample_size).map_err(|_| { + invalid_value(field_name, "version 0 subsample size does not fit in u16") + })?; + bytes.extend_from_slice(&subsample_size.to_be_bytes()); + } else { + bytes.extend_from_slice(&subsample.subsample_size.to_be_bytes()); + } + + bytes.push(subsample.subsample_priority); + bytes.push(subsample.discardable); + bytes.extend_from_slice(&subsample.codec_specific_parameters.to_be_bytes()); + } + } + + Ok(bytes) +} + +fn parse_subs_entries( + field_name: &'static str, + version: u8, + entry_count: u32, + bytes: &[u8], +) -> Result, FieldValueError> { + let mut entries = Vec::with_capacity(untrusted_prealloc_hint( + usize::try_from(entry_count).unwrap_or(0), + )); + let mut offset = 0usize; + + for _ in 0..entry_count { + if bytes.len().saturating_sub(offset) < 6 { + return Err(invalid_value(field_name, "entry payload is truncated")); + } + + let sample_delta = read_u32(bytes, offset); + let subsample_count = read_u16(bytes, offset + 4); + offset += 6; + + let mut subsamples = + Vec::with_capacity(untrusted_prealloc_hint(usize::from(subsample_count))); + for _ in 0..subsample_count { + let subsample_header_len = if version == 1 { 10 } else { 8 }; + if bytes.len().saturating_sub(offset) < subsample_header_len { + return Err(invalid_value(field_name, "subsample payload is truncated")); + } + + let subsample_size = if version == 1 { + let value = read_u32(bytes, offset); + offset += 4; + value + } else { + let value = u32::from(read_u16(bytes, offset)); + offset += 2; + value + }; + + let subsample_priority = bytes[offset]; + let discardable = bytes[offset + 1]; + let codec_specific_parameters = read_u32(bytes, offset + 2); + offset += 6; + + subsamples.push(SubsSample { + subsample_size, + subsample_priority, + discardable, + codec_specific_parameters, + }); + } + + entries.push(SubsEntry { + sample_delta, + subsample_count, + subsamples, + }); + } + + if offset != bytes.len() { + return Err(invalid_value( + field_name, + "entry payload length does not match the entry count", + )); + } + + Ok(entries) +} + +impl FieldHooks for Subs { + fn display_field(&self, name: &'static str) -> Option { + match name { + "Entries" => Some(format_subs_entries(&self.entries)), + _ => None, + } + } +} + +impl_full_box!(Subs, *b"subs"); + +impl FieldValueRead for Subs { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), + "Entries" => { + require_count(field_name, self.entry_count, self.entries.len())?; + Ok(FieldValue::Bytes(encode_subs_entries( + field_name, + self.version(), + &self.entries, + )?)) + } + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Subs { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("EntryCount", FieldValue::Unsigned(value)) => { + self.entry_count = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("Entries", FieldValue::Bytes(bytes)) => { + self.entries = + parse_subs_entries(field_name, self.version(), self.entry_count, &bytes)?; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Subs { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("EntryCount", 2, with_bit_width(32)), + codec_field!("Entries", 3, with_bit_width(8), as_bytes()), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; +} + +/// One packed sample dependency entry. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SdtpSampleElem { + pub is_leading: u8, + pub sample_depends_on: u8, + pub sample_is_depended_on: u8, + pub sample_has_redundancy: u8, +} + +fn encode_sdtp_sample( + field_name: &'static str, + sample: &SdtpSampleElem, +) -> Result { + if sample.is_leading > 0x03 + || sample.sample_depends_on > 0x03 + || sample.sample_is_depended_on > 0x03 + || sample.sample_has_redundancy > 0x03 + { + return Err(invalid_value( + field_name, + "sample dependency fields must fit in 2 bits", + )); + } + + Ok((sample.is_leading << 6) + | (sample.sample_depends_on << 4) + | (sample.sample_is_depended_on << 2) + | sample.sample_has_redundancy) +} + +/// Sample dependency type box. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Sdtp { + full_box: FullBoxState, + pub samples: Vec, +} + +impl FieldHooks for Sdtp { + fn display_field(&self, name: &'static str) -> Option { + match name { + "Samples" => Some(render_array(self.samples.iter().map(|sample| { + format!( + "{{IsLeading=0x{:x} SampleDependsOn=0x{:x} SampleIsDependedOn=0x{:x} SampleHasRedundancy=0x{:x}}}", + sample.is_leading, + sample.sample_depends_on, + sample.sample_is_depended_on, + sample.sample_has_redundancy + ) + }))), + _ => None, + } + } +} + +impl_full_box!(Sdtp, *b"sdtp"); + +impl FieldValueRead for Sdtp { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "Samples" => { + let mut bytes = Vec::with_capacity(self.samples.len()); + for sample in &self.samples { + bytes.push(encode_sdtp_sample(field_name, sample)?); + } + Ok(FieldValue::Bytes(bytes)) + } + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Sdtp { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("Samples", FieldValue::Bytes(bytes)) => { + self.samples = bytes + .into_iter() + .map(|sample| SdtpSampleElem { + is_leading: (sample >> 6) & 0x03, + sample_depends_on: (sample >> 4) & 0x03, + sample_is_depended_on: (sample >> 2) & 0x03, + sample_has_redundancy: sample & 0x03, + }) + .collect(); + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Sdtp { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("Samples", 2, with_bit_width(8), as_bytes()), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; +} + +/// Length-prefixed roll-distance description. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct RollDistanceWithLength { + pub description_length: u32, + pub roll_distance: i16, +} + +/// Optional alternative-startup sample counts. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct AlternativeStartupEntryOpt { + pub num_output_samples: u16, + pub num_total_samples: u16, +} + +/// Alternative-startup group description payload. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct AlternativeStartupEntry { + pub roll_count: u16, + pub first_output_sample: u16, + pub sample_offset: Vec, + pub opts: Vec, +} + +/// Length-prefixed alternative-startup description. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct AlternativeStartupEntryL { + pub description_length: u32, + pub alternative_startup_entry: AlternativeStartupEntry, +} + +/// Visual random-access group description payload. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct VisualRandomAccessEntry { + pub num_leading_samples_known: bool, + pub num_leading_samples: u8, +} + +/// Length-prefixed visual random-access description. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct VisualRandomAccessEntryL { + pub description_length: u32, + pub visual_random_access_entry: VisualRandomAccessEntry, +} + +/// Temporal-level group description payload. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TemporalLevelEntry { + pub level_independently_decodable: bool, +} + +/// Length-prefixed temporal-level description. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TemporalLevelEntryL { + pub description_length: u32, + pub temporal_level_entry: TemporalLevelEntry, +} + +/// Sample-encryption information group description payload for the `seig` grouping type. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SeigEntry { + pub reserved: u8, + pub crypt_byte_block: u8, + pub skip_byte_block: u8, + pub is_protected: u8, + pub per_sample_iv_size: u8, + pub kid: [u8; 16], + pub constant_iv_size: u8, + pub constant_iv: Vec, +} + +/// Length-prefixed sample-encryption information group description payload. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SeigEntryL { + pub description_length: u32, + pub seig_entry: SeigEntry, +} + +fn format_alternative_startup_opts(opts: &[AlternativeStartupEntryOpt]) -> String { + render_array(opts.iter().map(|opt| { + format!( + "{{NumOutputSamples={} NumTotalSamples={}}}", + opt.num_output_samples, opt.num_total_samples + ) + })) +} + +fn format_alternative_startup_entry(entry: &AlternativeStartupEntry) -> String { + format!( + "{{RollCount={} FirstOutputSample={} SampleOffset={} Opts={}}}", + entry.roll_count, + entry.first_output_sample, + render_array(entry.sample_offset.iter().map(|offset| offset.to_string())), + format_alternative_startup_opts(&entry.opts) + ) +} + +fn encode_alternative_startup_entry( + field_name: &'static str, + entry: &AlternativeStartupEntry, +) -> Result, FieldValueError> { + require_count( + field_name, + u32::from(entry.roll_count), + entry.sample_offset.len(), + )?; + + let mut bytes = Vec::with_capacity(4 + (entry.sample_offset.len() + entry.opts.len()) * 4); + bytes.extend_from_slice(&entry.roll_count.to_be_bytes()); + bytes.extend_from_slice(&entry.first_output_sample.to_be_bytes()); + for sample_offset in &entry.sample_offset { + bytes.extend_from_slice(&sample_offset.to_be_bytes()); + } + for opt in &entry.opts { + bytes.extend_from_slice(&opt.num_output_samples.to_be_bytes()); + bytes.extend_from_slice(&opt.num_total_samples.to_be_bytes()); + } + Ok(bytes) +} + +fn parse_alternative_startup_entry( + field_name: &'static str, + bytes: &[u8], +) -> Result { + if bytes.len() < 4 { + return Err(invalid_value( + field_name, + "alternative startup entry is too short", + )); + } + + let roll_count = read_u16(bytes, 0); + let sample_offset_count = usize::from(roll_count); + let sample_offset_bytes = sample_offset_count + .checked_mul(4) + .ok_or_else(|| invalid_value(field_name, "alternative startup entry is too large"))?; + let minimum_len = 4_usize + .checked_add(sample_offset_bytes) + .ok_or_else(|| invalid_value(field_name, "alternative startup entry is too large"))?; + if bytes.len() < minimum_len { + return Err(invalid_value( + field_name, + "alternative startup entry is shorter than its roll count requires", + )); + } + + let trailing_len = bytes.len() - minimum_len; + if !trailing_len.is_multiple_of(4) { + return Err(invalid_value( + field_name, + "alternative startup entry options do not align to 4 bytes", + )); + } + + let mut sample_offset = Vec::with_capacity(untrusted_prealloc_hint(sample_offset_count)); + let mut offset = 4; + for _ in 0..sample_offset_count { + sample_offset.push(read_u32(bytes, offset)); + offset += 4; + } + + let mut opts = Vec::with_capacity(untrusted_prealloc_hint(trailing_len / 4)); + while offset < bytes.len() { + opts.push(AlternativeStartupEntryOpt { + num_output_samples: read_u16(bytes, offset), + num_total_samples: read_u16(bytes, offset + 2), + }); + offset += 4; + } + + Ok(AlternativeStartupEntry { + roll_count, + first_output_sample: read_u16(bytes, 2), + sample_offset, + opts, + }) +} + +fn encode_visual_random_access_entry( + field_name: &'static str, + entry: &VisualRandomAccessEntry, +) -> Result { + if entry.num_leading_samples > 0x7f { + return Err(invalid_value( + field_name, + "num leading samples does not fit in 7 bits", + )); + } + + Ok((u8::from(entry.num_leading_samples_known) << 7) | entry.num_leading_samples) +} + +fn parse_visual_random_access_entry(byte: u8) -> VisualRandomAccessEntry { + VisualRandomAccessEntry { + num_leading_samples_known: byte & 0x80 != 0, + num_leading_samples: byte & 0x7f, + } +} + +fn encode_temporal_level_entry(entry: &TemporalLevelEntry) -> u8 { + u8::from(entry.level_independently_decodable) << 7 +} + +fn parse_temporal_level_entry( + field_name: &'static str, + byte: u8, +) -> Result { + if byte & 0x7f != 0 { + return Err(invalid_value( + field_name, + "temporal level entry reserved bits must be zero", + )); + } + + Ok(TemporalLevelEntry { + level_independently_decodable: byte & 0x80 != 0, + }) +} + +fn format_seig_entry(entry: &SeigEntry) -> String { + let mut rendered = format!( + "{{Reserved={} CryptByteBlock={} SkipByteBlock={} IsProtected={} PerSampleIVSize={} KID={}", + entry.reserved, + entry.crypt_byte_block, + entry.skip_byte_block, + entry.is_protected, + entry.per_sample_iv_size, + render_uuid(&entry.kid) + ); + if entry.is_protected == 1 && entry.per_sample_iv_size == 0 { + rendered.push_str(&format!( + " ConstantIVSize={} ConstantIV={}", + entry.constant_iv_size, + render_hex_bytes(&entry.constant_iv) + )); + } + rendered.push('}'); + rendered +} + +fn encode_seig_entry( + field_name: &'static str, + entry: &SeigEntry, +) -> Result, FieldValueError> { + if entry.crypt_byte_block > 0x0f { + return Err(invalid_value( + field_name, + "crypt byte block does not fit in 4 bits", + )); + } + if entry.skip_byte_block > 0x0f { + return Err(invalid_value( + field_name, + "skip byte block does not fit in 4 bits", + )); + } + + let mut bytes = Vec::with_capacity(20 + entry.constant_iv.len()); + bytes.push(entry.reserved); + bytes.push((entry.crypt_byte_block << 4) | entry.skip_byte_block); + bytes.push(entry.is_protected); + bytes.push(entry.per_sample_iv_size); + bytes.extend_from_slice(&entry.kid); + + if entry.is_protected == 1 && entry.per_sample_iv_size == 0 { + if entry.constant_iv.len() != usize::from(entry.constant_iv_size) { + return Err(invalid_value( + field_name, + "constant IV length does not match the constant IV size", + )); + } + bytes.push(entry.constant_iv_size); + bytes.extend_from_slice(&entry.constant_iv); + } + + Ok(bytes) +} + +fn parse_seig_entry( + field_name: &'static str, + bytes: &[u8], +) -> Result<(SeigEntry, usize), FieldValueError> { + if bytes.len() < 20 { + return Err(invalid_value(field_name, "seig entry is too short")); + } + + let reserved = bytes[0]; + let crypt_and_skip = bytes[1]; + let is_protected = bytes[2]; + let per_sample_iv_size = bytes[3]; + let kid = bytes[4..20].try_into().unwrap(); + let mut consumed = 20; + let mut constant_iv_size = 0; + let mut constant_iv = Vec::new(); + + if is_protected == 1 && per_sample_iv_size == 0 { + if bytes.len() < consumed + 1 { + return Err(invalid_value( + field_name, + "seig constant IV size is truncated", + )); + } + constant_iv_size = bytes[consumed]; + consumed += 1; + let constant_iv_len = usize::from(constant_iv_size); + if bytes.len() < consumed + constant_iv_len { + return Err(invalid_value( + field_name, + "seig constant IV exceeds the remaining payload", + )); + } + constant_iv = bytes[consumed..consumed + constant_iv_len].to_vec(); + consumed += constant_iv_len; + } + + Ok(( + SeigEntry { + reserved, + crypt_byte_block: crypt_and_skip >> 4, + skip_byte_block: crypt_and_skip & 0x0f, + is_protected, + per_sample_iv_size, + kid, + constant_iv_size, + constant_iv, + }, + consumed, + )) +} + +fn parse_seig_entry_exact( + field_name: &'static str, + bytes: &[u8], +) -> Result { + let (entry, consumed) = parse_seig_entry(field_name, bytes)?; + if consumed != bytes.len() { + return Err(invalid_value(field_name, "seig entry has trailing bytes")); + } + Ok(entry) +} + +/// Sample group description box. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Sgpd { + full_box: FullBoxState, + pub grouping_type: FourCc, + pub default_length: u32, + pub default_sample_description_index: u32, + pub entry_count: u32, + pub roll_distances: Vec, + pub roll_distances_l: Vec, + pub alternative_startup_entries: Vec, + pub alternative_startup_entries_l: Vec, + pub visual_random_access_entries: Vec, + pub visual_random_access_entries_l: Vec, + pub temporal_level_entries: Vec, + pub temporal_level_entries_l: Vec, + pub seig_entries: Vec, + pub seig_entries_l: Vec, + pub unsupported: Vec, +} + +impl Default for Sgpd { + fn default() -> Self { + Self { + full_box: FullBoxState::default(), + grouping_type: FourCc::ANY, + default_length: 0, + default_sample_description_index: 0, + entry_count: 0, + roll_distances: Vec::new(), + roll_distances_l: Vec::new(), + alternative_startup_entries: Vec::new(), + alternative_startup_entries_l: Vec::new(), + visual_random_access_entries: Vec::new(), + visual_random_access_entries_l: Vec::new(), + temporal_level_entries: Vec::new(), + temporal_level_entries_l: Vec::new(), + seig_entries: Vec::new(), + seig_entries_l: Vec::new(), + unsupported: Vec::new(), + } + } +} + +impl Sgpd { + fn no_default_length(&self) -> bool { + self.version() == 1 && self.default_length == 0 + } + + fn is_roll_grouping_type(&self) -> bool { + *self.grouping_type.as_bytes() == *b"roll" || *self.grouping_type.as_bytes() == *b"prol" + } + + fn is_alternative_startup_grouping_type(&self) -> bool { + *self.grouping_type.as_bytes() == *b"alst" + } + + fn is_visual_random_access_grouping_type(&self) -> bool { + *self.grouping_type.as_bytes() == *b"rap " + } + + fn is_temporal_level_grouping_type(&self) -> bool { + *self.grouping_type.as_bytes() == *b"tele" + } + + fn is_seig_grouping_type(&self) -> bool { + *self.grouping_type.as_bytes() == *b"seig" + } +} + +impl FieldHooks for Sgpd { + fn field_enabled(&self, name: &'static str) -> Option { + // The active payload shape depends on both the grouping type and whether version 1 uses per-entry lengths. + let no_default_length = self.no_default_length(); + let roll_distances = self.is_roll_grouping_type(); + let alternative_startup_entries = self.is_alternative_startup_grouping_type(); + let visual_random_access_entries = self.is_visual_random_access_grouping_type(); + let temporal_level_entries = self.is_temporal_level_grouping_type(); + let seig_entries = self.is_seig_grouping_type(); + + match name { + "RollDistances" => Some(roll_distances && !no_default_length), + "RollDistancesL" => Some(roll_distances && no_default_length), + "AlternativeStartupEntries" => Some(alternative_startup_entries && !no_default_length), + "AlternativeStartupEntriesL" => Some(alternative_startup_entries && no_default_length), + "VisualRandomAccessEntries" => Some(visual_random_access_entries && !no_default_length), + "VisualRandomAccessEntriesL" => Some(visual_random_access_entries && no_default_length), + "TemporalLevelEntries" => Some(temporal_level_entries && !no_default_length), + "TemporalLevelEntriesL" => Some(temporal_level_entries && no_default_length), + "SeigEntries" => Some(seig_entries && !no_default_length), + "SeigEntriesL" => Some(seig_entries && no_default_length), + "Unsupported" => Some( + !roll_distances + && !alternative_startup_entries + && !visual_random_access_entries + && !temporal_level_entries + && !seig_entries, + ), + _ => None, + } + } + + fn display_field(&self, name: &'static str) -> Option { + match name { + "GroupingType" => Some(quoted_fourcc(self.grouping_type)), + "RollDistances" => Some(render_array( + self.roll_distances.iter().map(|distance| distance.to_string()), + )), + "RollDistancesL" => Some(render_array(self.roll_distances_l.iter().map(|entry| { + format!( + "{{DescriptionLength={} RollDistance={}}}", + entry.description_length, entry.roll_distance + ) + }))), + "AlternativeStartupEntries" => Some(render_array( + self.alternative_startup_entries + .iter() + .map(format_alternative_startup_entry), + )), + "AlternativeStartupEntriesL" => Some(render_array( + self.alternative_startup_entries_l.iter().map(|entry| { + format!( + "{{DescriptionLength={} {}}}", + entry.description_length, + format_alternative_startup_entry(&entry.alternative_startup_entry) + .trim_start_matches('{') + .trim_end_matches('}') + ) + }), + )), + "VisualRandomAccessEntries" => Some(render_array( + self.visual_random_access_entries.iter().map(|entry| { + format!( + "{{NumLeadingSamplesKnown={} NumLeadingSamples=0x{:x}}}", + entry.num_leading_samples_known, entry.num_leading_samples + ) + }), + )), + "VisualRandomAccessEntriesL" => Some(render_array( + self.visual_random_access_entries_l.iter().map(|entry| { + format!( + "{{DescriptionLength={} NumLeadingSamplesKnown={} NumLeadingSamples=0x{:x}}}", + entry.description_length, + entry.visual_random_access_entry.num_leading_samples_known, + entry.visual_random_access_entry.num_leading_samples + ) + }), + )), + "TemporalLevelEntries" => Some(render_array( + self.temporal_level_entries.iter().map(|entry| { + format!( + "{{LevelIndependentlyDecodable={}}}", + entry.level_independently_decodable + ) + }), + )), + "TemporalLevelEntriesL" => Some(render_array( + self.temporal_level_entries_l.iter().map(|entry| { + format!( + "{{DescriptionLength={} LevelIndependentlyDecodable={}}}", + entry.description_length, + entry.temporal_level_entry.level_independently_decodable + ) + }), + )), + "SeigEntries" => Some(render_array( + self.seig_entries.iter().map(format_seig_entry), + )), + "SeigEntriesL" => Some(render_array(self.seig_entries_l.iter().map(|entry| { + format!( + "{{DescriptionLength={} {}}}", + entry.description_length, + format_seig_entry(&entry.seig_entry) + .trim_start_matches('{') + .trim_end_matches('}') + ) + }))), + _ => None, + } + } +} + +impl_full_box!(Sgpd, *b"sgpd"); + +impl FieldValueRead for Sgpd { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "GroupingType" => Ok(FieldValue::Bytes(self.grouping_type.as_bytes().to_vec())), + "DefaultLength" => Ok(FieldValue::Unsigned(u64::from(self.default_length))), + "DefaultSampleDescriptionIndex" => Ok(FieldValue::Unsigned(u64::from( + self.default_sample_description_index, + ))), + "EntryCount" => Ok(FieldValue::Unsigned(u64::from(self.entry_count))), + "RollDistances" => { + require_count(field_name, self.entry_count, self.roll_distances.len())?; + let mut bytes = Vec::with_capacity(self.roll_distances.len() * 2); + for roll_distance in &self.roll_distances { + bytes.extend_from_slice(&roll_distance.to_be_bytes()); + } + Ok(FieldValue::Bytes(bytes)) + } + "RollDistancesL" => { + require_count(field_name, self.entry_count, self.roll_distances_l.len())?; + let mut bytes = Vec::with_capacity(self.roll_distances_l.len() * 6); + for entry in &self.roll_distances_l { + bytes.extend_from_slice(&entry.description_length.to_be_bytes()); + bytes.extend_from_slice(&entry.roll_distance.to_be_bytes()); + } + Ok(FieldValue::Bytes(bytes)) + } + "AlternativeStartupEntries" => { + require_count( + field_name, + self.entry_count, + self.alternative_startup_entries.len(), + )?; + let mut bytes = Vec::new(); + for entry in &self.alternative_startup_entries { + let encoded = encode_alternative_startup_entry(field_name, entry)?; + if self.default_length != 0 && encoded.len() != self.default_length as usize { + return Err(invalid_value( + field_name, + "alternative startup entry does not match the default length", + )); + } + bytes.extend_from_slice(&encoded); + } + Ok(FieldValue::Bytes(bytes)) + } + "AlternativeStartupEntriesL" => { + require_count( + field_name, + self.entry_count, + self.alternative_startup_entries_l.len(), + )?; + let mut bytes = Vec::new(); + for entry in &self.alternative_startup_entries_l { + let encoded = encode_alternative_startup_entry( + field_name, + &entry.alternative_startup_entry, + )?; + if encoded.len() != entry.description_length as usize { + return Err(invalid_value( + field_name, + "alternative startup entry length does not match the description length", + )); + } + bytes.extend_from_slice(&entry.description_length.to_be_bytes()); + bytes.extend_from_slice(&encoded); + } + Ok(FieldValue::Bytes(bytes)) + } + "VisualRandomAccessEntries" => { + require_count( + field_name, + self.entry_count, + self.visual_random_access_entries.len(), + )?; + let mut bytes = Vec::with_capacity(self.visual_random_access_entries.len()); + for entry in &self.visual_random_access_entries { + bytes.push(encode_visual_random_access_entry(field_name, entry)?); + } + Ok(FieldValue::Bytes(bytes)) + } + "VisualRandomAccessEntriesL" => { + require_count( + field_name, + self.entry_count, + self.visual_random_access_entries_l.len(), + )?; + let mut bytes = Vec::new(); + for entry in &self.visual_random_access_entries_l { + if entry.description_length != 1 { + return Err(invalid_value( + field_name, + "visual random access entries with explicit lengths must be 1 byte", + )); + } + bytes.extend_from_slice(&entry.description_length.to_be_bytes()); + bytes.push(encode_visual_random_access_entry( + field_name, + &entry.visual_random_access_entry, + )?); + } + Ok(FieldValue::Bytes(bytes)) + } + "TemporalLevelEntries" => { + require_count( + field_name, + self.entry_count, + self.temporal_level_entries.len(), + )?; + Ok(FieldValue::Bytes( + self.temporal_level_entries + .iter() + .map(encode_temporal_level_entry) + .collect(), + )) + } + "TemporalLevelEntriesL" => { + require_count( + field_name, + self.entry_count, + self.temporal_level_entries_l.len(), + )?; + let mut bytes = Vec::new(); + for entry in &self.temporal_level_entries_l { + if entry.description_length != 1 { + return Err(invalid_value( + field_name, + "temporal level entries with explicit lengths must be 1 byte", + )); + } + bytes.extend_from_slice(&entry.description_length.to_be_bytes()); + bytes.push(encode_temporal_level_entry(&entry.temporal_level_entry)); + } + Ok(FieldValue::Bytes(bytes)) + } + "SeigEntries" => { + require_count(field_name, self.entry_count, self.seig_entries.len())?; + let mut bytes = Vec::new(); + for entry in &self.seig_entries { + let encoded = encode_seig_entry(field_name, entry)?; + if self.version() == 1 + && self.default_length != 0 + && encoded.len() != self.default_length as usize + { + return Err(invalid_value( + field_name, + "seig entry does not match the default length", + )); + } + bytes.extend_from_slice(&encoded); + } + Ok(FieldValue::Bytes(bytes)) + } + "SeigEntriesL" => { + require_count(field_name, self.entry_count, self.seig_entries_l.len())?; + let mut bytes = Vec::new(); + for entry in &self.seig_entries_l { + let encoded = encode_seig_entry(field_name, &entry.seig_entry)?; + if encoded.len() != entry.description_length as usize { + return Err(invalid_value( + field_name, + "seig entry length does not match the description length", + )); + } + bytes.extend_from_slice(&entry.description_length.to_be_bytes()); + bytes.extend_from_slice(&encoded); + } + Ok(FieldValue::Bytes(bytes)) + } + "Unsupported" => Ok(FieldValue::Bytes(self.unsupported.clone())), + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Sgpd { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("GroupingType", FieldValue::Bytes(bytes)) => { + self.grouping_type = bytes_to_fourcc(field_name, bytes)?; + Ok(()) + } + ("DefaultLength", FieldValue::Unsigned(value)) => { + self.default_length = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("DefaultSampleDescriptionIndex", FieldValue::Unsigned(value)) => { + self.default_sample_description_index = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("EntryCount", FieldValue::Unsigned(value)) => { + self.entry_count = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("RollDistances", FieldValue::Bytes(bytes)) => { + let expected_len = field_len_from_count(self.entry_count, 2) + .map(|len| len as usize) + .unwrap_or(0); + if bytes.len() != expected_len { + return Err(invalid_value( + field_name, + "roll distance payload length does not match the entry count", + )); + } + self.roll_distances = + parse_fixed_chunks(field_name, &bytes, 2, |chunk| read_i16(chunk, 0))?; + Ok(()) + } + ("RollDistancesL", FieldValue::Bytes(bytes)) => { + let expected_len = field_len_from_count(self.entry_count, 6) + .map(|len| len as usize) + .unwrap_or(0); + if bytes.len() != expected_len { + return Err(invalid_value( + field_name, + "roll distance payload length does not match the entry count", + )); + } + self.roll_distances_l = + parse_fixed_chunks(field_name, &bytes, 6, |chunk| RollDistanceWithLength { + description_length: read_u32(chunk, 0), + roll_distance: read_i16(chunk, 4), + })?; + Ok(()) + } + ("AlternativeStartupEntries", FieldValue::Bytes(bytes)) => { + let entry_len = usize::try_from(self.default_length) + .map_err(|_| invalid_value(field_name, "default length is too large"))?; + if entry_len == 0 { + return Err(invalid_value( + field_name, + "default length must be non-zero for alternative startup entries", + )); + } + let expected_len = field_len_from_count(self.entry_count, entry_len) + .map(|len| len as usize) + .unwrap_or(0); + if bytes.len() != expected_len { + return Err(invalid_value( + field_name, + "alternative startup payload length does not match the entry count", + )); + } + self.alternative_startup_entries = bytes + .chunks_exact(entry_len) + .map(|chunk| parse_alternative_startup_entry(field_name, chunk)) + .collect::, _>>()?; + Ok(()) + } + ("AlternativeStartupEntriesL", FieldValue::Bytes(bytes)) => { + let mut cursor = 0; + let mut entries = Vec::new(); + while cursor < bytes.len() { + if bytes.len() - cursor < 4 { + return Err(invalid_value( + field_name, + "alternative startup entry length prefix is truncated", + )); + } + let description_length = read_u32(&bytes, cursor); + cursor += 4; + let description_len = usize::try_from(description_length).map_err(|_| { + invalid_value(field_name, "alternative startup description is too large") + })?; + if bytes.len() - cursor < description_len { + return Err(invalid_value( + field_name, + "alternative startup entry exceeds the remaining payload", + )); + } + let payload = &bytes[cursor..cursor + description_len]; + cursor += description_len; + entries.push(AlternativeStartupEntryL { + description_length, + alternative_startup_entry: parse_alternative_startup_entry( + field_name, payload, + )?, + }); + } + require_count(field_name, self.entry_count, entries.len())?; + self.alternative_startup_entries_l = entries; + Ok(()) + } + ("VisualRandomAccessEntries", FieldValue::Bytes(bytes)) => { + require_count(field_name, self.entry_count, bytes.len())?; + self.visual_random_access_entries = bytes + .into_iter() + .map(parse_visual_random_access_entry) + .collect(); + Ok(()) + } + ("VisualRandomAccessEntriesL", FieldValue::Bytes(bytes)) => { + let mut cursor = 0; + let mut entries = Vec::new(); + while cursor < bytes.len() { + if bytes.len() - cursor < 5 { + return Err(invalid_value( + field_name, + "visual random access entry is truncated", + )); + } + let description_length = read_u32(&bytes, cursor); + cursor += 4; + if description_length != 1 { + return Err(invalid_value( + field_name, + "visual random access entries with explicit lengths must be 1 byte", + )); + } + entries.push(VisualRandomAccessEntryL { + description_length, + visual_random_access_entry: parse_visual_random_access_entry(bytes[cursor]), + }); + cursor += 1; + } + require_count(field_name, self.entry_count, entries.len())?; + self.visual_random_access_entries_l = entries; + Ok(()) + } + ("TemporalLevelEntries", FieldValue::Bytes(bytes)) => { + require_count(field_name, self.entry_count, bytes.len())?; + self.temporal_level_entries = bytes + .into_iter() + .map(|byte| parse_temporal_level_entry(field_name, byte)) + .collect::, _>>()?; + Ok(()) + } + ("TemporalLevelEntriesL", FieldValue::Bytes(bytes)) => { + let mut cursor = 0; + let mut entries = Vec::new(); + while cursor < bytes.len() { + if bytes.len() - cursor < 5 { + return Err(invalid_value( + field_name, + "temporal level entry is truncated", + )); + } + let description_length = read_u32(&bytes, cursor); + cursor += 4; + if description_length != 1 { + return Err(invalid_value( + field_name, + "temporal level entries with explicit lengths must be 1 byte", + )); + } + entries.push(TemporalLevelEntryL { + description_length, + temporal_level_entry: parse_temporal_level_entry( + field_name, + bytes[cursor], + )?, + }); + cursor += 1; + } + require_count(field_name, self.entry_count, entries.len())?; + self.temporal_level_entries_l = entries; + Ok(()) + } + ("SeigEntries", FieldValue::Bytes(bytes)) => { + if self.version() == 1 { + let entry_len = usize::try_from(self.default_length) + .map_err(|_| invalid_value(field_name, "default length is too large"))?; + if entry_len == 0 { + return Err(invalid_value( + field_name, + "default length must be non-zero for seig entries", + )); + } + let expected_len = field_len_from_count(self.entry_count, entry_len) + .map(|len| len as usize) + .unwrap_or(0); + if bytes.len() != expected_len { + return Err(invalid_value( + field_name, + "seig payload length does not match the entry count", + )); + } + self.seig_entries = bytes + .chunks_exact(entry_len) + .map(|chunk| parse_seig_entry_exact(field_name, chunk)) + .collect::, _>>()?; + return Ok(()); + } + + let mut cursor = 0; + let mut entries = Vec::new(); + while cursor < bytes.len() { + let (entry, consumed) = parse_seig_entry(field_name, &bytes[cursor..])?; + cursor += consumed; + entries.push(entry); + } + require_count(field_name, self.entry_count, entries.len())?; + self.seig_entries = entries; + Ok(()) + } + ("SeigEntriesL", FieldValue::Bytes(bytes)) => { + let mut cursor = 0; + let mut entries = Vec::new(); + while cursor < bytes.len() { + if bytes.len() - cursor < 4 { + return Err(invalid_value( + field_name, + "seig entry length prefix is truncated", + )); + } + let description_length = read_u32(&bytes, cursor); + cursor += 4; + let description_len = usize::try_from(description_length) + .map_err(|_| invalid_value(field_name, "seig description is too large"))?; + if bytes.len() - cursor < description_len { + return Err(invalid_value( + field_name, + "seig entry exceeds the remaining payload", + )); + } + let payload = &bytes[cursor..cursor + description_len]; + cursor += description_len; + entries.push(SeigEntryL { + description_length, + seig_entry: parse_seig_entry_exact(field_name, payload)?, + }); + } + require_count(field_name, self.entry_count, entries.len())?; + self.seig_entries_l = entries; + Ok(()) + } + ("Unsupported", FieldValue::Bytes(bytes)) => { + self.unsupported = bytes; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Sgpd { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!( + "GroupingType", + 2, + with_bit_width(8), + with_length(4), + as_bytes() + ), + codec_field!("DefaultLength", 3, with_bit_width(32), with_version(1)), + codec_field!( + "DefaultSampleDescriptionIndex", + 4, + with_bit_width(32), + with_version(2) + ), + codec_field!("EntryCount", 5, with_bit_width(32)), + codec_field!( + "RollDistances", + 6, + with_bit_width(8), + as_bytes(), + with_dynamic_presence() + ), + codec_field!( + "RollDistancesL", + 7, + with_bit_width(8), + as_bytes(), + with_dynamic_presence() + ), + codec_field!( + "AlternativeStartupEntries", + 8, + with_bit_width(8), + as_bytes(), + with_dynamic_presence() + ), + codec_field!( + "AlternativeStartupEntriesL", + 9, + with_bit_width(8), + as_bytes(), + with_dynamic_presence() + ), + codec_field!( + "VisualRandomAccessEntries", + 10, + with_bit_width(8), + as_bytes(), + with_dynamic_presence() + ), + codec_field!( + "VisualRandomAccessEntriesL", + 11, + with_bit_width(8), + as_bytes(), + with_dynamic_presence() + ), + codec_field!( + "TemporalLevelEntries", + 12, + with_bit_width(8), + as_bytes(), + with_dynamic_presence() + ), + codec_field!( + "TemporalLevelEntriesL", + 13, + with_bit_width(8), + as_bytes(), + with_dynamic_presence() + ), + codec_field!( + "SeigEntries", + 14, + with_bit_width(8), + as_bytes(), + with_dynamic_presence() + ), + codec_field!( + "SeigEntriesL", + 15, + with_bit_width(8), + as_bytes(), + with_dynamic_presence() + ), + codec_field!( + "Unsupported", + 16, + with_bit_width(8), + as_bytes(), + with_dynamic_presence() + ), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[1, 2]; +} + +/// One indexed byte range inside an [`SsixSubsegment`]. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SsixRange { + pub level: u8, + pub range_size: u32, +} + +/// One subsegment entry inside [`Ssix`]. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SsixSubsegment { + pub range_count: u32, + pub ranges: Vec, +} + +/// Subsegment-index box. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Ssix { + full_box: FullBoxState, + pub subsegment_count: u32, + pub subsegments: Vec, +} + +fn format_ssix_subsegments(subsegments: &[SsixSubsegment]) -> String { + render_array(subsegments.iter().map(|subsegment| { + format!( + "{{RangeCount={} Ranges={}}}", + subsegment.range_count, + render_array(subsegment.ranges.iter().map(|range| { + format!("{{Level={} RangeSize={}}}", range.level, range.range_size) + })) + ) + })) +} + +fn encode_ssix_subsegments( + field_name: &'static str, + subsegments: &[SsixSubsegment], +) -> Result, FieldValueError> { + let mut bytes = Vec::new(); + for subsegment in subsegments { + require_count(field_name, subsegment.range_count, subsegment.ranges.len())?; + bytes.extend_from_slice(&subsegment.range_count.to_be_bytes()); + for range in &subsegment.ranges { + if range.range_size > 0x00ff_ffff { + return Err(invalid_value( + field_name, + "range size does not fit in 24 bits", + )); + } + bytes.push(range.level); + push_uint(field_name, &mut bytes, 3, u64::from(range.range_size))?; + } + } + Ok(bytes) +} + +fn parse_ssix_subsegments( + field_name: &'static str, + subsegment_count: u32, + bytes: &[u8], +) -> Result, FieldValueError> { + let mut subsegments = Vec::with_capacity(untrusted_prealloc_hint( + usize::try_from(subsegment_count).unwrap_or(0), + )); + let mut offset = 0usize; + + for _ in 0..subsegment_count { + if bytes.len().saturating_sub(offset) < 4 { + return Err(invalid_value(field_name, "subsegment payload is truncated")); + } + + let range_count = read_u32(bytes, offset); + offset += 4; + let mut ranges = Vec::with_capacity(untrusted_prealloc_hint( + usize::try_from(range_count).unwrap_or(0), + )); + for _ in 0..range_count { + if bytes.len().saturating_sub(offset) < 4 { + return Err(invalid_value(field_name, "range payload is truncated")); + } + + ranges.push(SsixRange { + level: bytes[offset], + range_size: read_uint(bytes, offset + 1, 3) as u32, + }); + offset += 4; + } + + subsegments.push(SsixSubsegment { + range_count, + ranges, + }); + } + + if offset != bytes.len() { + return Err(invalid_value( + field_name, + "subsegment payload length does not match the subsegment count", + )); + } + + Ok(subsegments) +} + +impl FieldHooks for Ssix { + fn display_field(&self, name: &'static str) -> Option { + match name { + "Subsegments" => Some(format_ssix_subsegments(&self.subsegments)), + _ => None, + } + } +} + +impl_full_box!(Ssix, *b"ssix"); + +impl FieldValueRead for Ssix { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "SubsegmentCount" => Ok(FieldValue::Unsigned(u64::from(self.subsegment_count))), + "Subsegments" => { + require_count(field_name, self.subsegment_count, self.subsegments.len())?; + Ok(FieldValue::Bytes(encode_ssix_subsegments( + field_name, + &self.subsegments, + )?)) + } + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Ssix { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("SubsegmentCount", FieldValue::Unsigned(value)) => { + self.subsegment_count = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("Subsegments", FieldValue::Bytes(bytes)) => { + self.subsegments = + parse_ssix_subsegments(field_name, self.subsegment_count, &bytes)?; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Ssix { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("SubsegmentCount", 2, with_bit_width(32)), + codec_field!("Subsegments", 3, with_bit_width(8), as_bytes()), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; +} + +/// One segment index reference entry. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SidxReference { + pub reference_type: bool, + pub referenced_size: u32, + pub subsegment_duration: u32, + pub starts_with_sap: bool, + pub sap_type: u32, + pub sap_delta_time: u32, +} + +/// Segment index box. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Sidx { + full_box: FullBoxState, + pub reference_id: u32, + pub timescale: u32, + pub earliest_presentation_time_v0: u32, + pub first_offset_v0: u32, + pub earliest_presentation_time_v1: u64, + pub first_offset_v1: u64, + pub reference_count: u16, + pub references: Vec, +} + +impl FieldHooks for Sidx { + fn display_field(&self, name: &'static str) -> Option { + match name { + "References" => Some(render_array(self.references.iter().map(|entry| { + format!( + "{{ReferenceType={} ReferencedSize={} SubsegmentDuration={} StartsWithSAP={} SAPType={} SAPDeltaTime={}}}", + entry.reference_type, + entry.referenced_size, + entry.subsegment_duration, + entry.starts_with_sap, + entry.sap_type, + entry.sap_delta_time + ) + }))), + _ => None, + } + } +} + +impl_full_box!(Sidx, *b"sidx"); + +impl Sidx { + /// Returns the active earliest presentation time for the current box version. + pub fn earliest_presentation_time(&self) -> u64 { + match self.version() { + 0 => u64::from(self.earliest_presentation_time_v0), + 1 => self.earliest_presentation_time_v1, + _ => 0, + } + } + + /// Returns the active first offset for the current box version. + pub fn first_offset(&self) -> u64 { + match self.version() { + 0 => u64::from(self.first_offset_v0), + 1 => self.first_offset_v1, + _ => 0, + } + } +} + +impl FieldValueRead for Sidx { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "ReferenceID" => Ok(FieldValue::Unsigned(u64::from(self.reference_id))), + "Timescale" => Ok(FieldValue::Unsigned(u64::from(self.timescale))), + "EarliestPresentationTimeV0" => Ok(FieldValue::Unsigned(u64::from( + self.earliest_presentation_time_v0, + ))), + "FirstOffsetV0" => Ok(FieldValue::Unsigned(u64::from(self.first_offset_v0))), + "EarliestPresentationTimeV1" => { + Ok(FieldValue::Unsigned(self.earliest_presentation_time_v1)) + } + "FirstOffsetV1" => Ok(FieldValue::Unsigned(self.first_offset_v1)), + "ReferenceCount" => Ok(FieldValue::Unsigned(u64::from(self.reference_count))), + "References" => { + require_count( + field_name, + u32::from(self.reference_count), + self.references.len(), + )?; + let mut bytes = Vec::with_capacity(self.references.len() * 12); + for entry in &self.references { + if entry.referenced_size > 0x7fff_ffff { + return Err(invalid_value( + field_name, + "referenced size does not fit in 31 bits", + )); + } + if entry.sap_type > 0x07 { + return Err(invalid_value(field_name, "SAP type does not fit in 3 bits")); + } + if entry.sap_delta_time > 0x0fff_ffff { + return Err(invalid_value( + field_name, + "SAP delta time does not fit in 28 bits", + )); + } + + // The reference and SAP records pack their high-bit flags into the same 32-bit words as the payload values. + let reference_word = + (u32::from(entry.reference_type) << 31) | entry.referenced_size; + let sap_word = (u32::from(entry.starts_with_sap) << 31) + | (entry.sap_type << 28) + | entry.sap_delta_time; + bytes.extend_from_slice(&reference_word.to_be_bytes()); + bytes.extend_from_slice(&entry.subsegment_duration.to_be_bytes()); + bytes.extend_from_slice(&sap_word.to_be_bytes()); + } + Ok(FieldValue::Bytes(bytes)) + } + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Sidx { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("ReferenceID", FieldValue::Unsigned(value)) => { + self.reference_id = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("Timescale", FieldValue::Unsigned(value)) => { + self.timescale = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("EarliestPresentationTimeV0", FieldValue::Unsigned(value)) => { + self.earliest_presentation_time_v0 = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("FirstOffsetV0", FieldValue::Unsigned(value)) => { + self.first_offset_v0 = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("EarliestPresentationTimeV1", FieldValue::Unsigned(value)) => { + self.earliest_presentation_time_v1 = value; + Ok(()) + } + ("FirstOffsetV1", FieldValue::Unsigned(value)) => { + self.first_offset_v1 = value; + Ok(()) + } + ("ReferenceCount", FieldValue::Unsigned(value)) => { + self.reference_count = u16_from_unsigned(field_name, value)?; + Ok(()) + } + ("References", FieldValue::Bytes(bytes)) => { + let expected_len = field_len_from_count(u32::from(self.reference_count), 12) + .map(|len| len as usize) + .unwrap_or(0); + if bytes.len() != expected_len { + return Err(invalid_value( + field_name, + "reference payload length does not match the reference count", + )); + } + + self.references = + parse_fixed_chunks(field_name, &bytes, 12, |chunk| SidxReference { + reference_type: read_u32(chunk, 0) & 0x8000_0000 != 0, + referenced_size: read_u32(chunk, 0) & 0x7fff_ffff, + subsegment_duration: read_u32(chunk, 4), + starts_with_sap: read_u32(chunk, 8) & 0x8000_0000 != 0, + sap_type: (read_u32(chunk, 8) >> 28) & 0x07, + sap_delta_time: read_u32(chunk, 8) & 0x0fff_ffff, + })?; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Sidx { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("ReferenceID", 2, with_bit_width(32)), + codec_field!("Timescale", 3, with_bit_width(32)), + codec_field!( + "EarliestPresentationTimeV0", + 4, + with_bit_width(32), + with_version(0) + ), + codec_field!("FirstOffsetV0", 5, with_bit_width(32), with_version(0)), + codec_field!( + "EarliestPresentationTimeV1", + 6, + with_bit_width(64), + with_version(1) + ), + codec_field!("FirstOffsetV1", 7, with_bit_width(64), with_version(1)), + codec_field!("Reserved", 8, with_bit_width(16), with_constant("0")), + codec_field!("ReferenceCount", 9, with_bit_width(16)), + codec_field!("References", 10, with_bit_width(8), as_bytes()), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; +} + +/// One track-fragment random-access entry. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TfraEntry { + pub time_v0: u32, + pub moof_offset_v0: u32, + pub time_v1: u64, + pub moof_offset_v1: u64, + pub traf_number: u32, + pub trun_number: u32, + pub sample_number: u32, +} + +/// Track fragment random access box. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Tfra { + full_box: FullBoxState, + pub track_id: u32, + pub length_size_of_traf_num: u8, + pub length_size_of_trun_num: u8, + pub length_size_of_sample_num: u8, + pub number_of_entry: u32, + pub entries: Vec, +} + +impl Tfra { + fn entry_size_bytes(&self) -> usize { + // Each stored length field is encoded as "size minus one", so add one byte to recover the actual width. + let traf_bytes = usize::from(self.length_size_of_traf_num) + 1; + let trun_bytes = usize::from(self.length_size_of_trun_num) + 1; + let sample_bytes = usize::from(self.length_size_of_sample_num) + 1; + match self.version() { + 0 => 8 + traf_bytes + trun_bytes + sample_bytes, + 1 => 16 + traf_bytes + trun_bytes + sample_bytes, + _ => traf_bytes + trun_bytes + sample_bytes, + } + } + + /// Returns the active random-access time for `index`. + pub fn time(&self, index: usize) -> u64 { + match self.version() { + 0 => u64::from(self.entries[index].time_v0), + 1 => self.entries[index].time_v1, + _ => 0, + } + } + + /// Returns the active `moof` offset for `index`. + pub fn moof_offset(&self, index: usize) -> u64 { + match self.version() { + 0 => u64::from(self.entries[index].moof_offset_v0), + 1 => self.entries[index].moof_offset_v1, + _ => 0, + } + } +} + +impl FieldHooks for Tfra { + fn display_field(&self, name: &'static str) -> Option { + match name { + "Entries" => Some(render_array(self.entries.iter().map(|entry| { + if self.version() == 0 { + format!( + "{{TimeV0={} MoofOffsetV0={} TrafNumber={} TrunNumber={} SampleNumber={}}}", + entry.time_v0, + entry.moof_offset_v0, + entry.traf_number, + entry.trun_number, + entry.sample_number + ) + } else { + format!( + "{{TimeV1={} MoofOffsetV1={} TrafNumber={} TrunNumber={} SampleNumber={}}}", + entry.time_v1, + entry.moof_offset_v1, + entry.traf_number, + entry.trun_number, + entry.sample_number + ) + } + }))), + _ => None, + } + } +} + +impl_full_box!(Tfra, *b"tfra"); + +impl FieldValueRead for Tfra { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "TrackID" => Ok(FieldValue::Unsigned(u64::from(self.track_id))), + "LengthSizeOfTrafNum" => Ok(FieldValue::Unsigned(u64::from( + self.length_size_of_traf_num, + ))), + "LengthSizeOfTrunNum" => Ok(FieldValue::Unsigned(u64::from( + self.length_size_of_trun_num, + ))), + "LengthSizeOfSampleNum" => Ok(FieldValue::Unsigned(u64::from( + self.length_size_of_sample_num, + ))), + "NumberOfEntry" => Ok(FieldValue::Unsigned(u64::from(self.number_of_entry))), + "Entries" => { + require_count(field_name, self.number_of_entry, self.entries.len())?; + let traf_bytes = usize::from(self.length_size_of_traf_num) + 1; + let trun_bytes = usize::from(self.length_size_of_trun_num) + 1; + let sample_bytes = usize::from(self.length_size_of_sample_num) + 1; + let mut bytes = Vec::with_capacity(self.entries.len() * self.entry_size_bytes()); + for entry in &self.entries { + if self.version() == 0 { + bytes.extend_from_slice(&entry.time_v0.to_be_bytes()); + bytes.extend_from_slice(&entry.moof_offset_v0.to_be_bytes()); + } else { + bytes.extend_from_slice(&entry.time_v1.to_be_bytes()); + bytes.extend_from_slice(&entry.moof_offset_v1.to_be_bytes()); } - bytes.extend_from_slice(&entry.description_length.to_be_bytes()); - bytes.push(encode_temporal_level_entry(&entry.temporal_level_entry)); + push_uint( + field_name, + &mut bytes, + traf_bytes, + u64::from(entry.traf_number), + )?; + push_uint( + field_name, + &mut bytes, + trun_bytes, + u64::from(entry.trun_number), + )?; + push_uint( + field_name, + &mut bytes, + sample_bytes, + u64::from(entry.sample_number), + )?; } Ok(FieldValue::Bytes(bytes)) } - "Unsupported" => Ok(FieldValue::Bytes(self.unsupported.clone())), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Sgpd { +impl FieldValueWrite for Tfra { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("GroupingType", FieldValue::Bytes(bytes)) => { - self.grouping_type = bytes_to_fourcc(field_name, bytes)?; + ("TrackID", FieldValue::Unsigned(value)) => { + self.track_id = u32_from_unsigned(field_name, value)?; Ok(()) } - ("DefaultLength", FieldValue::Unsigned(value)) => { - self.default_length = u32_from_unsigned(field_name, value)?; + ("LengthSizeOfTrafNum", FieldValue::Unsigned(value)) => { + self.length_size_of_traf_num = u8_from_unsigned(field_name, value)?; Ok(()) } - ("DefaultSampleDescriptionIndex", FieldValue::Unsigned(value)) => { - self.default_sample_description_index = u32_from_unsigned(field_name, value)?; + ("LengthSizeOfTrunNum", FieldValue::Unsigned(value)) => { + self.length_size_of_trun_num = u8_from_unsigned(field_name, value)?; Ok(()) } - ("EntryCount", FieldValue::Unsigned(value)) => { - self.entry_count = u32_from_unsigned(field_name, value)?; + ("LengthSizeOfSampleNum", FieldValue::Unsigned(value)) => { + self.length_size_of_sample_num = u8_from_unsigned(field_name, value)?; Ok(()) } - ("RollDistances", FieldValue::Bytes(bytes)) => { - let expected_len = field_len_from_count(self.entry_count, 2) - .map(|len| len as usize) - .unwrap_or(0); - if bytes.len() != expected_len { - return Err(invalid_value( - field_name, - "roll distance payload length does not match the entry count", - )); - } - self.roll_distances = - parse_fixed_chunks(field_name, &bytes, 2, |chunk| read_i16(chunk, 0))?; + ("NumberOfEntry", FieldValue::Unsigned(value)) => { + self.number_of_entry = u32_from_unsigned(field_name, value)?; Ok(()) } - ("RollDistancesL", FieldValue::Bytes(bytes)) => { - let expected_len = field_len_from_count(self.entry_count, 6) + ("Entries", FieldValue::Bytes(bytes)) => { + let entry_size = self.entry_size_bytes(); + let expected_len = field_len_from_count(self.number_of_entry, entry_size) .map(|len| len as usize) .unwrap_or(0); if bytes.len() != expected_len { return Err(invalid_value( field_name, - "roll distance payload length does not match the entry count", + "random access payload length does not match the entry count", )); } - self.roll_distances_l = - parse_fixed_chunks(field_name, &bytes, 6, |chunk| RollDistanceWithLength { - description_length: read_u32(chunk, 0), - roll_distance: read_i16(chunk, 4), - })?; + + let traf_bytes = usize::from(self.length_size_of_traf_num) + 1; + let trun_bytes = usize::from(self.length_size_of_trun_num) + 1; + let sample_bytes = usize::from(self.length_size_of_sample_num) + 1; + self.entries = bytes + .chunks_exact(entry_size) + .map(|chunk| { + let mut offset = 0; + let mut entry = TfraEntry::default(); + if self.version() == 0 { + entry.time_v0 = read_u32(chunk, offset); + offset += 4; + entry.moof_offset_v0 = read_u32(chunk, offset); + offset += 4; + } else { + entry.time_v1 = read_u64(chunk, offset); + offset += 8; + entry.moof_offset_v1 = read_u64(chunk, offset); + offset += 8; + } + entry.traf_number = + u32_from_unsigned(field_name, read_uint(chunk, offset, traf_bytes))?; + offset += traf_bytes; + entry.trun_number = + u32_from_unsigned(field_name, read_uint(chunk, offset, trun_bytes))?; + offset += trun_bytes; + entry.sample_number = + u32_from_unsigned(field_name, read_uint(chunk, offset, sample_bytes))?; + Ok(entry) + }) + .collect::, FieldValueError>>()?; Ok(()) } - ("AlternativeStartupEntries", FieldValue::Bytes(bytes)) => { - let entry_len = usize::try_from(self.default_length) - .map_err(|_| invalid_value(field_name, "default length is too large"))?; - if entry_len == 0 { - return Err(invalid_value( - field_name, - "default length must be non-zero for alternative startup entries", - )); - } - let expected_len = field_len_from_count(self.entry_count, entry_len) - .map(|len| len as usize) - .unwrap_or(0); - if bytes.len() != expected_len { - return Err(invalid_value( - field_name, - "alternative startup payload length does not match the entry count", - )); - } - self.alternative_startup_entries = bytes - .chunks_exact(entry_len) - .map(|chunk| parse_alternative_startup_entry(field_name, chunk)) - .collect::, _>>()?; + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Tfra { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("TrackID", 2, with_bit_width(32)), + codec_field!("Reserved", 3, with_bit_width(26), with_constant("0")), + codec_field!("LengthSizeOfTrafNum", 4, with_bit_width(2), as_hex()), + codec_field!("LengthSizeOfTrunNum", 5, with_bit_width(2), as_hex()), + codec_field!("LengthSizeOfSampleNum", 6, with_bit_width(2), as_hex()), + codec_field!("NumberOfEntry", 7, with_bit_width(32)), + codec_field!("Entries", 8, with_bit_width(8), as_bytes()), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; +} + +/// Bitrate declaration box for sample entries. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Btrt { + pub buffer_size_db: u32, + pub max_bitrate: u32, + pub avg_bitrate: u32, +} + +impl_leaf_box!(Btrt, *b"btrt"); + +impl FieldValueRead for Btrt { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "BufferSizeDB" => Ok(FieldValue::Unsigned(u64::from(self.buffer_size_db))), + "MaxBitrate" => Ok(FieldValue::Unsigned(u64::from(self.max_bitrate))), + "AvgBitrate" => Ok(FieldValue::Unsigned(u64::from(self.avg_bitrate))), + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Btrt { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("BufferSizeDB", FieldValue::Unsigned(value)) => { + self.buffer_size_db = u32_from_unsigned(field_name, value)?; Ok(()) } - ("AlternativeStartupEntriesL", FieldValue::Bytes(bytes)) => { - let mut cursor = 0; - let mut entries = Vec::new(); - while cursor < bytes.len() { - if bytes.len() - cursor < 4 { - return Err(invalid_value( - field_name, - "alternative startup entry length prefix is truncated", - )); - } - let description_length = read_u32(&bytes, cursor); - cursor += 4; - let description_len = usize::try_from(description_length).map_err(|_| { - invalid_value(field_name, "alternative startup description is too large") - })?; - if bytes.len() - cursor < description_len { - return Err(invalid_value( - field_name, - "alternative startup entry exceeds the remaining payload", - )); - } - let payload = &bytes[cursor..cursor + description_len]; - cursor += description_len; - entries.push(AlternativeStartupEntryL { - description_length, - alternative_startup_entry: parse_alternative_startup_entry( - field_name, payload, - )?, - }); - } - require_count(field_name, self.entry_count, entries.len())?; - self.alternative_startup_entries_l = entries; + ("MaxBitrate", FieldValue::Unsigned(value)) => { + self.max_bitrate = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("AvgBitrate", FieldValue::Unsigned(value)) => { + self.avg_bitrate = u32_from_unsigned(field_name, value)?; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Btrt { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("BufferSizeDB", 0, with_bit_width(32)), + codec_field!("MaxBitrate", 1, with_bit_width(32)), + codec_field!("AvgBitrate", 2, with_bit_width(32)), + ]); +} + +/// Clean-aperture box that refines the displayed picture region for a visual sample entry. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Clap { + pub clean_aperture_width_n: u32, + pub clean_aperture_width_d: u32, + pub clean_aperture_height_n: u32, + pub clean_aperture_height_d: u32, + pub horiz_off_n: u32, + pub horiz_off_d: u32, + pub vert_off_n: u32, + pub vert_off_d: u32, +} + +impl_leaf_box!(Clap, *b"clap"); + +impl FieldValueRead for Clap { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "CleanApertureWidthN" => { + Ok(FieldValue::Unsigned(u64::from(self.clean_aperture_width_n))) + } + "CleanApertureWidthD" => { + Ok(FieldValue::Unsigned(u64::from(self.clean_aperture_width_d))) + } + "CleanApertureHeightN" => Ok(FieldValue::Unsigned(u64::from( + self.clean_aperture_height_n, + ))), + "CleanApertureHeightD" => Ok(FieldValue::Unsigned(u64::from( + self.clean_aperture_height_d, + ))), + "HorizOffN" => Ok(FieldValue::Unsigned(u64::from(self.horiz_off_n))), + "HorizOffD" => Ok(FieldValue::Unsigned(u64::from(self.horiz_off_d))), + "VertOffN" => Ok(FieldValue::Unsigned(u64::from(self.vert_off_n))), + "VertOffD" => Ok(FieldValue::Unsigned(u64::from(self.vert_off_d))), + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Clap { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("CleanApertureWidthN", FieldValue::Unsigned(value)) => { + self.clean_aperture_width_n = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("CleanApertureWidthD", FieldValue::Unsigned(value)) => { + self.clean_aperture_width_d = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("CleanApertureHeightN", FieldValue::Unsigned(value)) => { + self.clean_aperture_height_n = u32_from_unsigned(field_name, value)?; Ok(()) } - ("VisualRandomAccessEntries", FieldValue::Bytes(bytes)) => { - require_count(field_name, self.entry_count, bytes.len())?; - self.visual_random_access_entries = bytes - .into_iter() - .map(parse_visual_random_access_entry) - .collect(); + ("CleanApertureHeightD", FieldValue::Unsigned(value)) => { + self.clean_aperture_height_d = u32_from_unsigned(field_name, value)?; Ok(()) } - ("VisualRandomAccessEntriesL", FieldValue::Bytes(bytes)) => { - let mut cursor = 0; - let mut entries = Vec::new(); - while cursor < bytes.len() { - if bytes.len() - cursor < 5 { - return Err(invalid_value( - field_name, - "visual random access entry is truncated", - )); - } - let description_length = read_u32(&bytes, cursor); - cursor += 4; - if description_length != 1 { - return Err(invalid_value( - field_name, - "visual random access entries with explicit lengths must be 1 byte", - )); - } - entries.push(VisualRandomAccessEntryL { - description_length, - visual_random_access_entry: parse_visual_random_access_entry(bytes[cursor]), - }); - cursor += 1; - } - require_count(field_name, self.entry_count, entries.len())?; - self.visual_random_access_entries_l = entries; + ("HorizOffN", FieldValue::Unsigned(value)) => { + self.horiz_off_n = u32_from_unsigned(field_name, value)?; Ok(()) } - ("TemporalLevelEntries", FieldValue::Bytes(bytes)) => { - require_count(field_name, self.entry_count, bytes.len())?; - self.temporal_level_entries = bytes - .into_iter() - .map(|byte| parse_temporal_level_entry(field_name, byte)) - .collect::, _>>()?; + ("HorizOffD", FieldValue::Unsigned(value)) => { + self.horiz_off_d = u32_from_unsigned(field_name, value)?; Ok(()) } - ("TemporalLevelEntriesL", FieldValue::Bytes(bytes)) => { - let mut cursor = 0; - let mut entries = Vec::new(); - while cursor < bytes.len() { - if bytes.len() - cursor < 5 { - return Err(invalid_value( - field_name, - "temporal level entry is truncated", - )); - } - let description_length = read_u32(&bytes, cursor); - cursor += 4; - if description_length != 1 { - return Err(invalid_value( - field_name, - "temporal level entries with explicit lengths must be 1 byte", - )); - } - entries.push(TemporalLevelEntryL { - description_length, - temporal_level_entry: parse_temporal_level_entry( - field_name, - bytes[cursor], - )?, - }); - cursor += 1; - } - require_count(field_name, self.entry_count, entries.len())?; - self.temporal_level_entries_l = entries; + ("VertOffN", FieldValue::Unsigned(value)) => { + self.vert_off_n = u32_from_unsigned(field_name, value)?; Ok(()) } - ("Unsupported", FieldValue::Bytes(bytes)) => { - self.unsupported = bytes; + ("VertOffD", FieldValue::Unsigned(value)) => { + self.vert_off_d = u32_from_unsigned(field_name, value)?; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -5078,268 +8640,197 @@ impl FieldValueWrite for Sgpd { } } -impl CodecBox for Sgpd { +impl CodecBox for Clap { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("CleanApertureWidthN", 0, with_bit_width(32)), + codec_field!("CleanApertureWidthD", 1, with_bit_width(32)), + codec_field!("CleanApertureHeightN", 2, with_bit_width(32)), + codec_field!("CleanApertureHeightD", 3, with_bit_width(32)), + codec_field!("HorizOffN", 4, with_bit_width(32)), + codec_field!("HorizOffD", 5, with_bit_width(32)), + codec_field!("VertOffN", 6, with_bit_width(32)), + codec_field!("VertOffD", 7, with_bit_width(32)), + ]); +} + +/// Content-light-level box carried by some visual sample entries. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct CoLL { + full_box: FullBoxState, + pub max_cll: u16, + pub max_fall: u16, +} + +impl FieldHooks for CoLL {} + +impl_full_box!(CoLL, *b"CoLL"); + +impl FieldValueRead for CoLL { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "Version" => Ok(FieldValue::Unsigned(u64::from(self.version()))), + "Flags" => Ok(FieldValue::Unsigned(u64::from(self.flags()))), + "MaxCLL" => Ok(FieldValue::Unsigned(u64::from(self.max_cll))), + "MaxFALL" => Ok(FieldValue::Unsigned(u64::from(self.max_fall))), + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for CoLL { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("Version", FieldValue::Unsigned(value)) => { + self.set_version(u8_from_unsigned(field_name, value)?); + Ok(()) + } + ("Flags", FieldValue::Unsigned(value)) => { + self.set_flags(u32_from_unsigned(field_name, value)?); + Ok(()) + } + ("MaxCLL", FieldValue::Unsigned(value)) => { + self.max_cll = u16_from_unsigned(field_name, value)?; + Ok(()) + } + ("MaxFALL", FieldValue::Unsigned(value)) => { + self.max_fall = u16_from_unsigned(field_name, value)?; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for CoLL { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!( - "GroupingType", - 2, - with_bit_width(8), - with_length(4), - as_bytes() - ), - codec_field!("DefaultLength", 3, with_bit_width(32), with_version(1)), - codec_field!( - "DefaultSampleDescriptionIndex", - 4, - with_bit_width(32), - with_version(2) - ), - codec_field!("EntryCount", 5, with_bit_width(32)), - codec_field!( - "RollDistances", - 6, - with_bit_width(8), - as_bytes(), - with_dynamic_presence() - ), - codec_field!( - "RollDistancesL", - 7, - with_bit_width(8), - as_bytes(), - with_dynamic_presence() - ), - codec_field!( - "AlternativeStartupEntries", - 8, - with_bit_width(8), - as_bytes(), - with_dynamic_presence() - ), - codec_field!( - "AlternativeStartupEntriesL", - 9, - with_bit_width(8), - as_bytes(), - with_dynamic_presence() - ), - codec_field!( - "VisualRandomAccessEntries", - 10, - with_bit_width(8), - as_bytes(), - with_dynamic_presence() - ), - codec_field!( - "VisualRandomAccessEntriesL", - 11, - with_bit_width(8), - as_bytes(), - with_dynamic_presence() - ), - codec_field!( - "TemporalLevelEntries", - 12, - with_bit_width(8), - as_bytes(), - with_dynamic_presence() - ), - codec_field!( - "TemporalLevelEntriesL", - 13, - with_bit_width(8), - as_bytes(), - with_dynamic_presence() - ), - codec_field!( - "Unsupported", - 14, - with_bit_width(8), - as_bytes(), - with_dynamic_presence() - ), + codec_field!("MaxCLL", 2, with_bit_width(16)), + codec_field!("MaxFALL", 3, with_bit_width(16)), ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[1, 2]; + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; } -/// One segment index reference entry. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct SidxReference { - pub reference_type: bool, - pub referenced_size: u32, - pub subsegment_duration: u32, - pub starts_with_sap: bool, - pub sap_type: u32, - pub sap_delta_time: u32, +/// Color information leaf whose active fields depend on the stored colour type. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Colr { + pub colour_type: FourCc, + pub colour_primaries: u16, + pub transfer_characteristics: u16, + pub matrix_coefficients: u16, + pub full_range_flag: bool, + pub reserved: u8, + pub profile: Vec, + pub unknown: Vec, } -/// Segment index box. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Sidx { - full_box: FullBoxState, - pub reference_id: u32, - pub timescale: u32, - pub earliest_presentation_time_v0: u32, - pub first_offset_v0: u32, - pub earliest_presentation_time_v1: u64, - pub first_offset_v1: u64, - pub reference_count: u16, - pub references: Vec, +impl Default for Colr { + fn default() -> Self { + Self { + colour_type: FourCc::ANY, + colour_primaries: 0, + transfer_characteristics: 0, + matrix_coefficients: 0, + full_range_flag: false, + reserved: 0, + profile: Vec::new(), + unknown: Vec::new(), + } + } } -impl FieldHooks for Sidx { - fn display_field(&self, name: &'static str) -> Option { +impl FieldHooks for Colr { + fn field_enabled(&self, name: &'static str) -> Option { match name { - "References" => Some(render_array(self.references.iter().map(|entry| { - format!( - "{{ReferenceType={} ReferencedSize={} SubsegmentDuration={} StartsWithSAP={} SAPType={} SAPDeltaTime={}}}", - entry.reference_type, - entry.referenced_size, - entry.subsegment_duration, - entry.starts_with_sap, - entry.sap_type, - entry.sap_delta_time - ) - }))), + "ColourPrimaries" + | "TransferCharacteristics" + | "MatrixCoefficients" + | "FullRangeFlag" + | "Reserved" => Some(self.colour_type == COLR_NCLX), + "Profile" => Some(matches!(self.colour_type, COLR_RICC | COLR_PROF)), + "Unknown" => Some(!matches!( + self.colour_type, + COLR_NCLX | COLR_RICC | COLR_PROF + )), _ => None, } } -} - -impl_full_box!(Sidx, *b"sidx"); -impl Sidx { - /// Returns the active earliest presentation time for the current box version. - pub fn earliest_presentation_time(&self) -> u64 { - match self.version() { - 0 => u64::from(self.earliest_presentation_time_v0), - 1 => self.earliest_presentation_time_v1, - _ => 0, + fn display_field(&self, name: &'static str) -> Option { + match name { + "ColourType" => Some(quoted_fourcc(self.colour_type)), + _ => None, } } +} - /// Returns the active first offset for the current box version. - pub fn first_offset(&self) -> u64 { - match self.version() { - 0 => u64::from(self.first_offset_v0), - 1 => self.first_offset_v1, - _ => 0, - } +impl ImmutableBox for Colr { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"colr") } } -impl FieldValueRead for Sidx { +impl MutableBox for Colr {} + +impl FieldValueRead for Colr { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "ReferenceID" => Ok(FieldValue::Unsigned(u64::from(self.reference_id))), - "Timescale" => Ok(FieldValue::Unsigned(u64::from(self.timescale))), - "EarliestPresentationTimeV0" => Ok(FieldValue::Unsigned(u64::from( - self.earliest_presentation_time_v0, + "ColourType" => Ok(FieldValue::Bytes(self.colour_type.as_bytes().to_vec())), + "ColourPrimaries" => Ok(FieldValue::Unsigned(u64::from(self.colour_primaries))), + "TransferCharacteristics" => Ok(FieldValue::Unsigned(u64::from( + self.transfer_characteristics, ))), - "FirstOffsetV0" => Ok(FieldValue::Unsigned(u64::from(self.first_offset_v0))), - "EarliestPresentationTimeV1" => { - Ok(FieldValue::Unsigned(self.earliest_presentation_time_v1)) - } - "FirstOffsetV1" => Ok(FieldValue::Unsigned(self.first_offset_v1)), - "ReferenceCount" => Ok(FieldValue::Unsigned(u64::from(self.reference_count))), - "References" => { - require_count( - field_name, - u32::from(self.reference_count), - self.references.len(), - )?; - let mut bytes = Vec::with_capacity(self.references.len() * 12); - for entry in &self.references { - if entry.referenced_size > 0x7fff_ffff { - return Err(invalid_value( - field_name, - "referenced size does not fit in 31 bits", - )); - } - if entry.sap_type > 0x07 { - return Err(invalid_value(field_name, "SAP type does not fit in 3 bits")); - } - if entry.sap_delta_time > 0x0fff_ffff { - return Err(invalid_value( - field_name, - "SAP delta time does not fit in 28 bits", - )); - } - - // The reference and SAP records pack their high-bit flags into the same 32-bit words as the payload values. - let reference_word = - (u32::from(entry.reference_type) << 31) | entry.referenced_size; - let sap_word = (u32::from(entry.starts_with_sap) << 31) - | (entry.sap_type << 28) - | entry.sap_delta_time; - bytes.extend_from_slice(&reference_word.to_be_bytes()); - bytes.extend_from_slice(&entry.subsegment_duration.to_be_bytes()); - bytes.extend_from_slice(&sap_word.to_be_bytes()); - } - Ok(FieldValue::Bytes(bytes)) - } + "MatrixCoefficients" => Ok(FieldValue::Unsigned(u64::from(self.matrix_coefficients))), + "FullRangeFlag" => Ok(FieldValue::Boolean(self.full_range_flag)), + "Reserved" => Ok(FieldValue::Unsigned(u64::from(self.reserved))), + "Profile" => Ok(FieldValue::Bytes(self.profile.clone())), + "Unknown" => Ok(FieldValue::Bytes(self.unknown.clone())), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Sidx { +impl FieldValueWrite for Colr { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("ReferenceID", FieldValue::Unsigned(value)) => { - self.reference_id = u32_from_unsigned(field_name, value)?; + ("ColourType", FieldValue::Bytes(bytes)) => { + self.colour_type = bytes_to_fourcc(field_name, bytes)?; Ok(()) } - ("Timescale", FieldValue::Unsigned(value)) => { - self.timescale = u32_from_unsigned(field_name, value)?; + ("ColourPrimaries", FieldValue::Unsigned(value)) => { + self.colour_primaries = u16_from_unsigned(field_name, value)?; Ok(()) } - ("EarliestPresentationTimeV0", FieldValue::Unsigned(value)) => { - self.earliest_presentation_time_v0 = u32_from_unsigned(field_name, value)?; + ("TransferCharacteristics", FieldValue::Unsigned(value)) => { + self.transfer_characteristics = u16_from_unsigned(field_name, value)?; Ok(()) } - ("FirstOffsetV0", FieldValue::Unsigned(value)) => { - self.first_offset_v0 = u32_from_unsigned(field_name, value)?; + ("MatrixCoefficients", FieldValue::Unsigned(value)) => { + self.matrix_coefficients = u16_from_unsigned(field_name, value)?; Ok(()) } - ("EarliestPresentationTimeV1", FieldValue::Unsigned(value)) => { - self.earliest_presentation_time_v1 = value; + ("FullRangeFlag", FieldValue::Boolean(value)) => { + self.full_range_flag = value; Ok(()) } - ("FirstOffsetV1", FieldValue::Unsigned(value)) => { - self.first_offset_v1 = value; + ("Reserved", FieldValue::Unsigned(value)) => { + self.reserved = u8_from_unsigned(field_name, value)?; Ok(()) } - ("ReferenceCount", FieldValue::Unsigned(value)) => { - self.reference_count = u16_from_unsigned(field_name, value)?; + ("Profile", FieldValue::Bytes(value)) => { + self.profile = value; Ok(()) } - ("References", FieldValue::Bytes(bytes)) => { - let expected_len = field_len_from_count(u32::from(self.reference_count), 12) - .map(|len| len as usize) - .unwrap_or(0); - if bytes.len() != expected_len { - return Err(invalid_value( - field_name, - "reference payload length does not match the reference count", - )); - } - - self.references = - parse_fixed_chunks(field_name, &bytes, 12, |chunk| SidxReference { - reference_type: read_u32(chunk, 0) & 0x8000_0000 != 0, - referenced_size: read_u32(chunk, 0) & 0x7fff_ffff, - subsegment_duration: read_u32(chunk, 4), - starts_with_sap: read_u32(chunk, 8) & 0x8000_0000 != 0, - sap_type: (read_u32(chunk, 8) >> 28) & 0x07, - sap_delta_time: read_u32(chunk, 8) & 0x0fff_ffff, - })?; + ("Unknown", FieldValue::Bytes(value)) => { + self.unknown = value; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -5347,243 +8838,144 @@ impl FieldValueWrite for Sidx { } } -impl CodecBox for Sidx { +impl CodecBox for Colr { const FIELD_TABLE: FieldTable = FieldTable::new(&[ - codec_field!("Version", 0, with_bit_width(8), as_version_field()), - codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("ReferenceID", 2, with_bit_width(32)), - codec_field!("Timescale", 3, with_bit_width(32)), codec_field!( - "EarliestPresentationTimeV0", + "ColourType", + 0, + with_bit_width(8), + with_length(4), + as_bytes() + ), + codec_field!( + "ColourPrimaries", + 1, + with_bit_width(16), + with_dynamic_presence() + ), + codec_field!( + "TransferCharacteristics", + 2, + with_bit_width(16), + with_dynamic_presence() + ), + codec_field!( + "MatrixCoefficients", + 3, + with_bit_width(16), + with_dynamic_presence() + ), + codec_field!( + "FullRangeFlag", 4, - with_bit_width(32), - with_version(0) + with_bit_width(1), + as_boolean(), + with_dynamic_presence() ), - codec_field!("FirstOffsetV0", 5, with_bit_width(32), with_version(0)), codec_field!( - "EarliestPresentationTimeV1", + "Reserved", + 5, + with_bit_width(7), + as_hex(), + with_dynamic_presence() + ), + codec_field!( + "Profile", 6, - with_bit_width(64), - with_version(1) + with_bit_width(8), + as_bytes(), + with_dynamic_presence() ), - codec_field!("FirstOffsetV1", 7, with_bit_width(64), with_version(1)), - codec_field!("Reserved", 8, with_bit_width(16), with_constant("0")), - codec_field!("ReferenceCount", 9, with_bit_width(16)), - codec_field!("References", 10, with_bit_width(8), as_bytes()), - ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; -} - -/// One track-fragment random-access entry. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct TfraEntry { - pub time_v0: u32, - pub moof_offset_v0: u32, - pub time_v1: u64, - pub moof_offset_v1: u64, - pub traf_number: u32, - pub trun_number: u32, - pub sample_number: u32, -} - -/// Track fragment random access box. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Tfra { - full_box: FullBoxState, - pub track_id: u32, - pub length_size_of_traf_num: u8, - pub length_size_of_trun_num: u8, - pub length_size_of_sample_num: u8, - pub number_of_entry: u32, - pub entries: Vec, -} - -impl Tfra { - fn entry_size_bytes(&self) -> usize { - // Each stored length field is encoded as "size minus one", so add one byte to recover the actual width. - let traf_bytes = usize::from(self.length_size_of_traf_num) + 1; - let trun_bytes = usize::from(self.length_size_of_trun_num) + 1; - let sample_bytes = usize::from(self.length_size_of_sample_num) + 1; - match self.version() { - 0 => 8 + traf_bytes + trun_bytes + sample_bytes, - 1 => 16 + traf_bytes + trun_bytes + sample_bytes, - _ => traf_bytes + trun_bytes + sample_bytes, - } - } - - /// Returns the active random-access time for `index`. - pub fn time(&self, index: usize) -> u64 { - match self.version() { - 0 => u64::from(self.entries[index].time_v0), - 1 => self.entries[index].time_v1, - _ => 0, - } - } - - /// Returns the active `moof` offset for `index`. - pub fn moof_offset(&self, index: usize) -> u64 { - match self.version() { - 0 => u64::from(self.entries[index].moof_offset_v0), - 1 => self.entries[index].moof_offset_v1, - _ => 0, - } - } + codec_field!( + "Unknown", + 7, + with_bit_width(8), + as_bytes(), + with_dynamic_presence() + ), + ]); } -impl FieldHooks for Tfra { +/// Event-message box whose field order changes with the encoded version. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Emsg { + full_box: FullBoxState, + pub scheme_id_uri: String, + pub value: String, + pub timescale: u32, + pub presentation_time_delta: u32, + pub presentation_time: u64, + pub event_duration: u32, + pub id: u32, + pub message_data: Vec, +} + +impl FieldHooks for Emsg { fn display_field(&self, name: &'static str) -> Option { match name { - "Entries" => Some(render_array(self.entries.iter().map(|entry| { - if self.version() == 0 { - format!( - "{{TimeV0={} MoofOffsetV0={} TrafNumber={} TrunNumber={} SampleNumber={}}}", - entry.time_v0, - entry.moof_offset_v0, - entry.traf_number, - entry.trun_number, - entry.sample_number - ) - } else { - format!( - "{{TimeV1={} MoofOffsetV1={} TrafNumber={} TrunNumber={} SampleNumber={}}}", - entry.time_v1, - entry.moof_offset_v1, - entry.traf_number, - entry.trun_number, - entry.sample_number - ) - } - }))), + "MessageData" => Some(quote_bytes(&self.message_data)), _ => None, } } } -impl_full_box!(Tfra, *b"tfra"); +impl_full_box!(Emsg, *b"emsg"); -impl FieldValueRead for Tfra { +impl FieldValueRead for Emsg { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "TrackID" => Ok(FieldValue::Unsigned(u64::from(self.track_id))), - "LengthSizeOfTrafNum" => Ok(FieldValue::Unsigned(u64::from( - self.length_size_of_traf_num, - ))), - "LengthSizeOfTrunNum" => Ok(FieldValue::Unsigned(u64::from( - self.length_size_of_trun_num, - ))), - "LengthSizeOfSampleNum" => Ok(FieldValue::Unsigned(u64::from( - self.length_size_of_sample_num, + "SchemeIdUri" => Ok(FieldValue::String(self.scheme_id_uri.clone())), + "Value" => Ok(FieldValue::String(self.value.clone())), + "Timescale" => Ok(FieldValue::Unsigned(u64::from(self.timescale))), + "PresentationTimeDelta" => Ok(FieldValue::Unsigned(u64::from( + self.presentation_time_delta, ))), - "NumberOfEntry" => Ok(FieldValue::Unsigned(u64::from(self.number_of_entry))), - "Entries" => { - require_count(field_name, self.number_of_entry, self.entries.len())?; - let traf_bytes = usize::from(self.length_size_of_traf_num) + 1; - let trun_bytes = usize::from(self.length_size_of_trun_num) + 1; - let sample_bytes = usize::from(self.length_size_of_sample_num) + 1; - let mut bytes = Vec::with_capacity(self.entries.len() * self.entry_size_bytes()); - for entry in &self.entries { - if self.version() == 0 { - bytes.extend_from_slice(&entry.time_v0.to_be_bytes()); - bytes.extend_from_slice(&entry.moof_offset_v0.to_be_bytes()); - } else { - bytes.extend_from_slice(&entry.time_v1.to_be_bytes()); - bytes.extend_from_slice(&entry.moof_offset_v1.to_be_bytes()); - } - push_uint( - field_name, - &mut bytes, - traf_bytes, - u64::from(entry.traf_number), - )?; - push_uint( - field_name, - &mut bytes, - trun_bytes, - u64::from(entry.trun_number), - )?; - push_uint( - field_name, - &mut bytes, - sample_bytes, - u64::from(entry.sample_number), - )?; - } - Ok(FieldValue::Bytes(bytes)) - } + "PresentationTime" => Ok(FieldValue::Unsigned(self.presentation_time)), + "EventDuration" => Ok(FieldValue::Unsigned(u64::from(self.event_duration))), + "Id" => Ok(FieldValue::Unsigned(u64::from(self.id))), + "MessageData" => Ok(FieldValue::Bytes(self.message_data.clone())), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Tfra { +impl FieldValueWrite for Emsg { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("TrackID", FieldValue::Unsigned(value)) => { - self.track_id = u32_from_unsigned(field_name, value)?; + ("SchemeIdUri", FieldValue::String(value)) => { + self.scheme_id_uri = value; Ok(()) } - ("LengthSizeOfTrafNum", FieldValue::Unsigned(value)) => { - self.length_size_of_traf_num = u8_from_unsigned(field_name, value)?; + ("Value", FieldValue::String(value)) => { + self.value = value; Ok(()) } - ("LengthSizeOfTrunNum", FieldValue::Unsigned(value)) => { - self.length_size_of_trun_num = u8_from_unsigned(field_name, value)?; + ("Timescale", FieldValue::Unsigned(value)) => { + self.timescale = u32_from_unsigned(field_name, value)?; Ok(()) } - ("LengthSizeOfSampleNum", FieldValue::Unsigned(value)) => { - self.length_size_of_sample_num = u8_from_unsigned(field_name, value)?; + ("PresentationTimeDelta", FieldValue::Unsigned(value)) => { + self.presentation_time_delta = u32_from_unsigned(field_name, value)?; Ok(()) } - ("NumberOfEntry", FieldValue::Unsigned(value)) => { - self.number_of_entry = u32_from_unsigned(field_name, value)?; + ("PresentationTime", FieldValue::Unsigned(value)) => { + self.presentation_time = value; Ok(()) } - ("Entries", FieldValue::Bytes(bytes)) => { - let entry_size = self.entry_size_bytes(); - let expected_len = field_len_from_count(self.number_of_entry, entry_size) - .map(|len| len as usize) - .unwrap_or(0); - if bytes.len() != expected_len { - return Err(invalid_value( - field_name, - "random access payload length does not match the entry count", - )); - } - - let traf_bytes = usize::from(self.length_size_of_traf_num) + 1; - let trun_bytes = usize::from(self.length_size_of_trun_num) + 1; - let sample_bytes = usize::from(self.length_size_of_sample_num) + 1; - self.entries = bytes - .chunks_exact(entry_size) - .map(|chunk| { - let mut offset = 0; - let mut entry = TfraEntry::default(); - if self.version() == 0 { - entry.time_v0 = read_u32(chunk, offset); - offset += 4; - entry.moof_offset_v0 = read_u32(chunk, offset); - offset += 4; - } else { - entry.time_v1 = read_u64(chunk, offset); - offset += 8; - entry.moof_offset_v1 = read_u64(chunk, offset); - offset += 8; - } - entry.traf_number = - u32_from_unsigned(field_name, read_uint(chunk, offset, traf_bytes))?; - offset += traf_bytes; - entry.trun_number = - u32_from_unsigned(field_name, read_uint(chunk, offset, trun_bytes))?; - offset += trun_bytes; - entry.sample_number = - u32_from_unsigned(field_name, read_uint(chunk, offset, sample_bytes))?; - Ok(entry) - }) - .collect::, FieldValueError>>()?; + ("EventDuration", FieldValue::Unsigned(value)) => { + self.event_duration = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("Id", FieldValue::Unsigned(value)) => { + self.id = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("MessageData", FieldValue::Bytes(value)) => { + self.message_data = value; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -5591,59 +8983,119 @@ impl FieldValueWrite for Tfra { } } -impl CodecBox for Tfra { +impl CodecBox for Emsg { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), - codec_field!("TrackID", 2, with_bit_width(32)), - codec_field!("Reserved", 3, with_bit_width(26), with_constant("0")), - codec_field!("LengthSizeOfTrafNum", 4, with_bit_width(2), as_hex()), - codec_field!("LengthSizeOfTrunNum", 5, with_bit_width(2), as_hex()), - codec_field!("LengthSizeOfSampleNum", 6, with_bit_width(2), as_hex()), - codec_field!("NumberOfEntry", 7, with_bit_width(32)), - codec_field!("Entries", 8, with_bit_width(8), as_bytes()), + codec_field!( + "SchemeIdUri", + 2, + with_bit_width(8), + as_string(StringFieldMode::NullTerminated), + with_version(0) + ), + codec_field!( + "Value", + 3, + with_bit_width(8), + as_string(StringFieldMode::NullTerminated), + with_version(0) + ), + codec_field!("Timescale", 4, with_bit_width(32)), + codec_field!( + "PresentationTimeDelta", + 5, + with_bit_width(32), + with_version(0) + ), + codec_field!( + "PresentationTime", + 6, + with_bit_width(64), + with_version(1), + with_display_order(5) + ), + codec_field!( + "EventDuration", + 7, + with_bit_width(32), + with_display_order(6) + ), + codec_field!("Id", 8, with_bit_width(32), with_display_order(7)), + codec_field!( + "SchemeIdUri", + 9, + with_bit_width(8), + as_string(StringFieldMode::NullTerminated), + with_version(1), + with_display_order(2) + ), + codec_field!( + "Value", + 10, + with_bit_width(8), + as_string(StringFieldMode::NullTerminated), + with_version(1), + with_display_order(3) + ), + codec_field!( + "MessageData", + 11, + with_bit_width(8), + as_bytes(), + with_display_order(8) + ), ]); const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; } -/// Bitrate declaration box for sample entries. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Btrt { - pub buffer_size_db: u32, - pub max_bitrate: u32, - pub avg_bitrate: u32, +/// Event-message sample entry carried under `stsd`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EventMessageSampleEntry { + pub sample_entry: SampleEntry, +} + +impl Default for EventMessageSampleEntry { + fn default() -> Self { + Self { + sample_entry: SampleEntry { + box_type: FourCc::from_bytes(*b"evte"), + data_reference_index: 0, + }, + } + } } -impl_leaf_box!(Btrt, *b"btrt"); +impl FieldHooks for EventMessageSampleEntry {} -impl FieldValueRead for Btrt { +impl ImmutableBox for EventMessageSampleEntry { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"evte") + } +} + +impl MutableBox for EventMessageSampleEntry {} + +impl FieldValueRead for EventMessageSampleEntry { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "BufferSizeDB" => Ok(FieldValue::Unsigned(u64::from(self.buffer_size_db))), - "MaxBitrate" => Ok(FieldValue::Unsigned(u64::from(self.max_bitrate))), - "AvgBitrate" => Ok(FieldValue::Unsigned(u64::from(self.avg_bitrate))), + "DataReferenceIndex" => Ok(FieldValue::Unsigned(u64::from( + self.sample_entry.data_reference_index, + ))), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Btrt { +impl FieldValueWrite for EventMessageSampleEntry { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("BufferSizeDB", FieldValue::Unsigned(value)) => { - self.buffer_size_db = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("MaxBitrate", FieldValue::Unsigned(value)) => { - self.max_bitrate = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("AvgBitrate", FieldValue::Unsigned(value)) => { - self.avg_bitrate = u32_from_unsigned(field_name, value)?; + ("DataReferenceIndex", FieldValue::Unsigned(value)) => { + self.sample_entry.data_reference_index = u16_from_unsigned(field_name, value)?; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -5651,130 +9103,153 @@ impl FieldValueWrite for Btrt { } } -impl CodecBox for Btrt { +impl CodecBox for EventMessageSampleEntry { const FIELD_TABLE: FieldTable = FieldTable::new(&[ - codec_field!("BufferSizeDB", 0, with_bit_width(32)), - codec_field!("MaxBitrate", 1, with_bit_width(32)), - codec_field!("AvgBitrate", 2, with_bit_width(32)), + codec_field!("Reserved0A", 0, with_bit_width(16), with_constant("0")), + codec_field!("Reserved0B", 1, with_bit_width(16), with_constant("0")), + codec_field!("Reserved0C", 2, with_bit_width(16), with_constant("0")), + codec_field!("DataReferenceIndex", 3, with_bit_width(16)), ]); } -/// Color information leaf whose active fields depend on the stored colour type. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Colr { - pub colour_type: FourCc, - pub colour_primaries: u16, - pub transfer_characteristics: u16, - pub matrix_coefficients: u16, - pub full_range_flag: bool, - pub reserved: u8, - pub profile: Vec, - pub unknown: Vec, +/// One scheme-identification record carried by [`Silb`]. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SilbEntry { + pub scheme_id_uri: String, + pub value: String, + pub at_least_one_flag: bool, } -impl Default for Colr { - fn default() -> Self { - Self { - colour_type: FourCc::ANY, - colour_primaries: 0, - transfer_characteristics: 0, - matrix_coefficients: 0, - full_range_flag: false, - reserved: 0, - profile: Vec::new(), - unknown: Vec::new(), - } +/// Scheme-identifier box carried by `evte` sample entries. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Silb { + full_box: FullBoxState, + pub scheme_count: u32, + pub schemes: Vec, + pub other_schemes_flag: bool, +} + +fn format_silb_schemes(schemes: &[SilbEntry]) -> String { + render_array(schemes.iter().map(|scheme| { + format!( + "{{SchemeIdUri=\"{}\" Value=\"{}\" AtLeastOneFlag={}}}", + scheme.scheme_id_uri, scheme.value, scheme.at_least_one_flag + ) + })) +} + +fn encode_silb_schemes( + field_name: &'static str, + schemes: &[SilbEntry], +) -> Result, FieldValueError> { + let mut bytes = Vec::new(); + for scheme in schemes { + validate_c_string_value(field_name, &scheme.scheme_id_uri)?; + validate_c_string_value(field_name, &scheme.value)?; + bytes.extend_from_slice(scheme.scheme_id_uri.as_bytes()); + bytes.push(0); + bytes.extend_from_slice(scheme.value.as_bytes()); + bytes.push(0); + bytes.push(u8::from(scheme.at_least_one_flag)); } + Ok(bytes) } -impl FieldHooks for Colr { - fn field_enabled(&self, name: &'static str) -> Option { - match name { - "ColourPrimaries" - | "TransferCharacteristics" - | "MatrixCoefficients" - | "FullRangeFlag" - | "Reserved" => Some(self.colour_type == COLR_NCLX), - "Profile" => Some(matches!(self.colour_type, COLR_RICC | COLR_PROF)), - "Unknown" => Some(!matches!( - self.colour_type, - COLR_NCLX | COLR_RICC | COLR_PROF - )), - _ => None, +fn parse_silb_schemes( + field_name: &'static str, + scheme_count: u32, + bytes: &[u8], +) -> Result, FieldValueError> { + let mut schemes = Vec::with_capacity(untrusted_prealloc_hint( + usize::try_from(scheme_count).unwrap_or(0), + )); + let mut offset = 0usize; + + for _ in 0..scheme_count { + let (scheme_id_uri, consumed) = parse_required_c_string(field_name, &bytes[offset..])?; + offset += consumed; + + let (value, consumed) = parse_required_c_string(field_name, &bytes[offset..])?; + offset += consumed; + + if bytes.len().saturating_sub(offset) < 1 { + return Err(invalid_value( + field_name, + "scheme flag payload is truncated", + )); } + + let at_least_one_flag = match bytes[offset] { + 0 => false, + 1 => true, + _ => { + return Err(invalid_value(field_name, "scheme flag byte must be 0 or 1")); + } + }; + offset += 1; + + schemes.push(SilbEntry { + scheme_id_uri, + value, + at_least_one_flag, + }); + } + + if offset != bytes.len() { + return Err(invalid_value( + field_name, + "scheme payload length does not match the scheme count", + )); } + Ok(schemes) +} + +impl FieldHooks for Silb { fn display_field(&self, name: &'static str) -> Option { match name { - "ColourType" => Some(quoted_fourcc(self.colour_type)), + "Schemes" => Some(format_silb_schemes(&self.schemes)), _ => None, } } } -impl ImmutableBox for Colr { - fn box_type(&self) -> FourCc { - FourCc::from_bytes(*b"colr") - } -} - -impl MutableBox for Colr {} +impl_full_box!(Silb, *b"silb"); -impl FieldValueRead for Colr { +impl FieldValueRead for Silb { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "ColourType" => Ok(FieldValue::Bytes(self.colour_type.as_bytes().to_vec())), - "ColourPrimaries" => Ok(FieldValue::Unsigned(u64::from(self.colour_primaries))), - "TransferCharacteristics" => Ok(FieldValue::Unsigned(u64::from( - self.transfer_characteristics, - ))), - "MatrixCoefficients" => Ok(FieldValue::Unsigned(u64::from(self.matrix_coefficients))), - "FullRangeFlag" => Ok(FieldValue::Boolean(self.full_range_flag)), - "Reserved" => Ok(FieldValue::Unsigned(u64::from(self.reserved))), - "Profile" => Ok(FieldValue::Bytes(self.profile.clone())), - "Unknown" => Ok(FieldValue::Bytes(self.unknown.clone())), + "SchemeCount" => Ok(FieldValue::Unsigned(u64::from(self.scheme_count))), + "Schemes" => { + require_count(field_name, self.scheme_count, self.schemes.len())?; + Ok(FieldValue::Bytes(encode_silb_schemes( + field_name, + &self.schemes, + )?)) + } + "OtherSchemesFlag" => Ok(FieldValue::Boolean(self.other_schemes_flag)), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Colr { +impl FieldValueWrite for Silb { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("ColourType", FieldValue::Bytes(bytes)) => { - self.colour_type = bytes_to_fourcc(field_name, bytes)?; - Ok(()) - } - ("ColourPrimaries", FieldValue::Unsigned(value)) => { - self.colour_primaries = u16_from_unsigned(field_name, value)?; - Ok(()) - } - ("TransferCharacteristics", FieldValue::Unsigned(value)) => { - self.transfer_characteristics = u16_from_unsigned(field_name, value)?; - Ok(()) - } - ("MatrixCoefficients", FieldValue::Unsigned(value)) => { - self.matrix_coefficients = u16_from_unsigned(field_name, value)?; - Ok(()) - } - ("FullRangeFlag", FieldValue::Boolean(value)) => { - self.full_range_flag = value; - Ok(()) - } - ("Reserved", FieldValue::Unsigned(value)) => { - self.reserved = u8_from_unsigned(field_name, value)?; + ("SchemeCount", FieldValue::Unsigned(value)) => { + self.scheme_count = u32_from_unsigned(field_name, value)?; Ok(()) } - ("Profile", FieldValue::Bytes(value)) => { - self.profile = value; + ("Schemes", FieldValue::Bytes(bytes)) => { + self.schemes = parse_silb_schemes(field_name, self.scheme_count, &bytes)?; Ok(()) } - ("Unknown", FieldValue::Bytes(value)) => { - self.unknown = value; + ("OtherSchemesFlag", FieldValue::Boolean(value)) => { + self.other_schemes_flag = value; Ok(()) } (field_name, value) => Err(unexpected_field(field_name, value)), @@ -5782,79 +9257,91 @@ impl FieldValueWrite for Colr { } } -impl CodecBox for Colr { +impl CodecBox for Silb { const FIELD_TABLE: FieldTable = FieldTable::new(&[ - codec_field!( - "ColourType", - 0, - with_bit_width(8), - with_length(4), - as_bytes() - ), - codec_field!( - "ColourPrimaries", - 1, - with_bit_width(16), - with_dynamic_presence() - ), - codec_field!( - "TransferCharacteristics", - 2, - with_bit_width(16), - with_dynamic_presence() - ), - codec_field!( - "MatrixCoefficients", - 3, - with_bit_width(16), - with_dynamic_presence() - ), - codec_field!( - "FullRangeFlag", - 4, - with_bit_width(1), - as_boolean(), - with_dynamic_presence() - ), - codec_field!( - "Reserved", - 5, - with_bit_width(7), - as_hex(), - with_dynamic_presence() - ), - codec_field!( - "Profile", - 6, - with_bit_width(8), - as_bytes(), - with_dynamic_presence() - ), - codec_field!( - "Unknown", - 7, - with_bit_width(8), - as_bytes(), - with_dynamic_presence() - ), + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("SchemeCount", 2, with_bit_width(32)), + codec_field!("Schemes", 3, with_bit_width(8), as_bytes()), + codec_field!("OtherSchemesFlag", 4, with_bit_width(8), as_boolean()), ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; + + fn custom_marshal(&self, writer: &mut dyn Write) -> Result, CodecError> { + if self.version() != 0 { + return Err(CodecError::UnsupportedVersion { + box_type: self.box_type(), + version: self.version(), + }); + } + + require_count("Schemes", self.scheme_count, self.schemes.len())?; + + let mut payload = Vec::new(); + payload.push(self.version()); + push_uint("Flags", &mut payload, 3, u64::from(self.flags()))?; + payload.extend_from_slice(&self.scheme_count.to_be_bytes()); + payload.extend_from_slice(&encode_silb_schemes("Schemes", &self.schemes)?); + payload.push(u8::from(self.other_schemes_flag)); + writer.write_all(&payload)?; + Ok(Some(payload.len() as u64)) + } + + fn custom_unmarshal( + &mut self, + reader: &mut dyn ReadSeek, + payload_size: u64, + ) -> Result, CodecError> { + let payload_len = usize::try_from(payload_size) + .map_err(|_| invalid_value("Payload", "payload is too large to decode"))?; + let payload = read_exact_vec_untrusted(reader, payload_len).map_err(CodecError::Io)?; + + if payload.len() < 9 { + return Err(invalid_value("Payload", "payload is too short").into()); + } + + let version = payload[0]; + if version != 0 { + return Err(CodecError::UnsupportedVersion { + box_type: self.box_type(), + version, + }); + } + + let other_schemes_flag = match payload[payload.len() - 1] { + 0 => false, + 1 => true, + _ => { + return Err(invalid_value("OtherSchemesFlag", "flag byte must be 0 or 1").into()); + } + }; + + self.full_box = FullBoxState { + version, + flags: read_uint(&payload, 1, 3) as u32, + }; + self.scheme_count = read_u32(&payload, 4); + self.schemes = + parse_silb_schemes("Schemes", self.scheme_count, &payload[8..payload.len() - 1])?; + self.other_schemes_flag = other_schemes_flag; + + Ok(Some(payload_size)) + } } -/// Event-message box whose field order changes with the encoded version. +/// Embedded event-message instance box. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Emsg { +pub struct Emib { full_box: FullBoxState, - pub scheme_id_uri: String, - pub value: String, - pub timescale: u32, - pub presentation_time_delta: u32, - pub presentation_time: u64, + pub presentation_time_delta: i64, pub event_duration: u32, pub id: u32, + pub scheme_id_uri: String, + pub value: String, pub message_data: Vec, } -impl FieldHooks for Emsg { +impl FieldHooks for Emib { fn display_field(&self, name: &'static str) -> Option { match name { "MessageData" => Some(quote_bytes(&self.message_data)), @@ -5863,51 +9350,31 @@ impl FieldHooks for Emsg { } } -impl_full_box!(Emsg, *b"emsg"); +impl_full_box!(Emib, *b"emib"); -impl FieldValueRead for Emsg { +impl FieldValueRead for Emib { fn field_value(&self, field_name: &'static str) -> Result { match field_name { - "SchemeIdUri" => Ok(FieldValue::String(self.scheme_id_uri.clone())), - "Value" => Ok(FieldValue::String(self.value.clone())), - "Timescale" => Ok(FieldValue::Unsigned(u64::from(self.timescale))), - "PresentationTimeDelta" => Ok(FieldValue::Unsigned(u64::from( - self.presentation_time_delta, - ))), - "PresentationTime" => Ok(FieldValue::Unsigned(self.presentation_time)), + "PresentationTimeDelta" => Ok(FieldValue::Signed(self.presentation_time_delta)), "EventDuration" => Ok(FieldValue::Unsigned(u64::from(self.event_duration))), "Id" => Ok(FieldValue::Unsigned(u64::from(self.id))), + "SchemeIdUri" => Ok(FieldValue::String(self.scheme_id_uri.clone())), + "Value" => Ok(FieldValue::String(self.value.clone())), "MessageData" => Ok(FieldValue::Bytes(self.message_data.clone())), _ => Err(missing_field(field_name)), } } } -impl FieldValueWrite for Emsg { +impl FieldValueWrite for Emib { fn set_field_value( &mut self, field_name: &'static str, value: FieldValue, ) -> Result<(), FieldValueError> { match (field_name, value) { - ("SchemeIdUri", FieldValue::String(value)) => { - self.scheme_id_uri = value; - Ok(()) - } - ("Value", FieldValue::String(value)) => { - self.value = value; - Ok(()) - } - ("Timescale", FieldValue::Unsigned(value)) => { - self.timescale = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("PresentationTimeDelta", FieldValue::Unsigned(value)) => { - self.presentation_time_delta = u32_from_unsigned(field_name, value)?; - Ok(()) - } - ("PresentationTime", FieldValue::Unsigned(value)) => { - self.presentation_time = value; + ("PresentationTimeDelta", FieldValue::Signed(value)) => { + self.presentation_time_delta = value; Ok(()) } ("EventDuration", FieldValue::Unsigned(value)) => { @@ -5918,6 +9385,16 @@ impl FieldValueWrite for Emsg { self.id = u32_from_unsigned(field_name, value)?; Ok(()) } + ("SchemeIdUri", FieldValue::String(value)) => { + validate_c_string_value(field_name, &value)?; + self.scheme_id_uri = value; + Ok(()) + } + ("Value", FieldValue::String(value)) => { + validate_c_string_value(field_name, &value)?; + self.value = value; + Ok(()) + } ("MessageData", FieldValue::Bytes(value)) => { self.message_data = value; Ok(()) @@ -5927,70 +9404,151 @@ impl FieldValueWrite for Emsg { } } -impl CodecBox for Emsg { +impl CodecBox for Emib { const FIELD_TABLE: FieldTable = FieldTable::new(&[ codec_field!("Version", 0, with_bit_width(8), as_version_field()), codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("PresentationTimeDelta", 2, with_bit_width(64), as_signed()), + codec_field!("EventDuration", 3, with_bit_width(32)), + codec_field!("Id", 4, with_bit_width(32)), codec_field!( "SchemeIdUri", - 2, - with_bit_width(8), - as_string(StringFieldMode::NullTerminated), - with_version(0) - ), - codec_field!( - "Value", - 3, - with_bit_width(8), - as_string(StringFieldMode::NullTerminated), - with_version(0) - ), - codec_field!("Timescale", 4, with_bit_width(32)), - codec_field!( - "PresentationTimeDelta", 5, - with_bit_width(32), - with_version(0) - ), - codec_field!( - "PresentationTime", - 6, - with_bit_width(64), - with_version(1), - with_display_order(5) - ), - codec_field!( - "EventDuration", - 7, - with_bit_width(32), - with_display_order(6) - ), - codec_field!("Id", 8, with_bit_width(32), with_display_order(7)), - codec_field!( - "SchemeIdUri", - 9, with_bit_width(8), - as_string(StringFieldMode::NullTerminated), - with_version(1), - with_display_order(2) + as_string(StringFieldMode::NullTerminated) ), codec_field!( "Value", - 10, - with_bit_width(8), - as_string(StringFieldMode::NullTerminated), - with_version(1), - with_display_order(3) - ), - codec_field!( - "MessageData", - 11, + 6, with_bit_width(8), - as_bytes(), - with_display_order(8) + as_string(StringFieldMode::NullTerminated) ), + codec_field!("MessageData", 7, with_bit_width(8), as_bytes()), ]); - const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; + + fn custom_marshal(&self, writer: &mut dyn Write) -> Result, CodecError> { + validate_c_string_value("SchemeIdUri", &self.scheme_id_uri)?; + validate_c_string_value("Value", &self.value)?; + if self.version() != 0 { + return Err(CodecError::UnsupportedVersion { + box_type: self.box_type(), + version: self.version(), + }); + } + + let mut payload = Vec::with_capacity( + 24 + self.scheme_id_uri.len() + self.value.len() + self.message_data.len() + 2, + ); + payload.push(self.version()); + push_uint("Flags", &mut payload, 3, u64::from(self.flags()))?; + payload.extend_from_slice(&0_u32.to_be_bytes()); + payload.extend_from_slice(&self.presentation_time_delta.to_be_bytes()); + payload.extend_from_slice(&self.event_duration.to_be_bytes()); + payload.extend_from_slice(&self.id.to_be_bytes()); + payload.extend_from_slice(self.scheme_id_uri.as_bytes()); + payload.push(0); + payload.extend_from_slice(self.value.as_bytes()); + payload.push(0); + payload.extend_from_slice(&self.message_data); + writer.write_all(&payload)?; + Ok(Some(payload.len() as u64)) + } + + fn custom_unmarshal( + &mut self, + reader: &mut dyn ReadSeek, + payload_size: u64, + ) -> Result, CodecError> { + let payload_len = usize::try_from(payload_size) + .map_err(|_| invalid_value("Payload", "payload is too large to decode"))?; + let payload = read_exact_vec_untrusted(reader, payload_len).map_err(CodecError::Io)?; + + if payload.len() < 24 { + return Err(invalid_value("Payload", "payload is too short").into()); + } + + let version = payload[0]; + if version != 0 { + return Err(CodecError::UnsupportedVersion { + box_type: self.box_type(), + version, + }); + } + + if read_u32(&payload, 4) != 0 { + return Err(invalid_value("Reserved", "reserved field must be zero").into()); + } + + let (scheme_id_uri, scheme_len) = decode_required_c_string("SchemeIdUri", &payload[24..])?; + let value_offset = 24 + scheme_len; + let (value, value_len) = decode_required_c_string("Value", &payload[value_offset..])?; + let message_offset = value_offset + value_len; + + self.full_box = FullBoxState { + version, + flags: read_uint(&payload, 1, 3) as u32, + }; + self.presentation_time_delta = read_i64(&payload, 8); + self.event_duration = read_u32(&payload, 16); + self.id = read_u32(&payload, 20); + self.scheme_id_uri = scheme_id_uri; + self.value = value; + self.message_data = payload[message_offset..].to_vec(); + + Ok(Some(payload_size)) + } +} + +/// Empty embedded event-message box. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Emeb; + +impl FieldHooks for Emeb {} + +impl ImmutableBox for Emeb { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"emeb") + } +} + +impl MutableBox for Emeb {} + +impl FieldValueRead for Emeb { + fn field_value(&self, field_name: &'static str) -> Result { + Err(missing_field(field_name)) + } +} + +impl FieldValueWrite for Emeb { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + Err(unexpected_field(field_name, value)) + } +} + +impl CodecBox for Emeb { + const FIELD_TABLE: FieldTable = FieldTable::new(&[]); + + fn custom_marshal(&self, _writer: &mut dyn Write) -> Result, CodecError> { + Ok(Some(0)) + } + + fn custom_unmarshal( + &mut self, + reader: &mut dyn ReadSeek, + payload_size: u64, + ) -> Result, CodecError> { + let start = reader.stream_position()?; + if payload_size != 0 { + reader.seek(SeekFrom::Start(start))?; + return Err(invalid_value("Payload", "payload must be empty").into()); + } + Ok(Some(0)) + } } /// Field-ordering leaf used by some video sample entries. @@ -6151,6 +9709,140 @@ impl CodecBox for Pasp { ]); } +/// Mastering-display metadata box carried by some visual sample entries. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SmDm { + full_box: FullBoxState, + pub primary_r_chromaticity_x: u16, + pub primary_r_chromaticity_y: u16, + pub primary_g_chromaticity_x: u16, + pub primary_g_chromaticity_y: u16, + pub primary_b_chromaticity_x: u16, + pub primary_b_chromaticity_y: u16, + pub white_point_chromaticity_x: u16, + pub white_point_chromaticity_y: u16, + pub luminance_max: u32, + pub luminance_min: u32, +} + +impl FieldHooks for SmDm {} + +impl_full_box!(SmDm, *b"SmDm"); + +impl FieldValueRead for SmDm { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "Version" => Ok(FieldValue::Unsigned(u64::from(self.version()))), + "Flags" => Ok(FieldValue::Unsigned(u64::from(self.flags()))), + "PrimaryRChromaticityX" => Ok(FieldValue::Unsigned(u64::from( + self.primary_r_chromaticity_x, + ))), + "PrimaryRChromaticityY" => Ok(FieldValue::Unsigned(u64::from( + self.primary_r_chromaticity_y, + ))), + "PrimaryGChromaticityX" => Ok(FieldValue::Unsigned(u64::from( + self.primary_g_chromaticity_x, + ))), + "PrimaryGChromaticityY" => Ok(FieldValue::Unsigned(u64::from( + self.primary_g_chromaticity_y, + ))), + "PrimaryBChromaticityX" => Ok(FieldValue::Unsigned(u64::from( + self.primary_b_chromaticity_x, + ))), + "PrimaryBChromaticityY" => Ok(FieldValue::Unsigned(u64::from( + self.primary_b_chromaticity_y, + ))), + "WhitePointChromaticityX" => Ok(FieldValue::Unsigned(u64::from( + self.white_point_chromaticity_x, + ))), + "WhitePointChromaticityY" => Ok(FieldValue::Unsigned(u64::from( + self.white_point_chromaticity_y, + ))), + "LuminanceMax" => Ok(FieldValue::Unsigned(u64::from(self.luminance_max))), + "LuminanceMin" => Ok(FieldValue::Unsigned(u64::from(self.luminance_min))), + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for SmDm { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("Version", FieldValue::Unsigned(value)) => { + self.set_version(u8_from_unsigned(field_name, value)?); + Ok(()) + } + ("Flags", FieldValue::Unsigned(value)) => { + self.set_flags(u32_from_unsigned(field_name, value)?); + Ok(()) + } + ("PrimaryRChromaticityX", FieldValue::Unsigned(value)) => { + self.primary_r_chromaticity_x = u16_from_unsigned(field_name, value)?; + Ok(()) + } + ("PrimaryRChromaticityY", FieldValue::Unsigned(value)) => { + self.primary_r_chromaticity_y = u16_from_unsigned(field_name, value)?; + Ok(()) + } + ("PrimaryGChromaticityX", FieldValue::Unsigned(value)) => { + self.primary_g_chromaticity_x = u16_from_unsigned(field_name, value)?; + Ok(()) + } + ("PrimaryGChromaticityY", FieldValue::Unsigned(value)) => { + self.primary_g_chromaticity_y = u16_from_unsigned(field_name, value)?; + Ok(()) + } + ("PrimaryBChromaticityX", FieldValue::Unsigned(value)) => { + self.primary_b_chromaticity_x = u16_from_unsigned(field_name, value)?; + Ok(()) + } + ("PrimaryBChromaticityY", FieldValue::Unsigned(value)) => { + self.primary_b_chromaticity_y = u16_from_unsigned(field_name, value)?; + Ok(()) + } + ("WhitePointChromaticityX", FieldValue::Unsigned(value)) => { + self.white_point_chromaticity_x = u16_from_unsigned(field_name, value)?; + Ok(()) + } + ("WhitePointChromaticityY", FieldValue::Unsigned(value)) => { + self.white_point_chromaticity_y = u16_from_unsigned(field_name, value)?; + Ok(()) + } + ("LuminanceMax", FieldValue::Unsigned(value)) => { + self.luminance_max = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("LuminanceMin", FieldValue::Unsigned(value)) => { + self.luminance_min = u32_from_unsigned(field_name, value)?; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for SmDm { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("PrimaryRChromaticityX", 2, with_bit_width(16)), + codec_field!("PrimaryRChromaticityY", 3, with_bit_width(16)), + codec_field!("PrimaryGChromaticityX", 4, with_bit_width(16)), + codec_field!("PrimaryGChromaticityY", 5, with_bit_width(16)), + codec_field!("PrimaryBChromaticityX", 6, with_bit_width(16)), + codec_field!("PrimaryBChromaticityY", 7, with_bit_width(16)), + codec_field!("WhitePointChromaticityX", 8, with_bit_width(16)), + codec_field!("WhitePointChromaticityY", 9, with_bit_width(16)), + codec_field!("LuminanceMax", 10, with_bit_width(32)), + codec_field!("LuminanceMin", 11, with_bit_width(32)), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; +} + /// Scheme-type declaration box inside a protection-scheme path. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Schm { @@ -6257,6 +9949,11 @@ impl Default for SampleEntry { } /// Visual sample-entry wrapper used by multiple codec-specific visual types. +/// +/// Child boxes remain outside this typed header model and are still traversed through the normal +/// structure-walking APIs. Some files also carry opaque bytes after the last valid child box; the +/// box walker and rewrite helpers preserve those layouts instead of failing late while descending +/// into the trailing payload. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct VisualSampleEntry { pub sample_entry: SampleEntry, @@ -6420,6 +10117,95 @@ impl CodecBox for VisualSampleEntry { codec_field!("Depth", 14, with_bit_width(16)), codec_field!("PreDefined3", 15, with_bit_width(16), as_signed()), ]); + + fn custom_unmarshal( + &mut self, + reader: &mut dyn ReadSeek, + payload_size: u64, + ) -> Result, CodecError> { + const VISUAL_SAMPLE_ENTRY_HEADER_SIZE: usize = 78; + + let start = reader.stream_position()?; + let payload_len = usize::try_from(payload_size) + .map_err(|_| invalid_value("Payload", "payload is too large to decode"))?; + let payload = read_exact_vec_untrusted(reader, payload_len).map_err(CodecError::Io)?; + if payload.len() < VISUAL_SAMPLE_ENTRY_HEADER_SIZE { + return Err(invalid_value("Payload", "payload is too short").into()); + } + + if read_u16(&payload, 0) != 0 { + return Err(CodecError::ConstantMismatch { + field_name: "Reserved0A", + constant: "0", + }); + } + if read_u16(&payload, 2) != 0 { + return Err(CodecError::ConstantMismatch { + field_name: "Reserved0B", + constant: "0", + }); + } + if read_u16(&payload, 4) != 0 { + return Err(CodecError::ConstantMismatch { + field_name: "Reserved0C", + constant: "0", + }); + } + if read_u16(&payload, 10) != 0 { + return Err(CodecError::ConstantMismatch { + field_name: "Reserved1", + constant: "0", + }); + } + + self.sample_entry.data_reference_index = read_u16(&payload, 6); + self.pre_defined = read_u16(&payload, 8); + self.pre_defined2 = [ + read_u32(&payload, 12), + read_u32(&payload, 16), + read_u32(&payload, 20), + ]; + self.width = read_u16(&payload, 24); + self.height = read_u16(&payload, 26); + self.horizresolution = read_u32(&payload, 28); + self.vertresolution = read_u32(&payload, 32); + self.reserved2 = read_u32(&payload, 36); + self.frame_count = read_u16(&payload, 40); + self.compressorname = payload[42..74].try_into().unwrap(); + self.depth = read_u16(&payload, 74); + self.pre_defined3 = read_i16(&payload, 76); + + reader.seek(SeekFrom::Start( + start + VISUAL_SAMPLE_ENTRY_HEADER_SIZE as u64, + ))?; + Ok(Some(VISUAL_SAMPLE_ENTRY_HEADER_SIZE as u64)) + } +} + +pub(crate) fn split_box_children_with_optional_trailing_bytes(bytes: &[u8]) -> usize { + let mut cursor = Cursor::new(bytes); + let mut child_payload_len = 0usize; + + loop { + let start = cursor.position(); + let remaining = (bytes.len() as u64).saturating_sub(start); + if remaining < SMALL_HEADER_SIZE { + break; + } + + let info = match BoxInfo::read(&mut cursor) { + Ok(info) => info, + Err(_) => break, + }; + if info.extend_to_eof() || info.size() < info.header_size() || info.size() > remaining { + break; + } + + cursor.set_position(start + info.size()); + child_payload_len = cursor.position() as usize; + } + + child_payload_len } /// Audio sample-entry wrapper used by multiple codec-specific audio types. @@ -7865,15 +11651,23 @@ fn matches_audio_sample_entry_context(box_type: FourCc, context: BoxLookupContex pub fn register_boxes(registry: &mut BoxRegistry) { registry.register::(FourCc::from_bytes(*b"avcC")); registry.register::(FourCc::from_bytes(*b"btrt")); + registry.register::(FourCc::from_bytes(*b"cdat")); + registry.register::(FourCc::from_bytes(*b"clap")); registry.register::(FourCc::from_bytes(*b"colr")); + registry.register::(FourCc::from_bytes(*b"CoLL")); registry.register::(FourCc::from_bytes(*b"co64")); registry.register::(FourCc::from_bytes(*b"cslg")); registry.register::(FourCc::from_bytes(*b"ctts")); registry.register::(FourCc::from_bytes(*b"dinf")); registry.register::(FourCc::from_bytes(*b"dref")); registry.register::(FourCc::from_bytes(*b"edts")); + registry.register::(FourCc::from_bytes(*b"elng")); registry.register::(FourCc::from_bytes(*b"elst")); + registry.register::(FourCc::from_bytes(*b"emeb")); + registry.register::(FourCc::from_bytes(*b"emib")); registry.register::(FourCc::from_bytes(*b"emsg")); + registry.register::(FourCc::from_bytes(*b"evte")); + registry.register::(FourCc::from_bytes(*b"alou")); registry.register_any::(FourCc::from_bytes(*b"avc1")); registry.register_contextual_any::( FourCc::from_bytes(*b"enca"), @@ -7888,6 +11682,9 @@ pub fn register_boxes(registry: &mut BoxRegistry) { registry.register::(FourCc::from_bytes(*b"hvcC")); registry.register_any::(FourCc::from_bytes(*b"hev1")); registry.register_any::(FourCc::from_bytes(*b"hvc1")); + registry.register::(FourCc::from_bytes(*b"kind")); + registry.register::(FourCc::from_bytes(*b"leva")); + registry.register::(FourCc::from_bytes(*b"ludt")); registry.register::(FourCc::from_bytes(*b"mdat")); registry.register::(FourCc::from_bytes(*b"mdhd")); registry.register::(FourCc::from_bytes(*b"mdia")); @@ -7896,6 +11693,9 @@ pub fn register_boxes(registry: &mut BoxRegistry) { registry.register::(FourCc::from_bytes(*b"mfhd")); registry.register::(FourCc::from_bytes(*b"mfra")); registry.register::(FourCc::from_bytes(*b"mfro")); + registry.register::(FourCc::from_bytes(*b"mime")); + registry.register::(FourCc::from_bytes(*b"nmhd")); + registry.register::(FourCc::from_bytes(*b"prft")); registry.register::(FourCc::from_bytes(*b"minf")); registry.register::(FourCc::from_bytes(*b"moof")); registry.register::(FourCc::from_bytes(*b"moov")); @@ -7913,6 +11713,7 @@ pub fn register_boxes(registry: &mut BoxRegistry) { registry.register::(FourCc::from_bytes(*b"sbgp")); registry.register::(FourCc::from_bytes(*b"schi")); registry.register::(FourCc::from_bytes(*b"schm")); + registry.register::(FourCc::from_bytes(*b"silb")); registry.register::(FourCc::from_bytes(*b"sbtt")); registry.register::(FourCc::from_bytes(*b"sdtp")); registry.register::(FourCc::from_bytes(*b"sgpd")); @@ -7920,6 +11721,9 @@ pub fn register_boxes(registry: &mut BoxRegistry) { registry.register::(FourCc::from_bytes(*b"sinf")); registry.register::(FourCc::from_bytes(*b"skip")); registry.register::(FourCc::from_bytes(*b"smhd")); + registry.register::(FourCc::from_bytes(*b"SmDm")); + registry.register::(FourCc::from_bytes(*b"ssix")); + registry.register::(FourCc::from_bytes(*b"sthd")); registry.register::(FourCc::from_bytes(*b"stbl")); registry.register::(FourCc::from_bytes(*b"stco")); registry.register::(FourCc::from_bytes(*b"stsc")); @@ -7928,18 +11732,33 @@ pub fn register_boxes(registry: &mut BoxRegistry) { registry.register::(FourCc::from_bytes(*b"stsz")); registry.register::(FourCc::from_bytes(*b"stts")); registry.register::(FourCc::from_bytes(*b"styp")); + registry.register::(FourCc::from_bytes(*b"subs")); registry.register::(FourCc::from_bytes(*b"tfdt")); registry.register::(FourCc::from_bytes(*b"tfhd")); registry.register::(FourCc::from_bytes(*b"tfra")); registry.register::(FourCc::from_bytes(*b"traf")); registry.register::(FourCc::from_bytes(*b"trak")); + registry.register::(FourCc::from_bytes(*b"tlou")); + registry.register::(FourCc::from_bytes(*b"tref")); registry.register::(FourCc::from_bytes(*b"trep")); registry.register::(FourCc::from_bytes(*b"trex")); registry.register::(FourCc::from_bytes(*b"trun")); registry.register::(FourCc::from_bytes(*b"tkhd")); + registry.register::(FourCc::from_bytes(*b"cdsc")); + registry.register::(FourCc::from_bytes(*b"dpnd")); + registry.register::(FourCc::from_bytes(*b"font")); + registry.register::(FourCc::from_bytes(*b"hind")); + registry.register::(FourCc::from_bytes(*b"hint")); + registry.register::(FourCc::from_bytes(*b"ipir")); + registry.register::(FourCc::from_bytes(*b"mpod")); + registry.register::(FourCc::from_bytes(*b"subt")); registry.register::(FourCc::from_bytes(*b"udta")); + registry.register::(FourCc::from_bytes(*b"uuid")); registry.register::(FourCc::from_bytes(*b"url ")); registry.register::(FourCc::from_bytes(*b"urn ")); + registry.register::(FourCc::from_bytes(*b"sync")); + registry.register::(FourCc::from_bytes(*b"vdep")); + registry.register::(FourCc::from_bytes(*b"vplx")); registry.register::(FourCc::from_bytes(*b"vmhd")); registry.register::(FourCc::from_bytes(*b"wave")); registry.register::(FourCc::from_bytes(*b"stpp")); diff --git a/src/boxes/iso14496_15.rs b/src/boxes/iso14496_15.rs new file mode 100644 index 0000000..a16b271 --- /dev/null +++ b/src/boxes/iso14496_15.rs @@ -0,0 +1,131 @@ +//! ISO/IEC 14496-15 VVC sample-entry and decoder-configuration box definitions. + +use super::iso14496_12::VisualSampleEntry; +use crate::boxes::BoxRegistry; +use crate::codec::{ + CodecBox, FieldHooks, FieldTable, FieldValue, FieldValueError, FieldValueRead, FieldValueWrite, + ImmutableBox, MutableBox, +}; +use crate::{FourCc, codec_field}; + +fn missing_field(field_name: &'static str) -> FieldValueError { + FieldValueError::MissingField { field_name } +} + +fn unexpected_field(field_name: &'static str, value: FieldValue) -> FieldValueError { + FieldValueError::UnexpectedType { + field_name, + expected: "matching codec field value", + actual: value.kind_name(), + } +} + +fn invalid_value(field_name: &'static str, reason: &'static str) -> FieldValueError { + FieldValueError::InvalidValue { field_name, reason } +} + +fn u8_from_unsigned(field_name: &'static str, value: u64) -> Result { + u8::try_from(value).map_err(|_| invalid_value(field_name, "value does not fit in u8")) +} + +fn u32_from_unsigned(field_name: &'static str, value: u64) -> Result { + if value > 0x00ff_ffff { + return Err(invalid_value(field_name, "value does not fit in 24 bits")); + } + Ok(value as u32) +} + +/// VVC decoder configuration box carried by `vvc1` and `vvi1` sample entries. +/// +/// The decoder configuration record is preserved as raw bytes so typed extraction, rewriting, and +/// registry lookup can land cleanly before deeper VVC record parsing is added. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct VVCDecoderConfiguration { + pub version: u8, + pub flags: u32, + pub decoder_configuration_record: Vec, +} + +impl FieldHooks for VVCDecoderConfiguration {} + +impl ImmutableBox for VVCDecoderConfiguration { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"vvcC") + } + + fn version(&self) -> u8 { + self.version + } + + fn flags(&self) -> u32 { + self.flags + } +} + +impl MutableBox for VVCDecoderConfiguration { + fn set_version(&mut self, version: u8) { + self.version = version; + } + + fn set_flags(&mut self, flags: u32) { + self.flags = flags; + } +} + +impl FieldValueRead for VVCDecoderConfiguration { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "Version" => Ok(FieldValue::Unsigned(u64::from(self.version))), + "Flags" => Ok(FieldValue::Unsigned(u64::from(self.flags))), + "DecoderConfigurationRecord" => { + Ok(FieldValue::Bytes(self.decoder_configuration_record.clone())) + } + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for VVCDecoderConfiguration { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("Version", FieldValue::Unsigned(value)) => { + self.version = u8_from_unsigned(field_name, value)?; + Ok(()) + } + ("Flags", FieldValue::Unsigned(value)) => { + self.flags = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("DecoderConfigurationRecord", FieldValue::Bytes(value)) => { + self.decoder_configuration_record = value; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for VVCDecoderConfiguration { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!( + "DecoderConfigurationRecord", + 2, + with_bit_width(8), + as_bytes() + ), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; +} + +/// Registers the currently implemented ISO/IEC 14496-15 boxes in `registry`. +pub fn register_boxes(registry: &mut BoxRegistry) { + registry.register_any::(FourCc::from_bytes(*b"vvc1")); + registry.register_any::(FourCc::from_bytes(*b"vvi1")); + registry.register::(FourCc::from_bytes(*b"vvcC")); +} diff --git a/src/boxes/iso23001_7.rs b/src/boxes/iso23001_7.rs index 2937ef3..8e22a9d 100644 --- a/src/boxes/iso23001_7.rs +++ b/src/boxes/iso23001_7.rs @@ -1,9 +1,12 @@ //! ISO/IEC 23001-7 common-encryption box definitions. +use std::io::{SeekFrom, Write}; + use crate::boxes::BoxRegistry; use crate::codec::{ - CodecBox, FieldHooks, FieldTable, FieldValue, FieldValueError, FieldValueRead, FieldValueWrite, - ImmutableBox, MutableBox, + CodecBox, CodecError, FieldHooks, FieldTable, FieldValue, FieldValueError, FieldValueRead, + FieldValueWrite, ImmutableBox, MutableBox, ReadSeek, read_exact_vec_untrusted, + untrusted_prealloc_hint, }; use crate::{FourCc, codec_field}; @@ -97,6 +100,288 @@ fn render_uuid(value: &[u8; 16]) -> String { ) } +fn read_u32(bytes: &[u8], offset: usize) -> u32 { + u32::from_be_bytes(bytes[offset..offset + 4].try_into().unwrap()) +} + +fn render_hex_bytes(bytes: &[u8]) -> String { + render_array(bytes.iter().map(|byte| format!("0x{:x}", byte))) +} + +fn render_senc_subsamples(subsamples: &[SencSubsample]) -> String { + render_array(subsamples.iter().map(|subsample| { + format!( + "{{BytesOfClearData={} BytesOfProtectedData={}}}", + subsample.bytes_of_clear_data, subsample.bytes_of_protected_data + ) + })) +} + +pub(crate) fn render_senc_samples_display(samples: &[SencSample]) -> String { + render_array(samples.iter().map(|sample| { + let mut parts = vec![format!( + "InitializationVector={}", + render_hex_bytes(&sample.initialization_vector) + )]; + if !sample.subsamples.is_empty() { + parts.push(format!( + "Subsamples={}", + render_senc_subsamples(&sample.subsamples) + )); + } + format!("{{{}}}", parts.join(" ")) + })) +} + +fn require_senc_sample_count( + field_name: &'static str, + sample_count: u32, + actual_count: usize, +) -> Result<(), CodecError> { + if usize::try_from(sample_count).ok() != Some(actual_count) { + return Err(CodecError::InvalidLength { + field_name, + expected: usize::try_from(sample_count).unwrap_or(usize::MAX), + actual: actual_count, + }); + } + + Ok(()) +} + +pub(crate) fn encode_senc_payload(senc: &Senc) -> Result, CodecError> { + if !senc.is_supported_version(senc.version()) { + return Err(CodecError::UnsupportedVersion { + box_type: senc.box_type(), + version: senc.version(), + }); + } + validate_senc_flags(senc.flags())?; + require_senc_sample_count("Samples", senc.sample_count, senc.samples.len())?; + + let mut payload = Vec::new(); + payload.push(senc.version()); + payload.extend_from_slice(&(senc.flags() & 0x00ff_ffff).to_be_bytes()[1..]); + payload.extend_from_slice(&senc.sample_count.to_be_bytes()); + payload.extend_from_slice(&encode_senc_samples( + "Samples", + &senc.samples, + senc.uses_subsample_encryption(), + )?); + Ok(payload) +} + +pub(crate) fn decode_senc_payload(payload: &[u8]) -> Result { + if payload.len() < 8 { + return Err(invalid_value("Payload", "payload is too short").into()); + } + + let version = payload[0]; + let flags = u32::from_be_bytes([0, payload[1], payload[2], payload[3]]); + let mut senc = Senc::default(); + if !senc.is_supported_version(version) { + return Err(CodecError::UnsupportedVersion { + box_type: senc.box_type(), + version, + }); + } + validate_senc_flags(flags)?; + + let sample_count = read_u32(payload, 4); + let samples = parse_senc_samples( + "Samples", + &payload[8..], + sample_count, + flags & SENC_USE_SUBSAMPLE_ENCRYPTION != 0, + )?; + + senc.set_version(version); + senc.set_flags(flags); + senc.sample_count = sample_count; + senc.samples = samples; + Ok(senc) +} + +fn resolve_senc_iv_size( + field_name: &'static str, + samples: &[SencSample], +) -> Result { + let Some(first) = samples.first() else { + return Ok(0); + }; + + let iv_size = u8::try_from(first.initialization_vector.len()).map_err(|_| { + invalid_value( + field_name, + "initialization vector length does not fit in u8", + ) + })?; + if samples + .iter() + .any(|sample| sample.initialization_vector.len() != usize::from(iv_size)) + { + return Err(invalid_value( + field_name, + "sample IV lengths do not match across entries", + )); + } + + Ok(iv_size) +} + +fn encode_senc_samples( + field_name: &'static str, + samples: &[SencSample], + use_subsample_encryption: bool, +) -> Result, FieldValueError> { + let iv_size = usize::from(resolve_senc_iv_size(field_name, samples)?); + let mut bytes = Vec::new(); + + for sample in samples { + if sample.initialization_vector.len() != iv_size { + return Err(invalid_value( + field_name, + "sample IV lengths do not match across entries", + )); + } + + bytes.extend_from_slice(&sample.initialization_vector); + if use_subsample_encryption { + let subsample_count = u16::try_from(sample.subsamples.len()) + .map_err(|_| invalid_value(field_name, "subsample count does not fit in u16"))?; + bytes.extend_from_slice(&subsample_count.to_be_bytes()); + for subsample in &sample.subsamples { + bytes.extend_from_slice(&subsample.bytes_of_clear_data.to_be_bytes()); + bytes.extend_from_slice(&subsample.bytes_of_protected_data.to_be_bytes()); + } + } else if !sample.subsamples.is_empty() { + return Err(invalid_value( + field_name, + "subsample records require the UseSubSampleEncryption flag", + )); + } + } + + Ok(bytes) +} + +fn try_parse_senc_samples_with_iv_size( + bytes: &[u8], + sample_count: usize, + iv_size: usize, + use_subsample_encryption: bool, +) -> Option> { + let mut cursor = 0_usize; + let mut samples = Vec::with_capacity(untrusted_prealloc_hint(sample_count)); + + for _ in 0..sample_count { + if bytes.len().saturating_sub(cursor) < iv_size { + return None; + } + + let initialization_vector = bytes[cursor..cursor + iv_size].to_vec(); + cursor += iv_size; + + let mut subsamples = Vec::new(); + if use_subsample_encryption { + if bytes.len().saturating_sub(cursor) < 2 { + return None; + } + + let subsample_count = + usize::from(u16::from_be_bytes([bytes[cursor], bytes[cursor + 1]])); + cursor += 2; + + let subsample_bytes = subsample_count.checked_mul(6)?; + if bytes.len().saturating_sub(cursor) < subsample_bytes { + return None; + } + + subsamples = Vec::with_capacity(untrusted_prealloc_hint(subsample_count)); + for _ in 0..subsample_count { + subsamples.push(SencSubsample { + bytes_of_clear_data: u16::from_be_bytes([bytes[cursor], bytes[cursor + 1]]), + bytes_of_protected_data: u32::from_be_bytes([ + bytes[cursor + 2], + bytes[cursor + 3], + bytes[cursor + 4], + bytes[cursor + 5], + ]), + }); + cursor += 6; + } + } + + samples.push(SencSample { + initialization_vector, + subsamples, + }); + } + + (cursor == bytes.len()).then_some(samples) +} + +fn parse_senc_samples( + field_name: &'static str, + bytes: &[u8], + sample_count: u32, + use_subsample_encryption: bool, +) -> Result, FieldValueError> { + let sample_count = usize::try_from(sample_count) + .map_err(|_| invalid_value(field_name, "sample count does not fit in usize"))?; + if sample_count == 0 { + if !bytes.is_empty() { + return Err(invalid_value( + field_name, + "sample payload must be empty when sample count is zero", + )); + } + return Ok(Vec::new()); + } + + let max_iv_size = bytes.len().min(usize::from(u8::MAX)); + let mut matched = None; + for iv_size in 0..=max_iv_size { + let Some(samples) = try_parse_senc_samples_with_iv_size( + bytes, + sample_count, + iv_size, + use_subsample_encryption, + ) else { + continue; + }; + + if matched.is_some() { + return Err(invalid_value( + field_name, + "sample IV size is ambiguous without external encryption parameters", + )); + } + matched = Some(samples); + } + + matched.ok_or_else(|| { + invalid_value( + field_name, + "sample IV size cannot be inferred from the payload", + ) + }) +} + +fn validate_senc_flags(flags: u32) -> Result<(), FieldValueError> { + if flags & !SENC_USE_SUBSAMPLE_ENCRYPTION != 0 { + return Err(invalid_value( + "Flags", + "unsupported SampleEncryptionBox flags are set", + )); + } + + Ok(()) +} + +/// `senc` flag indicating that per-sample subsample encryption records are present. +pub const SENC_USE_SUBSAMPLE_ENCRYPTION: u32 = 0x000002; + /// Protection-system-specific header box. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Pssh { @@ -395,8 +680,177 @@ impl CodecBox for Tenc { const SUPPORTED_VERSIONS: &'static [u8] = &[0, 1]; } +/// One clear/protected byte-range pair carried by a subsample-encrypted sample. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SencSubsample { + pub bytes_of_clear_data: u16, + pub bytes_of_protected_data: u32, +} + +/// One sample-specific encryption record carried by `senc`. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct SencSample { + pub initialization_vector: Vec, + pub subsamples: Vec, +} + +/// Sample encryption box that stores per-sample IVs and optional subsample ranges. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Senc { + full_box: FullBoxState, + pub sample_count: u32, + pub samples: Vec, +} + +impl FieldHooks for Senc { + fn display_field(&self, name: &'static str) -> Option { + match name { + "Samples" => Some(render_senc_samples_display(&self.samples)), + _ => None, + } + } +} + +impl ImmutableBox for Senc { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"senc") + } + + fn version(&self) -> u8 { + self.full_box.version + } + + fn flags(&self) -> u32 { + self.full_box.flags + } +} + +impl MutableBox for Senc { + fn set_version(&mut self, version: u8) { + self.full_box.version = version; + } + + fn set_flags(&mut self, flags: u32) { + self.full_box.flags = flags; + } +} + +impl Senc { + /// Returns `true` when the payload carries per-sample subsample encryption data. + pub fn uses_subsample_encryption(&self) -> bool { + self.flags() & SENC_USE_SUBSAMPLE_ENCRYPTION != 0 + } + + /// Returns the shared per-sample IV size when every sample record uses the same width. + pub fn per_sample_iv_size(&self) -> Option { + (!self.samples.is_empty()) + .then(|| resolve_senc_iv_size("Samples", &self.samples).ok()) + .flatten() + } +} + +impl FieldValueRead for Senc { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "SampleCount" => Ok(FieldValue::Unsigned(u64::from(self.sample_count))), + "Samples" => { + if usize::try_from(self.sample_count).ok() != Some(self.samples.len()) { + return Err(invalid_value( + field_name, + "sample count does not match the number of sample records", + )); + } + Ok(FieldValue::Bytes(encode_senc_samples( + field_name, + &self.samples, + self.uses_subsample_encryption(), + )?)) + } + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Senc { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("SampleCount", FieldValue::Unsigned(value)) => { + self.sample_count = u32_from_unsigned(field_name, value)?; + Ok(()) + } + ("Samples", FieldValue::Bytes(bytes)) => { + validate_senc_flags(self.flags())?; + if !self.is_supported_version(self.version()) { + return Err(invalid_value( + field_name, + "unsupported SampleEncryptionBox version", + )); + } + self.samples = parse_senc_samples( + field_name, + &bytes, + self.sample_count, + self.uses_subsample_encryption(), + )?; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Senc { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!("SampleCount", 2, with_bit_width(32)), + codec_field!("Samples", 3, with_bit_width(8), as_bytes()), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; + + fn custom_marshal(&self, writer: &mut dyn Write) -> Result, CodecError> { + let payload = encode_senc_payload(self)?; + writer.write_all(&payload)?; + Ok(Some(payload.len() as u64)) + } + + fn custom_unmarshal( + &mut self, + reader: &mut dyn ReadSeek, + payload_size: u64, + ) -> Result, CodecError> { + let start = reader.stream_position()?; + let payload_len = usize::try_from(payload_size) + .map_err(|_| invalid_value("Payload", "payload is too large to decode"))?; + let payload = match read_exact_vec_untrusted(reader, payload_len) { + Ok(payload) => payload, + Err(error) => { + reader.seek(SeekFrom::Start(start))?; + return Err(error.into()); + } + }; + + let parse_result = (|| -> Result<(), CodecError> { + *self = decode_senc_payload(&payload)?; + Ok(()) + })(); + + if let Err(error) = parse_result { + reader.seek(SeekFrom::Start(start))?; + return Err(error); + } + + Ok(Some(payload_size)) + } +} + /// Registers the currently implemented ISO/IEC 23001-7 boxes in `registry`. pub fn register_boxes(registry: &mut BoxRegistry) { registry.register::(FourCc::from_bytes(*b"pssh")); + registry.register::(FourCc::from_bytes(*b"senc")); registry.register::(FourCc::from_bytes(*b"tenc")); } diff --git a/src/boxes/metadata.rs b/src/boxes/metadata.rs index c5eef02..fb74310 100644 --- a/src/boxes/metadata.rs +++ b/src/boxes/metadata.rs @@ -4,8 +4,9 @@ use std::io::{SeekFrom, Write}; use crate::boxes::{AnyTypeBox, BoxLookupContext, BoxRegistry}; use crate::codec::{ - CodecBox, FieldHooks, FieldTable, FieldValue, FieldValueError, FieldValueRead, FieldValueWrite, - ImmutableBox, MutableBox, ReadSeek, read_exact_vec_untrusted, untrusted_prealloc_hint, + CodecBox, CodecError, FieldHooks, FieldTable, FieldValue, FieldValueError, FieldValueRead, + FieldValueWrite, ImmutableBox, MutableBox, ReadSeek, read_exact_vec_untrusted, + untrusted_prealloc_hint, }; use crate::stringify::stringify; use crate::{BoxInfo, FourCc, codec_field}; @@ -183,6 +184,65 @@ fn quote_fourcc(value: FourCc) -> String { format!("\"{value}\"") } +fn validate_iso639_2_language( + field_name: &'static str, + language: &str, +) -> Result<[u8; 3], FieldValueError> { + let bytes = language.as_bytes(); + if bytes.len() != 3 { + return Err(invalid_value( + field_name, + "value must be exactly 3 lowercase ISO-639-2 letters", + )); + } + + let mut validated = [0_u8; 3]; + for (slot, byte) in validated.iter_mut().zip(bytes.iter().copied()) { + if !byte.is_ascii_lowercase() { + return Err(invalid_value( + field_name, + "value must be exactly 3 lowercase ISO-639-2 letters", + )); + } + *slot = byte; + } + + Ok(validated) +} + +fn encode_iso639_2_language( + field_name: &'static str, + language: &str, +) -> Result { + let [first, second, third] = validate_iso639_2_language(field_name, language)?; + Ok((u16::from(first - b'`') << 10) | (u16::from(second - b'`') << 5) | u16::from(third - b'`')) +} + +fn decode_iso639_2_language( + field_name: &'static str, + packed_language: u16, +) -> Result { + let values = [ + ((packed_language >> 10) & 0x1f) as u8, + ((packed_language >> 5) & 0x1f) as u8, + (packed_language & 0x1f) as u8, + ]; + + let mut language = [0_u8; 3]; + for (slot, value) in language.iter_mut().zip(values) { + if !(1..=26).contains(&value) { + return Err(invalid_value( + field_name, + "language code uses an out-of-range character value", + ) + .into()); + } + *slot = b'`' + value; + } + + Ok(String::from_utf8(language.to_vec()).unwrap()) +} + fn quote_bytes(bytes: &[u8]) -> String { format!("\"{}\"", escape_bytes(bytes)) } @@ -2404,9 +2464,146 @@ impl CodecBox for Keys { ]); } +/// ID3v2 metadata box that stores a packed ISO-639-2 language code plus raw tag bytes. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Id32 { + full_box: FullBoxState, + pub language: String, + pub id3v2_data: Vec, +} + +impl FieldHooks for Id32 {} + +impl ImmutableBox for Id32 { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"ID32") + } + + fn version(&self) -> u8 { + self.full_box.version + } + + fn flags(&self) -> u32 { + self.full_box.flags + } +} + +impl MutableBox for Id32 { + fn set_version(&mut self, version: u8) { + self.full_box.version = version; + } + + fn set_flags(&mut self, flags: u32) { + self.full_box.flags = flags; + } +} + +impl FieldValueRead for Id32 { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "Language" => Ok(FieldValue::String(self.language.clone())), + "ID3v2Data" => Ok(FieldValue::Bytes(self.id3v2_data.clone())), + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for Id32 { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("Language", FieldValue::String(value)) => { + validate_iso639_2_language(field_name, &value)?; + self.language = value; + Ok(()) + } + ("ID3v2Data", FieldValue::Bytes(value)) => { + self.id3v2_data = value; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for Id32 { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("Version", 0, with_bit_width(8), as_version_field()), + codec_field!("Flags", 1, with_bit_width(24), as_flags_field()), + codec_field!( + "Language", + 2, + with_bit_width(8), + as_string(crate::codec::StringFieldMode::RawBox) + ), + codec_field!("ID3v2Data", 3, with_bit_width(8), as_bytes()), + ]); + const SUPPORTED_VERSIONS: &'static [u8] = &[0]; + + fn custom_marshal( + &self, + writer: &mut dyn Write, + ) -> Result, crate::codec::CodecError> { + if self.version() != 0 { + return Err(CodecError::UnsupportedVersion { + box_type: self.box_type(), + version: self.version(), + }); + } + if self.flags() != 0 { + return Err(invalid_value("Flags", "non-zero flags are not supported").into()); + } + + let packed_language = encode_iso639_2_language("Language", &self.language)?; + let mut payload = Vec::with_capacity(6 + self.id3v2_data.len()); + payload.push(self.version()); + payload.extend_from_slice(&self.flags().to_be_bytes()[1..]); + payload.extend_from_slice(&packed_language.to_be_bytes()); + payload.extend_from_slice(&self.id3v2_data); + writer.write_all(&payload)?; + Ok(Some(payload.len() as u64)) + } + + fn custom_unmarshal( + &mut self, + reader: &mut dyn ReadSeek, + payload_size: u64, + ) -> Result, crate::codec::CodecError> { + let payload_len = usize::try_from(payload_size) + .map_err(|_| invalid_value("Payload", "payload is too large to decode"))?; + if payload_len < 6 { + return Err(invalid_value("Payload", "payload is too short").into()); + } + + let payload = read_exact_vec_untrusted(reader, payload_len)?; + let version = payload[0]; + if version != 0 { + return Err(CodecError::UnsupportedVersion { + box_type: self.box_type(), + version, + }); + } + + let flags = u32::from_be_bytes([0, payload[1], payload[2], payload[3]]); + if flags != 0 { + return Err(invalid_value("Flags", "non-zero flags are not supported").into()); + } + + self.full_box = FullBoxState { version, flags }; + self.language = + decode_iso639_2_language("Language", u16::from_be_bytes([payload[4], payload[5]]))?; + self.id3v2_data = payload[6..].to_vec(); + Ok(Some(payload_size)) + } +} + /// Registers the currently landed metadata boxes in `registry`. pub fn register_boxes(registry: &mut BoxRegistry) { registry.register::(FourCc::from_bytes(*b"ilst")); + registry.register::(FourCc::from_bytes(*b"ID32")); registry.register::(FourCc::from_bytes(*b"keys")); registry.register_contextual::( FourCc::from_bytes(*b"data"), diff --git a/src/boxes/mod.rs b/src/boxes/mod.rs index 2dd82ad..f2f6cec 100644 --- a/src/boxes/mod.rs +++ b/src/boxes/mod.rs @@ -7,12 +7,20 @@ use crate::codec::{CodecBox, DynCodecBox}; /// AV1 sample-entry and codec-configuration box definitions. pub mod av1; +/// AVS3 sample-entry and decoder-configuration box definitions. +pub mod avs3; /// ETSI TS 102 366 AC-3 sample-entry and decoder-configuration box definitions. pub mod etsi_ts_102_366; +/// ETSI TS 103 190 AC-4 sample-entry and decoder-configuration box definitions. +pub mod etsi_ts_103_190; +/// FLAC sample-entry and decoder-configuration box definitions. +pub mod flac; /// ISO/IEC 14496-12 box definitions and codec support. pub mod iso14496_12; /// ISO/IEC 14496-14 ES descriptor box definitions and codec support. pub mod iso14496_14; +/// ISO/IEC 14496-15 VVC sample-entry and decoder-configuration box definitions. +pub mod iso14496_15; /// ISO/IEC 14496-30 WebVTT box definitions and codec support. pub mod iso14496_30; /// ISO/IEC 23001-5 uncompressed-audio box definitions and codec support. @@ -21,6 +29,8 @@ pub mod iso23001_5; pub mod iso23001_7; /// Item-list metadata and key-table box definitions. pub mod metadata; +/// MPEG-H sample-entry and decoder-configuration box definitions. +pub mod mpeg_h; /// Opus sample-entry and decoder-configuration box definitions. pub mod opus; /// 3GPP `udta`-scoped metadata string box definitions and codec support. @@ -423,9 +433,14 @@ pub fn default_registry() -> BoxRegistry { metadata::register_boxes(&mut registry); threegpp::register_boxes(&mut registry); av1::register_boxes(&mut registry); + avs3::register_boxes(&mut registry); etsi_ts_102_366::register_boxes(&mut registry); + etsi_ts_103_190::register_boxes(&mut registry); + flac::register_boxes(&mut registry); + mpeg_h::register_boxes(&mut registry); opus::register_boxes(&mut registry); vp::register_boxes(&mut registry); + iso14496_15::register_boxes(&mut registry); iso23001_5::register_boxes(&mut registry); iso23001_7::register_boxes(&mut registry); registry diff --git a/src/boxes/mpeg_h.rs b/src/boxes/mpeg_h.rs new file mode 100644 index 0000000..071aa2b --- /dev/null +++ b/src/boxes/mpeg_h.rs @@ -0,0 +1,154 @@ +//! MPEG-H sample-entry and decoder-configuration box definitions. + +use std::io::Write; + +use super::iso14496_12::AudioSampleEntry; +use crate::boxes::BoxRegistry; +use crate::codec::{ + CodecBox, CodecError, FieldHooks, FieldTable, FieldValue, FieldValueError, FieldValueRead, + FieldValueWrite, ImmutableBox, MutableBox, +}; +use crate::{FourCc, codec_field}; + +fn missing_field(field_name: &'static str) -> FieldValueError { + FieldValueError::MissingField { field_name } +} + +fn unexpected_field(field_name: &'static str, value: FieldValue) -> FieldValueError { + FieldValueError::UnexpectedType { + field_name, + expected: "matching codec field value", + actual: value.kind_name(), + } +} + +fn invalid_value(field_name: &'static str, reason: &'static str) -> FieldValueError { + FieldValueError::InvalidValue { field_name, reason } +} + +fn u8_from_unsigned(field_name: &'static str, value: u64) -> Result { + u8::try_from(value).map_err(|_| invalid_value(field_name, "value does not fit in u8")) +} + +fn u16_from_unsigned(field_name: &'static str, value: u64) -> Result { + u16::try_from(value).map_err(|_| invalid_value(field_name, "value does not fit in u16")) +} + +/// MPEG-H decoder-configuration box carried by MPEG-H audio sample entries. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct MhaC { + /// Decoder-configuration record version. + pub config_version: u8, + /// MPEG-H 3D Audio profile-level indication. + pub mpeg_h_3da_profile_level_indication: u8, + /// Reference-channel-layout code. + pub reference_channel_layout: u8, + /// Declared byte length of the opaque MPEG-H configuration payload. + pub mpeg_h_3da_config_length: u16, + /// Opaque MPEG-H configuration bytes. + pub mpeg_h_3da_config: Vec, +} + +impl FieldHooks for MhaC { + fn field_length(&self, name: &'static str) -> Option { + match name { + "MpegH3DAConfig" => Some(u32::from(self.mpeg_h_3da_config_length)), + _ => None, + } + } +} + +impl ImmutableBox for MhaC { + fn box_type(&self) -> FourCc { + FourCc::from_bytes(*b"mhaC") + } +} + +impl MutableBox for MhaC {} + +impl FieldValueRead for MhaC { + fn field_value(&self, field_name: &'static str) -> Result { + match field_name { + "ConfigVersion" => Ok(FieldValue::Unsigned(u64::from(self.config_version))), + "MpegH3DAProfileLevelIndication" => Ok(FieldValue::Unsigned(u64::from( + self.mpeg_h_3da_profile_level_indication, + ))), + "ReferenceChannelLayout" => Ok(FieldValue::Unsigned(u64::from( + self.reference_channel_layout, + ))), + "MpegH3DAConfigLength" => Ok(FieldValue::Unsigned(u64::from( + self.mpeg_h_3da_config_length, + ))), + "MpegH3DAConfig" => Ok(FieldValue::Bytes(self.mpeg_h_3da_config.clone())), + _ => Err(missing_field(field_name)), + } + } +} + +impl FieldValueWrite for MhaC { + fn set_field_value( + &mut self, + field_name: &'static str, + value: FieldValue, + ) -> Result<(), FieldValueError> { + match (field_name, value) { + ("ConfigVersion", FieldValue::Unsigned(value)) => { + self.config_version = u8_from_unsigned(field_name, value)?; + Ok(()) + } + ("MpegH3DAProfileLevelIndication", FieldValue::Unsigned(value)) => { + self.mpeg_h_3da_profile_level_indication = u8_from_unsigned(field_name, value)?; + Ok(()) + } + ("ReferenceChannelLayout", FieldValue::Unsigned(value)) => { + self.reference_channel_layout = u8_from_unsigned(field_name, value)?; + Ok(()) + } + ("MpegH3DAConfigLength", FieldValue::Unsigned(value)) => { + self.mpeg_h_3da_config_length = u16_from_unsigned(field_name, value)?; + Ok(()) + } + ("MpegH3DAConfig", FieldValue::Bytes(value)) => { + self.mpeg_h_3da_config = value; + Ok(()) + } + (field_name, value) => Err(unexpected_field(field_name, value)), + } + } +} + +impl CodecBox for MhaC { + const FIELD_TABLE: FieldTable = FieldTable::new(&[ + codec_field!("ConfigVersion", 0, with_bit_width(8)), + codec_field!("MpegH3DAProfileLevelIndication", 1, with_bit_width(8)), + codec_field!("ReferenceChannelLayout", 2, with_bit_width(8)), + codec_field!("MpegH3DAConfigLength", 3, with_bit_width(16)), + codec_field!( + "MpegH3DAConfig", + 4, + with_bit_width(8), + with_dynamic_length(), + as_bytes() + ), + ]); + + fn custom_marshal(&self, _writer: &mut dyn Write) -> Result, CodecError> { + if usize::from(self.mpeg_h_3da_config_length) != self.mpeg_h_3da_config.len() { + return Err(invalid_value( + "MpegH3DAConfig", + "length does not match MpegH3DAConfigLength", + ) + .into()); + } + Ok(None) + } +} + +/// Registers the currently implemented MPEG-H boxes in `registry`. +pub fn register_boxes(registry: &mut BoxRegistry) { + registry.register_any::(FourCc::from_bytes(*b"mha1")); + registry.register_any::(FourCc::from_bytes(*b"mha2")); + registry.register_any::(FourCc::from_bytes(*b"mhm1")); + registry.register_any::(FourCc::from_bytes(*b"mhm2")); + registry.register::(FourCc::from_bytes(*b"mhaC")); +} diff --git a/src/cli/divide.rs b/src/cli/divide.rs index 49e0c3b..d06c295 100644 --- a/src/cli/divide.rs +++ b/src/cli/divide.rs @@ -187,7 +187,7 @@ pub enum DivideTrackRole { /// Validation summary for one active fragmented track accepted by `divide`. #[derive(Clone, Debug, PartialEq, Eq)] pub struct DivideValidationTrack { - /// Track identifier selected from `tkhd` and referenced by fragmented runs. + /// Track identifier selected from `tkhd` and used by fragmented runs. pub track_id: u32, /// Role assigned by the current divide layout rules. pub role: DivideTrackRole, diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 85781f1..fc8eb1b 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -10,7 +10,7 @@ pub mod probe; pub mod pssh; pub mod util; -/// Dispatches the top-level command-line arguments to the matching command implementation. +/// Dispatches the top-level command-line arguments to the matching command handler. pub fn dispatch(args: &[String], stdout: &mut W, stderr: &mut E) -> i32 where W: Write, diff --git a/src/cli/probe.rs b/src/cli/probe.rs index 98467a5..19a0f46 100644 --- a/src/cli/probe.rs +++ b/src/cli/probe.rs @@ -8,8 +8,9 @@ use std::io::{self, Read, Seek, Write}; use crate::probe::{ DetailedTrackInfo, ProbeError, ProbeOptions, TrackCodec, TrackCodecDetails, TrackCodecFamily, TrackMediaCharacteristics, average_sample_bitrate, average_segment_bitrate, find_idr_frames, - max_sample_bitrate, max_segment_bitrate, probe_codec_detailed_with_options, - probe_detailed_with_options, probe_media_characteristics_with_options, probe_with_options, + max_sample_bitrate, max_segment_bitrate, normalized_codec_family_name, + probe_codec_detailed_with_options, probe_detailed_with_options, + probe_media_characteristics_with_options, probe_with_options, }; /// Structured output format supported by the probe command. @@ -530,7 +531,12 @@ where duration: basic.duration, duration_seconds: seconds(basic.duration, basic.timescale), codec: detailed_track_codec_string(track), - codec_family: track_codec_family_string(track.codec_family).to_string(), + codec_family: track_codec_family_string( + track.codec_family, + track.sample_entry_type, + track.original_format, + ) + .to_string(), encrypted: basic.encrypted, handler_type: track.handler_type.map(|value| value.to_string()), language: track.language.clone(), @@ -616,7 +622,12 @@ where duration: basic.duration, duration_seconds: seconds(basic.duration, basic.timescale), codec: detailed_track_codec_string(&track.summary), - codec_family: track_codec_family_string(track.summary.codec_family).to_string(), + codec_family: track_codec_family_string( + track.summary.codec_family, + track.summary.sample_entry_type, + track.summary.original_format, + ) + .to_string(), codec_details: track.codec_details.clone(), encrypted: basic.encrypted, handler_type: track.summary.handler_type.map(|value| value.to_string()), @@ -709,7 +720,12 @@ where duration: basic.duration, duration_seconds: seconds(basic.duration, basic.timescale), codec: detailed_track_codec_string(&track.summary), - codec_family: track_codec_family_string(track.summary.codec_family).to_string(), + codec_family: track_codec_family_string( + track.summary.codec_family, + track.summary.sample_entry_type, + track.summary.original_format, + ) + .to_string(), codec_details: track.codec_details.clone(), media_characteristics: track.media_characteristics.clone(), encrypted: basic.encrypted, @@ -920,22 +936,12 @@ fn detailed_track_codec_string(track: &DetailedTrackInfo) -> String { } } -fn track_codec_family_string(family: TrackCodecFamily) -> &'static str { - match family { - TrackCodecFamily::Unknown => "unknown", - TrackCodecFamily::Avc => "avc", - TrackCodecFamily::Hevc => "hevc", - TrackCodecFamily::Av1 => "av1", - TrackCodecFamily::Vp8 => "vp8", - TrackCodecFamily::Vp9 => "vp9", - TrackCodecFamily::Mp4Audio => "mp4_audio", - TrackCodecFamily::Opus => "opus", - TrackCodecFamily::Ac3 => "ac3", - TrackCodecFamily::Pcm => "pcm", - TrackCodecFamily::XmlSubtitle => "xml_subtitle", - TrackCodecFamily::TextSubtitle => "text_subtitle", - TrackCodecFamily::WebVtt => "webvtt", - } +fn track_codec_family_string( + family: TrackCodecFamily, + sample_entry_type: Option, + original_format: Option, +) -> &'static str { + normalized_codec_family_name(family, sample_entry_type, original_format) } fn summarize_bitrate( diff --git a/src/encryption.rs b/src/encryption.rs new file mode 100644 index 0000000..5a224aa --- /dev/null +++ b/src/encryption.rs @@ -0,0 +1,741 @@ +//! Resolved common-encryption metadata helpers built on typed MP4 boxes. +//! +//! These helpers keep the existing low-level box model unchanged while providing a small semantic +//! layer for callers that need the effective sample-encryption parameters for a decoded `senc` +//! payload. + +use std::error::Error; +use std::fmt; + +use crate::FourCc; +use crate::boxes::iso14496_12::{Saiz, Sbgp, SeigEntry, SeigEntryL, Sgpd}; +use crate::boxes::iso23001_7::{ + SENC_USE_SUBSAMPLE_ENCRYPTION, Senc, SencSample, SencSubsample, Tenc, +}; +use crate::codec::ImmutableBox; + +const SEIG_GROUPING_TYPE: FourCc = FourCc::from_bytes(*b"seig"); +const SEIG_GROUPING_TYPE_U32: u32 = u32::from_be_bytes(*b"seig"); +const FRAGMENT_LOCAL_DESCRIPTION_INDEX_BASE: u32 = 1 << 16; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct SampleGroupDescriptionRef { + group_description_index: u32, + description_index: u32, + fragment_local: bool, +} + +struct ResolvedEncryptionDefaults<'a> { + source: ResolvedSampleEncryptionSource, + is_protected: bool, + crypt_byte_block: u8, + skip_byte_block: u8, + per_sample_iv_size: Option, + constant_iv: Option<&'a [u8]>, + kid: [u8; 16], +} + +struct DefaultSourceConfig<'a> { + source_name: &'static str, + is_protected: u8, + crypt_byte_block: u8, + skip_byte_block: u8, + per_sample_iv_size: u8, + constant_iv_size: u8, + constant_iv: &'a [u8], + kid: [u8; 16], + source: ResolvedSampleEncryptionSource, +} + +enum SeigEntries<'a> { + Fixed(&'a [SeigEntry]), + LengthPrefixed(&'a [SeigEntryL]), +} + +impl<'a> SeigEntries<'a> { + fn get(&self, index: usize) -> Option<&'a SeigEntry> { + match self { + Self::Fixed(entries) => entries.get(index), + Self::LengthPrefixed(entries) => entries.get(index).map(|entry| &entry.seig_entry), + } + } +} + +/// Optional typed context used to resolve the effective encryption parameters for one `senc`. +/// +/// Supply whichever typed boxes are already available at the call site. The resolver prefers an +/// explicit `seig` description selected through `sbgp`, then falls back to track-level `tenc` +/// defaults for samples that are not covered by a sample-group override. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct SampleEncryptionContext<'a> { + /// Track-level encryption defaults from the protected sample entry. + pub tenc: Option<&'a Tenc>, + /// Typed `sgpd(seig)` description entries available for the current scope. + pub sgpd: Option<&'a Sgpd>, + /// Typed `sbgp(seig)` sample-to-group mapping for the current `senc`. + pub sbgp: Option<&'a Sbgp>, + /// Optional auxiliary-size box used to validate each resolved sample-info record length. + pub saiz: Option<&'a Saiz>, +} + +/// Resolved semantic view of one decoded `senc` payload. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ResolvedSampleEncryptionInfo<'a> { + /// Whether the source `senc` payload includes subsample-encryption counts and ranges. + pub uses_subsample_encryption: bool, + /// Per-sample resolved encryption metadata in sample order. + pub samples: Vec>, +} + +/// Resolved semantic view of one sample's encryption metadata. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ResolvedSampleEncryptionSample<'a> { + /// One-based sample index inside the resolved `senc` payload. + pub sample_index: u32, + /// Source that supplied the effective encryption defaults for this sample. + pub metadata_source: ResolvedSampleEncryptionSource, + /// Whether the resolved defaults mark the sample as protected. + pub is_protected: bool, + /// Number of encrypted 16-byte blocks in each protection pattern cycle. + pub crypt_byte_block: u8, + /// Number of skipped 16-byte blocks in each protection pattern cycle. + pub skip_byte_block: u8, + /// Per-sample IV size when the sample carries an inline IV. + pub per_sample_iv_size: Option, + /// Per-sample IV bytes read directly from the `senc` sample record. + pub initialization_vector: &'a [u8], + /// Constant IV bytes inherited from the resolved defaults when inline IV bytes are absent. + pub constant_iv: Option<&'a [u8]>, + /// Effective key identifier for the sample. + pub kid: [u8; 16], + /// Decoded subsample-encryption records from the `senc` sample entry. + pub subsamples: &'a [SencSubsample], + /// Resolved auxiliary-information record length for this sample. + pub auxiliary_info_size: u32, +} + +impl<'a> ResolvedSampleEncryptionSample<'a> { + /// Returns the effective IV bytes for the sample, preferring inline `senc` IV bytes when they + /// are present and otherwise falling back to the resolved constant IV. + pub fn effective_initialization_vector(&self) -> &'a [u8] { + if !self.initialization_vector.is_empty() { + self.initialization_vector + } else { + self.constant_iv.unwrap_or(&[]) + } + } +} + +/// Source that supplied the effective encryption defaults for one resolved sample. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ResolvedSampleEncryptionSource { + /// Track-level defaults were taken from the active `tenc`. + TrackEncryptionBox, + /// Sample-level defaults were taken from a `seig` description selected by `sbgp`. + SampleGroupDescription { + /// Raw `group_description_index` value carried by `sbgp`. + group_description_index: u32, + /// One-based typed `seig` description index resolved inside the supplied `sgpd`. + description_index: u32, + /// Whether the resolved description index used fragment-local numbering. + fragment_local: bool, + }, +} + +/// Errors raised while resolving effective sample-encryption metadata. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ResolveSampleEncryptionError { + /// The source `senc` version is not supported by the typed box model. + UnsupportedSencVersion { + /// Unsupported full-box version from `senc`. + version: u8, + }, + /// The source `senc` flags include unsupported bits. + UnsupportedSencFlags { + /// Raw `senc` flag bits. + flags: u32, + }, + /// The declared `senc` sample count does not match the decoded sample records. + SampleCountMismatch { + /// Declared sample count from `senc`. + declared: u32, + /// Actual decoded sample-record count. + actual: usize, + }, + /// One of the supplied typed context boxes is internally inconsistent for resolution. + InvalidConfiguration { + /// Box or helper component whose configuration was invalid. + source: &'static str, + /// Human-readable explanation of the rejected configuration. + reason: &'static str, + }, + /// The supplied `sbgp` does not describe `seig` sample groups. + InvalidSbgpGroupingType { + /// Raw grouping type from `sbgp`. + actual: u32, + }, + /// The supplied `sgpd` does not describe `seig` sample groups. + InvalidSgpdGroupingType { + /// Grouping type from `sgpd`. + actual: FourCc, + }, + /// `sbgp` covers more samples than the resolved `senc` contains. + SampleGroupCoverageExceeded { + /// Declared sample count from `senc`. + sample_count: u32, + /// Number of samples that the `sbgp` mapping attempted to cover. + covered_sample_count: u64, + }, + /// A fragment-local `group_description_index` encoded an invalid zero-based entry. + InvalidFragmentLocalDescriptionIndex { + /// Raw `group_description_index` from `sbgp`. + group_description_index: u32, + }, + /// No `tenc` defaults were available for a sample that was not covered by `sbgp(seig)`. + MissingTrackEncryptionDefaults { + /// One-based sample index that needed a `tenc` fallback. + sample_index: u32, + }, + /// An explicit `sbgp` description entry could not be resolved from the supplied `sgpd`. + MissingSampleGroupDescription { + /// One-based sample index whose override could not be resolved. + sample_index: u32, + /// Raw `group_description_index` from `sbgp`. + group_description_index: u32, + /// One-based typed description index expected inside `sgpd`. + description_index: u32, + /// Whether the missing description used fragment-local numbering. + fragment_local: bool, + }, + /// The sample's inline IV length does not match the resolved per-sample IV size. + SampleInitializationVectorSizeMismatch { + /// One-based sample index whose inline IV length was invalid. + sample_index: u32, + /// Expected inline IV size from the resolved defaults. + expected: usize, + /// Actual inline IV size stored in the decoded `senc` sample. + actual: usize, + }, + /// The sample carried inline IV bytes even though the resolved defaults use a constant IV. + UnexpectedSampleInitializationVector { + /// One-based sample index whose inline IV bytes were unexpected. + sample_index: u32, + /// Actual inline IV size stored in the decoded `senc` sample. + actual: usize, + }, + /// The supplied `saiz` box is internally inconsistent for the current `senc`. + InvalidSaizLayout { + /// Human-readable explanation of the rejected `saiz` layout. + reason: &'static str, + }, + /// The resolved auxiliary-information length does not match `saiz` for one sample. + SaizSampleInfoSizeMismatch { + /// One-based sample index whose resolved size mismatched `saiz`. + sample_index: u32, + /// Resolved auxiliary-information size computed from `senc`. + expected: u32, + /// Actual sample-info size read from `saiz`. + actual: u32, + }, + /// The resolved auxiliary-information size exceeded what the helper can represent. + SampleInfoSizeOverflow { + /// One-based sample index whose resolved size overflowed. + sample_index: u32, + }, +} + +impl fmt::Display for ResolveSampleEncryptionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnsupportedSencVersion { version } => { + write!(f, "unsupported senc version {version}") + } + Self::UnsupportedSencFlags { flags } => { + write!(f, "unsupported senc flags 0x{flags:06x}") + } + Self::SampleCountMismatch { declared, actual } => write!( + f, + "senc sample count mismatch: declared {declared} sample(s) but decoded {actual}" + ), + Self::InvalidConfiguration { source, reason } => { + write!(f, "invalid {source} configuration: {reason}") + } + Self::InvalidSbgpGroupingType { actual } => write!( + f, + "sbgp grouping type must be \"seig\", found 0x{actual:08x}" + ), + Self::InvalidSgpdGroupingType { actual } => { + write!(f, "sgpd grouping type must be \"seig\", found {actual}") + } + Self::SampleGroupCoverageExceeded { + sample_count, + covered_sample_count, + } => write!( + f, + "sbgp sample coverage exceeds senc sample count: covered {covered_sample_count} sample(s) for senc sample count {sample_count}" + ), + Self::InvalidFragmentLocalDescriptionIndex { + group_description_index, + } => write!( + f, + "fragment-local sbgp group description index {group_description_index} resolves to description 0" + ), + Self::MissingTrackEncryptionDefaults { sample_index } => write!( + f, + "sample {sample_index} is not covered by sgpd(seig) and no tenc fallback was supplied" + ), + Self::MissingSampleGroupDescription { + sample_index, + group_description_index, + description_index, + fragment_local, + } => write!( + f, + "sample {sample_index} uses sbgp group description index {group_description_index} but sgpd is missing description {description_index} (fragment_local={fragment_local})" + ), + Self::SampleInitializationVectorSizeMismatch { + sample_index, + expected, + actual, + } => write!( + f, + "sample {sample_index} has inline IV size {actual} but resolved defaults require {expected}" + ), + Self::UnexpectedSampleInitializationVector { + sample_index, + actual, + } => write!( + f, + "sample {sample_index} has unexpected inline IV bytes in constant-IV mode ({actual} byte(s))" + ), + Self::InvalidSaizLayout { reason } => write!(f, "invalid saiz layout: {reason}"), + Self::SaizSampleInfoSizeMismatch { + sample_index, + expected, + actual, + } => write!( + f, + "sample {sample_index} resolved auxiliary info size {expected} does not match saiz size {actual}" + ), + Self::SampleInfoSizeOverflow { sample_index } => write!( + f, + "sample {sample_index} auxiliary info size is too large to represent" + ), + } + } +} + +impl Error for ResolveSampleEncryptionError {} + +/// Resolves the effective encryption metadata for every sample carried by `senc`. +/// +/// The resolver prefers `sgpd(seig)` entries selected by `sbgp`, then falls back to `tenc` +/// defaults for samples that have no explicit group-description override. When `saiz` is present, +/// the resolved auxiliary-information length for every sample is validated against the declared +/// size table. +pub fn resolve_sample_encryption<'a>( + senc: &'a Senc, + context: SampleEncryptionContext<'a>, +) -> Result, ResolveSampleEncryptionError> { + if senc.version() != 0 { + return Err(ResolveSampleEncryptionError::UnsupportedSencVersion { + version: senc.version(), + }); + } + + if senc.flags() & !SENC_USE_SUBSAMPLE_ENCRYPTION != 0 { + return Err(ResolveSampleEncryptionError::UnsupportedSencFlags { + flags: senc.flags(), + }); + } + + if usize::try_from(senc.sample_count).ok() != Some(senc.samples.len()) { + return Err(ResolveSampleEncryptionError::SampleCountMismatch { + declared: senc.sample_count, + actual: senc.samples.len(), + }); + } + + validate_saiz_layout(context.saiz, senc.sample_count)?; + let sample_group_refs = + resolve_sample_group_refs(senc.sample_count, &senc.samples, context.sbgp)?; + let uses_subsample_encryption = senc.uses_subsample_encryption(); + + let mut samples = Vec::with_capacity(senc.samples.len()); + for (sample_offset, sample) in senc.samples.iter().enumerate() { + let sample_index = (sample_offset as u32) + 1; + let defaults = match sample_group_refs[sample_offset] { + Some(group_ref) => resolve_seig_defaults(sample_index, context.sgpd, group_ref)?, + None => resolve_tenc_defaults(sample_index, context.tenc)?, + }; + + validate_sample_initialization_vector(sample_index, sample, &defaults)?; + let auxiliary_info_size = + resolved_sample_info_size(sample_index, sample, uses_subsample_encryption)?; + validate_saiz_sample_info_size(context.saiz, sample_index, auxiliary_info_size)?; + + samples.push(ResolvedSampleEncryptionSample { + sample_index, + metadata_source: defaults.source, + is_protected: defaults.is_protected, + crypt_byte_block: defaults.crypt_byte_block, + skip_byte_block: defaults.skip_byte_block, + per_sample_iv_size: defaults.per_sample_iv_size, + initialization_vector: &sample.initialization_vector, + constant_iv: defaults.constant_iv, + kid: defaults.kid, + subsamples: &sample.subsamples, + auxiliary_info_size, + }); + } + + Ok(ResolvedSampleEncryptionInfo { + uses_subsample_encryption, + samples, + }) +} + +fn validate_saiz_layout( + saiz: Option<&Saiz>, + sample_count: u32, +) -> Result<(), ResolveSampleEncryptionError> { + let Some(saiz) = saiz else { + return Ok(()); + }; + + if saiz.sample_count != sample_count { + return Err(ResolveSampleEncryptionError::InvalidSaizLayout { + reason: "sample count does not match senc", + }); + } + + if saiz.default_sample_info_size == 0 + && usize::try_from(saiz.sample_count).ok() != Some(saiz.sample_info_size.len()) + { + return Err(ResolveSampleEncryptionError::InvalidSaizLayout { + reason: "per-sample size table length does not match the declared sample count", + }); + } + + Ok(()) +} + +fn resolve_sample_group_refs( + sample_count: u32, + samples: &[SencSample], + sbgp: Option<&Sbgp>, +) -> Result>, ResolveSampleEncryptionError> { + let mut group_refs = vec![None; samples.len()]; + let Some(sbgp) = sbgp else { + return Ok(group_refs); + }; + + if sbgp.grouping_type != SEIG_GROUPING_TYPE_U32 { + return Err(ResolveSampleEncryptionError::InvalidSbgpGroupingType { + actual: sbgp.grouping_type, + }); + } + + let mut cursor = 0usize; + for entry in &sbgp.entries { + let entry_sample_count = + usize::try_from(entry.sample_count).unwrap_or(group_refs.len().saturating_add(1)); + let next = cursor.saturating_add(entry_sample_count); + if next > group_refs.len() { + return Err(ResolveSampleEncryptionError::SampleGroupCoverageExceeded { + sample_count, + covered_sample_count: next as u64, + }); + } + + let normalized = normalize_group_description_index(entry.group_description_index)?; + for slot in &mut group_refs[cursor..next] { + *slot = normalized; + } + cursor = next; + } + + Ok(group_refs) +} + +fn normalize_group_description_index( + group_description_index: u32, +) -> Result, ResolveSampleEncryptionError> { + if group_description_index == 0 { + return Ok(None); + } + + if group_description_index >= FRAGMENT_LOCAL_DESCRIPTION_INDEX_BASE { + let description_index = group_description_index - FRAGMENT_LOCAL_DESCRIPTION_INDEX_BASE; + if description_index == 0 { + return Err( + ResolveSampleEncryptionError::InvalidFragmentLocalDescriptionIndex { + group_description_index, + }, + ); + } + return Ok(Some(SampleGroupDescriptionRef { + group_description_index, + description_index, + fragment_local: true, + })); + } + + Ok(Some(SampleGroupDescriptionRef { + group_description_index, + description_index: group_description_index, + fragment_local: false, + })) +} + +fn resolve_tenc_defaults<'a>( + sample_index: u32, + tenc: Option<&'a Tenc>, +) -> Result, ResolveSampleEncryptionError> { + let Some(tenc) = tenc else { + return Err(ResolveSampleEncryptionError::MissingTrackEncryptionDefaults { sample_index }); + }; + + resolve_defaults(DefaultSourceConfig { + source_name: "tenc", + is_protected: tenc.default_is_protected, + crypt_byte_block: tenc.default_crypt_byte_block, + skip_byte_block: tenc.default_skip_byte_block, + per_sample_iv_size: tenc.default_per_sample_iv_size, + constant_iv_size: tenc.default_constant_iv_size, + constant_iv: &tenc.default_constant_iv, + kid: tenc.default_kid, + source: ResolvedSampleEncryptionSource::TrackEncryptionBox, + }) +} + +fn resolve_seig_defaults<'a>( + sample_index: u32, + sgpd: Option<&'a Sgpd>, + group_ref: SampleGroupDescriptionRef, +) -> Result, ResolveSampleEncryptionError> { + let Some(sgpd) = sgpd else { + return Err( + ResolveSampleEncryptionError::MissingSampleGroupDescription { + sample_index, + group_description_index: group_ref.group_description_index, + description_index: group_ref.description_index, + fragment_local: group_ref.fragment_local, + }, + ); + }; + + if sgpd.grouping_type != SEIG_GROUPING_TYPE { + return Err(ResolveSampleEncryptionError::InvalidSgpdGroupingType { + actual: sgpd.grouping_type, + }); + } + + let description_offset = + usize::try_from(group_ref.description_index.saturating_sub(1)).unwrap_or(usize::MAX); + let entry = typed_seig_entries(sgpd)?.get(description_offset).ok_or( + ResolveSampleEncryptionError::MissingSampleGroupDescription { + sample_index, + group_description_index: group_ref.group_description_index, + description_index: group_ref.description_index, + fragment_local: group_ref.fragment_local, + }, + )?; + + resolve_defaults(DefaultSourceConfig { + source_name: "sgpd(seig)", + is_protected: entry.is_protected, + crypt_byte_block: entry.crypt_byte_block, + skip_byte_block: entry.skip_byte_block, + per_sample_iv_size: entry.per_sample_iv_size, + constant_iv_size: entry.constant_iv_size, + constant_iv: &entry.constant_iv, + kid: entry.kid, + source: ResolvedSampleEncryptionSource::SampleGroupDescription { + group_description_index: group_ref.group_description_index, + description_index: group_ref.description_index, + fragment_local: group_ref.fragment_local, + }, + }) +} + +fn typed_seig_entries<'a>(sgpd: &'a Sgpd) -> Result, ResolveSampleEncryptionError> { + match (sgpd.seig_entries.is_empty(), sgpd.seig_entries_l.is_empty()) { + (false, false) => Err(ResolveSampleEncryptionError::InvalidConfiguration { + source: "sgpd", + reason: "typed seig entries are populated in both fixed-length and length-prefixed storage", + }), + (false, true) => Ok(SeigEntries::Fixed(&sgpd.seig_entries)), + (true, false) => Ok(SeigEntries::LengthPrefixed(&sgpd.seig_entries_l)), + (true, true) if sgpd.entry_count == 0 => Ok(SeigEntries::Fixed(&[])), + (true, true) => Err(ResolveSampleEncryptionError::InvalidConfiguration { + source: "sgpd", + reason: "typed seig entries are not populated for the declared entry count", + }), + } +} + +fn resolve_defaults<'a>( + config: DefaultSourceConfig<'a>, +) -> Result, ResolveSampleEncryptionError> { + let is_protected = match config.is_protected { + 0 => false, + 1 => true, + _ => { + return Err(ResolveSampleEncryptionError::InvalidConfiguration { + source: config.source_name, + reason: "IsProtected must be either 0 or 1", + }); + } + }; + + let has_constant_iv = config.constant_iv_size != 0 || !config.constant_iv.is_empty(); + if !is_protected { + if config.per_sample_iv_size != 0 { + return Err(ResolveSampleEncryptionError::InvalidConfiguration { + source: config.source_name, + reason: "unprotected samples must not declare a per-sample IV size", + }); + } + if has_constant_iv { + return Err(ResolveSampleEncryptionError::InvalidConfiguration { + source: config.source_name, + reason: "unprotected samples must not declare a constant IV", + }); + } + + return Ok(ResolvedEncryptionDefaults { + source: config.source, + is_protected, + crypt_byte_block: config.crypt_byte_block, + skip_byte_block: config.skip_byte_block, + per_sample_iv_size: None, + constant_iv: None, + kid: config.kid, + }); + } + + if config.per_sample_iv_size != 0 { + if has_constant_iv { + return Err(ResolveSampleEncryptionError::InvalidConfiguration { + source: config.source_name, + reason: "per-sample IV mode must not also declare a constant IV", + }); + } + + return Ok(ResolvedEncryptionDefaults { + source: config.source, + is_protected, + crypt_byte_block: config.crypt_byte_block, + skip_byte_block: config.skip_byte_block, + per_sample_iv_size: Some(config.per_sample_iv_size), + constant_iv: None, + kid: config.kid, + }); + } + + if usize::from(config.constant_iv_size) != config.constant_iv.len() { + return Err(ResolveSampleEncryptionError::InvalidConfiguration { + source: config.source_name, + reason: "constant IV length does not match the declared constant IV size", + }); + } + if config.constant_iv.is_empty() { + return Err(ResolveSampleEncryptionError::InvalidConfiguration { + source: config.source_name, + reason: "protected samples with no per-sample IV size must declare a constant IV", + }); + } + + Ok(ResolvedEncryptionDefaults { + source: config.source, + is_protected, + crypt_byte_block: config.crypt_byte_block, + skip_byte_block: config.skip_byte_block, + per_sample_iv_size: None, + constant_iv: Some(config.constant_iv), + kid: config.kid, + }) +} + +fn validate_sample_initialization_vector( + sample_index: u32, + sample: &SencSample, + defaults: &ResolvedEncryptionDefaults<'_>, +) -> Result<(), ResolveSampleEncryptionError> { + let actual = sample.initialization_vector.len(); + match defaults.per_sample_iv_size { + Some(expected) if actual == usize::from(expected) => Ok(()), + Some(expected) => Err( + ResolveSampleEncryptionError::SampleInitializationVectorSizeMismatch { + sample_index, + expected: usize::from(expected), + actual, + }, + ), + None if actual == 0 => Ok(()), + None => Err( + ResolveSampleEncryptionError::UnexpectedSampleInitializationVector { + sample_index, + actual, + }, + ), + } +} + +fn resolved_sample_info_size( + sample_index: u32, + sample: &SencSample, + uses_subsample_encryption: bool, +) -> Result { + let mut size = u32::try_from(sample.initialization_vector.len()) + .map_err(|_| ResolveSampleEncryptionError::SampleInfoSizeOverflow { sample_index })?; + + if uses_subsample_encryption { + size = size + .checked_add(2) + .ok_or(ResolveSampleEncryptionError::SampleInfoSizeOverflow { sample_index })?; + let subsample_count = u32::try_from(sample.subsamples.len()) + .map_err(|_| ResolveSampleEncryptionError::SampleInfoSizeOverflow { sample_index })?; + let subsample_bytes = subsample_count + .checked_mul(6) + .ok_or(ResolveSampleEncryptionError::SampleInfoSizeOverflow { sample_index })?; + size = size + .checked_add(subsample_bytes) + .ok_or(ResolveSampleEncryptionError::SampleInfoSizeOverflow { sample_index })?; + } + + Ok(size) +} + +fn validate_saiz_sample_info_size( + saiz: Option<&Saiz>, + sample_index: u32, + expected: u32, +) -> Result<(), ResolveSampleEncryptionError> { + let Some(saiz) = saiz else { + return Ok(()); + }; + + let actual = if saiz.default_sample_info_size != 0 { + u32::from(saiz.default_sample_info_size) + } else { + let offset = usize::try_from(sample_index - 1).unwrap_or(usize::MAX); + let Some(size) = saiz.sample_info_size.get(offset) else { + return Err(ResolveSampleEncryptionError::InvalidSaizLayout { + reason: "per-sample size table is shorter than the declared sample count", + }); + }; + u32::from(*size) + }; + + if actual != expected { + return Err(ResolveSampleEncryptionError::SaizSampleInfoSizeMismatch { + sample_index, + expected, + actual, + }); + } + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 301eddf..b924fb1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,8 @@ pub mod boxes; pub mod cli; /// Descriptor-driven binary codec primitives. pub mod codec; +/// Resolved common-encryption metadata helpers built on typed box models. +pub mod encryption; /// Path-based box extraction helpers, including typed convenience reads. pub mod extract; /// Four-character box identifier support. @@ -18,6 +20,8 @@ pub mod header; pub mod probe; /// Path-based typed payload rewrite helpers built on the writer layer. pub mod rewrite; +/// Fragmented top-level `sidx` analysis, planning, and rewrite helpers. +pub mod sidx; /// Stable field-order string rendering for descriptor-backed boxes. pub mod stringify; /// Depth-first structure walking with path tracking and lazy payload access. diff --git a/src/probe.rs b/src/probe.rs index 69edaae..0a79237 100644 --- a/src/probe.rs +++ b/src/probe.rs @@ -11,9 +11,10 @@ use crate::bitio::BitReader; use crate::boxes::av1::AV1CodecConfiguration; use crate::boxes::etsi_ts_102_366::Dac3; use crate::boxes::iso14496_12::{ - AVCDecoderConfiguration, AudioSampleEntry, Btrt, Co64, Colr, Ctts, Fiel, - HEVCDecoderConfiguration, Mvhd, Pasp, Stco, Stsc, Stsz, Stts, TextSubtitleSampleEntry, Tfdt, - Tfhd, Tkhd, Trun, VisualSampleEntry, XMLSubtitleSampleEntry, + AVCDecoderConfiguration, AudioSampleEntry, Btrt, Clap, Co64, CoLL, Colr, Ctts, Elng, + EventMessageSampleEntry, Fiel, HEVCDecoderConfiguration, Mvhd, Pasp, SmDm, Stco, Stsc, Stsz, + Stts, TextSubtitleSampleEntry, Tfdt, Tfhd, Tkhd, Trun, VisualSampleEntry, + XMLSubtitleSampleEntry, }; use crate::boxes::iso14496_12::{Frma, Hdlr, Schm}; use crate::boxes::iso14496_14::Esds; @@ -34,6 +35,7 @@ const MOOF: FourCc = FourCc::from_bytes(*b"moof"); const MDAT: FourCc = FourCc::from_bytes(*b"mdat"); const TKHD: FourCc = FourCc::from_bytes(*b"tkhd"); const EDTS: FourCc = FourCc::from_bytes(*b"edts"); +const ELNG: FourCc = FourCc::from_bytes(*b"elng"); const ELST: FourCc = FourCc::from_bytes(*b"elst"); const MDIA: FourCc = FourCc::from_bytes(*b"mdia"); const HDLR: FourCc = FourCc::from_bytes(*b"hdlr"); @@ -46,6 +48,11 @@ const AVCC: FourCc = FourCc::from_bytes(*b"avcC"); const HEV1: FourCc = FourCc::from_bytes(*b"hev1"); const HVC1: FourCc = FourCc::from_bytes(*b"hvc1"); const HVCC: FourCc = FourCc::from_bytes(*b"hvcC"); +const VVC1: FourCc = FourCc::from_bytes(*b"vvc1"); +const VVI1: FourCc = FourCc::from_bytes(*b"vvi1"); +const VVCC: FourCc = FourCc::from_bytes(*b"vvcC"); +const AVS3: FourCc = FourCc::from_bytes(*b"avs3"); +const AV3C: FourCc = FourCc::from_bytes(*b"av3c"); const AV01: FourCc = FourCc::from_bytes(*b"av01"); const AV1C: FourCc = FourCc::from_bytes(*b"av1C"); const VP08: FourCc = FourCc::from_bytes(*b"vp08"); @@ -53,14 +60,28 @@ const VP09: FourCc = FourCc::from_bytes(*b"vp09"); const VPCC: FourCc = FourCc::from_bytes(*b"vpcC"); const ENCV: FourCc = FourCc::from_bytes(*b"encv"); const BTRT: FourCc = FourCc::from_bytes(*b"btrt"); +const CLAP: FourCc = FourCc::from_bytes(*b"clap"); +const COLL: FourCc = FourCc::from_bytes(*b"CoLL"); const COLR: FourCc = FourCc::from_bytes(*b"colr"); const FIEL: FourCc = FourCc::from_bytes(*b"fiel"); const PASP: FourCc = FourCc::from_bytes(*b"pasp"); +const SMDM: FourCc = FourCc::from_bytes(*b"SmDm"); const MP4A: FourCc = FourCc::from_bytes(*b"mp4a"); const OPUS: FourCc = FourCc::from_bytes(*b"Opus"); const DOPS: FourCc = FourCc::from_bytes(*b"dOps"); const AC_3: FourCc = FourCc::from_bytes(*b"ac-3"); +const EC_3: FourCc = FourCc::from_bytes(*b"ec-3"); const DAC3: FourCc = FourCc::from_bytes(*b"dac3"); +const DEC3: FourCc = FourCc::from_bytes(*b"dec3"); +const AC_4: FourCc = FourCc::from_bytes(*b"ac-4"); +const DAC4: FourCc = FourCc::from_bytes(*b"dac4"); +const FLAC: FourCc = FourCc::from_bytes(*b"fLaC"); +const DFLA: FourCc = FourCc::from_bytes(*b"dfLa"); +const MHA1: FourCc = FourCc::from_bytes(*b"mha1"); +const MHA2: FourCc = FourCc::from_bytes(*b"mha2"); +const MHM1: FourCc = FourCc::from_bytes(*b"mhm1"); +const MHM2: FourCc = FourCc::from_bytes(*b"mhm2"); +const MHAC: FourCc = FourCc::from_bytes(*b"mhaC"); const IPCM: FourCc = FourCc::from_bytes(*b"ipcm"); const FPCM: FourCc = FourCc::from_bytes(*b"fpcm"); const PCMC: FourCc = FourCc::from_bytes(*b"pcmC"); @@ -70,6 +91,7 @@ const ENCA: FourCc = FourCc::from_bytes(*b"enca"); const STPP: FourCc = FourCc::from_bytes(*b"stpp"); const SBTT: FourCc = FourCc::from_bytes(*b"sbtt"); const WVTT: FourCc = FourCc::from_bytes(*b"wvtt"); +const EVTE: FourCc = FourCc::from_bytes(*b"evte"); const VTTC_CONFIG: FourCc = FourCc::from_bytes(*b"vttC"); const VLAB: FourCc = FourCc::from_bytes(*b"vlab"); const COLR_NCLX: FourCc = FourCc::from_bytes(*b"nclx"); @@ -237,7 +259,7 @@ pub struct DetailedTrackInfo { pub codec_family: TrackCodecFamily, /// Handler type from `hdlr` when present. pub handler_type: Option, - /// ISO-639-2 language code derived from `mdhd` when present. + /// Language tag from `elng` when present, otherwise the ISO-639-2 code derived from `mdhd`. pub language: Option, /// Sample-entry box type found under `stsd`, including encrypted wrappers such as `encv`. pub sample_entry_type: Option, @@ -339,6 +361,43 @@ impl Default for MediaCharacteristicsProbeInfo { } } +/// Additive detailed probe summary that extends [`MediaCharacteristicsProbeInfo`] with extra +/// typed visual sample-entry metadata. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ExtendedMediaCharacteristicsProbeInfo { + /// Major brand from the root `ftyp` box. + pub major_brand: FourCc, + /// Minor version from the root `ftyp` box. + pub minor_version: u32, + /// Compatible brands listed by the root `ftyp` box. + pub compatible_brands: Vec, + /// Whether the `moov` box appears before the first `mdat`. + pub fast_start: bool, + /// Movie timescale from `mvhd`. + pub timescale: u32, + /// Movie duration from `mvhd`. + pub duration: u64, + /// Per-track detailed summaries extracted from `trak` boxes. + pub tracks: Vec, + /// Fragment summaries extracted from `moof` boxes. + pub segments: Vec, +} + +impl Default for ExtendedMediaCharacteristicsProbeInfo { + fn default() -> Self { + Self { + major_brand: FourCc::ANY, + minor_version: 0, + compatible_brands: Vec::new(), + fast_start: false, + timescale: 0, + duration: 0, + tracks: Vec::new(), + segments: Vec::new(), + } + } +} + /// Additive per-track summary that extends [`DetailedTrackInfo`] with parsed codec and media /// characteristics. #[derive(Clone, Debug, Default, PartialEq, Eq)] @@ -351,8 +410,23 @@ pub struct MediaCharacteristicsTrackInfo { pub media_characteristics: TrackMediaCharacteristics, } -/// Media characteristics derived from sample-entry side boxes such as `btrt`, `colr`, `pasp`, -/// and `fiel`. +/// Additive per-track summary that extends [`MediaCharacteristicsTrackInfo`] with extra typed +/// visual sample-entry metadata. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ExtendedMediaCharacteristicsTrackInfo { + /// Backwards-compatible detailed track summary preserved from [`DetailedTrackInfo`]. + pub summary: DetailedTrackInfo, + /// Parsed codec-specific configuration when it is available for the track family. + pub codec_details: TrackCodecDetails, + /// Sample-entry media characteristics already parsed by the crate. + pub media_characteristics: TrackMediaCharacteristics, + /// Additional typed visual sample-entry metadata from boxes such as `clap`, `CoLL`, and + /// `SmDm`. + pub visual_metadata: TrackVisualMetadata, +} + +/// Media characteristics derived from stable sample-entry side boxes such as `btrt`, `colr`, +/// `pasp`, and `fiel`. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct TrackMediaCharacteristics { @@ -366,6 +440,19 @@ pub struct TrackMediaCharacteristics { pub field_order: Option, } +/// Additional typed visual sample-entry metadata parsed from boxes that were added after the +/// original [`TrackMediaCharacteristics`] shape was stabilized. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TrackVisualMetadata { + /// Clean-aperture declaration from `clap` when present. + pub clean_aperture: Option, + /// Content-light-level metadata from `CoLL` when present. + pub content_light_level: Option, + /// Mastering-display metadata from `SmDm` when present. + pub mastering_display: Option, +} + /// Declared buffering and bitrate values parsed from `btrt`. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, Default, PartialEq, Eq)] @@ -412,6 +499,38 @@ impl Default for ColorInfo { } } +/// Clean-aperture values parsed from `clap`. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct CleanApertureInfo { + /// Clean-aperture width numerator. + pub width_numerator: u32, + /// Clean-aperture width denominator. + pub width_denominator: u32, + /// Clean-aperture height numerator. + pub height_numerator: u32, + /// Clean-aperture height denominator. + pub height_denominator: u32, + /// Horizontal offset numerator. + pub horizontal_offset_numerator: u32, + /// Horizontal offset denominator. + pub horizontal_offset_denominator: u32, + /// Vertical offset numerator. + pub vertical_offset_numerator: u32, + /// Vertical offset denominator. + pub vertical_offset_denominator: u32, +} + +/// Content-light-level metadata parsed from `CoLL`. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct ContentLightLevelInfo { + /// Maximum content light level. + pub max_cll: u16, + /// Maximum frame-average light level. + pub max_fall: u16, +} + /// Declared pixel aspect ratio parsed from `pasp`. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, Default, PartialEq, Eq)] @@ -434,6 +553,32 @@ pub struct FieldOrderInfo { pub interlaced: bool, } +/// Mastering-display metadata parsed from `SmDm`. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct MasteringDisplayInfo { + /// Red-primary chromaticity X coordinate. + pub primary_r_chromaticity_x: u16, + /// Red-primary chromaticity Y coordinate. + pub primary_r_chromaticity_y: u16, + /// Green-primary chromaticity X coordinate. + pub primary_g_chromaticity_x: u16, + /// Green-primary chromaticity Y coordinate. + pub primary_g_chromaticity_y: u16, + /// Blue-primary chromaticity X coordinate. + pub primary_b_chromaticity_x: u16, + /// Blue-primary chromaticity Y coordinate. + pub primary_b_chromaticity_y: u16, + /// White-point chromaticity X coordinate. + pub white_point_chromaticity_x: u16, + /// White-point chromaticity Y coordinate. + pub white_point_chromaticity_y: u16, + /// Maximum mastering-display luminance. + pub luminance_max: u32, + /// Minimum mastering-display luminance. + pub luminance_min: u32, +} + /// Parsed codec-specific configuration for one recognized track family. #[cfg_attr( feature = "serde", @@ -695,15 +840,19 @@ struct TrackCodecConfigRefs<'a> { #[derive(Default)] struct TrackMediaCharacteristicRefs<'a> { btrt: Option<&'a Btrt>, + clap: Option<&'a Clap>, + coll: Option<&'a CoLL>, colr: Option<&'a Colr>, pasp: Option<&'a Pasp>, fiel: Option<&'a Fiel>, + smdm: Option<&'a SmDm>, } struct ParsedRichTrackInfo { summary: DetailedTrackInfo, codec_details: TrackCodecDetails, media_characteristics: TrackMediaCharacteristics, + visual_metadata: TrackVisualMetadata, } /// Coarse codec classification used by the probe surface. @@ -750,6 +899,38 @@ pub enum TrackCodecFamily { WebVtt, } +/// Returns the additive codec-family label used by detailed reporting. +/// +/// The stable [`TrackCodecFamily`] enum intentionally keeps its current shape. Newer sample-entry +/// families that do not yet warrant an enum expansion still surface here through their +/// sample-entry or protected original-format box type. +pub fn normalized_codec_family_name( + codec_family: TrackCodecFamily, + sample_entry_type: Option, + original_format: Option, +) -> &'static str { + match codec_family { + TrackCodecFamily::Unknown => match original_format.or(sample_entry_type) { + Some(AVS3) => "avs3", + Some(FLAC) => "flac", + Some(MHA1 | MHA2 | MHM1 | MHM2) => "mpeg_h", + _ => "unknown", + }, + TrackCodecFamily::Avc => "avc", + TrackCodecFamily::Hevc => "hevc", + TrackCodecFamily::Av1 => "av1", + TrackCodecFamily::Vp8 => "vp8", + TrackCodecFamily::Vp9 => "vp9", + TrackCodecFamily::Mp4Audio => "mp4_audio", + TrackCodecFamily::Opus => "opus", + TrackCodecFamily::Ac3 => "ac3", + TrackCodecFamily::Pcm => "pcm", + TrackCodecFamily::XmlSubtitle => "xml_subtitle", + TrackCodecFamily::TextSubtitle => "text_subtitle", + TrackCodecFamily::WebVtt => "webvtt", + } +} + /// Protection-scheme summary derived from `schm`. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ProtectionSchemeInfo { @@ -1015,6 +1196,68 @@ where Ok(summary) } +/// Probes a file and returns an additive summary with parsed codec, media characteristics, and +/// extra typed visual sample-entry metadata. +pub fn probe_extended_media_characteristics( + reader: &mut R, +) -> Result +where + R: Read + Seek, +{ + probe_extended_media_characteristics_with_options(reader, ProbeOptions::default()) +} + +/// Probes a file with additive expansion controls and returns the extended +/// media-characteristics summary. +pub fn probe_extended_media_characteristics_with_options( + reader: &mut R, + options: ProbeOptions, +) -> Result +where + R: Read + Seek, +{ + let paths = root_probe_box_paths(options); + let infos = extract_boxes(reader, None, &paths)?; + + let mut summary = ExtendedMediaCharacteristicsProbeInfo::default(); + let mut mdat_appeared = false; + + for info in infos { + match info.box_type() { + FTYP => { + let ftyp = read_payload_as::<_, crate::boxes::iso14496_12::Ftyp>(reader, &info)?; + summary.major_brand = ftyp.major_brand; + summary.minor_version = ftyp.minor_version; + summary.compatible_brands = ftyp.compatible_brands; + } + MOOV => { + summary.fast_start = !mdat_appeared; + } + MVHD => { + let mvhd = read_payload_as::<_, Mvhd>(reader, &info)?; + summary.timescale = mvhd.timescale; + summary.duration = mvhd.duration(); + } + TRAK => { + summary + .tracks + .push(probe_trak_extended_media_characteristics( + reader, &info, options, + )?); + } + MOOF if options.include_segments => { + summary.segments.push(probe_moof(reader, &info)?); + } + MDAT => { + mdat_appeared = true; + } + _ => {} + } + } + + Ok(summary) +} + /// Probes an in-memory MP4 byte slice and returns the coarse movie, track, and fragment /// summary. /// @@ -1090,6 +1333,26 @@ pub fn probe_media_characteristics_bytes_with_options( probe_media_characteristics_with_options(&mut reader, options) } +/// Probes an in-memory MP4 byte slice and returns the extended media-characteristics summary. +/// +/// This is equivalent to calling [`probe_extended_media_characteristics`] with `Cursor<&[u8]>`. +pub fn probe_extended_media_characteristics_bytes( + input: &[u8], +) -> Result { + let mut reader = Cursor::new(input); + probe_extended_media_characteristics(&mut reader) +} + +/// Probes an in-memory MP4 byte slice with additive expansion controls and returns the extended +/// media-characteristics summary. +pub fn probe_extended_media_characteristics_bytes_with_options( + input: &[u8], + options: ProbeOptions, +) -> Result { + let mut reader = Cursor::new(input); + probe_extended_media_characteristics_with_options(&mut reader, options) +} + /// Legacy fragmented-file probe entry point that currently aliases [`probe`]. pub fn probe_fra(reader: &mut R) -> Result where @@ -1422,12 +1685,15 @@ fn root_probe_box_paths(options: ProbeOptions) -> Vec { } fn track_probe_box_paths(options: ProbeOptions) -> Vec { - let visual_sample_entries = [AVC1, HEV1, HVC1, AV01, VP08, VP09, ENCV]; - let audio_sample_entries = [MP4A, OPUS, AC_3, IPCM, FPCM, ENCA]; + let visual_sample_entries = [AVC1, HEV1, HVC1, VVC1, VVI1, AVS3, AV01, VP08, VP09, ENCV]; + let audio_sample_entries = [ + MP4A, OPUS, AC_3, EC_3, AC_4, FLAC, MHA1, MHA2, MHM1, MHM2, IPCM, FPCM, ENCA, + ]; let mut paths = vec![ BoxPath::from([TKHD]), BoxPath::from([EDTS, ELST]), BoxPath::from([MDIA, MDHD]), + BoxPath::from([MDIA, ELNG]), BoxPath::from([MDIA, HDLR]), BoxPath::from([MDIA, MINF, STBL, STSD, AVC1]), BoxPath::from([MDIA, MINF, STBL, STSD, AVC1, AVCC]), @@ -1435,6 +1701,12 @@ fn track_probe_box_paths(options: ProbeOptions) -> Vec { BoxPath::from([MDIA, MINF, STBL, STSD, HEV1, HVCC]), BoxPath::from([MDIA, MINF, STBL, STSD, HVC1]), BoxPath::from([MDIA, MINF, STBL, STSD, HVC1, HVCC]), + BoxPath::from([MDIA, MINF, STBL, STSD, VVC1]), + BoxPath::from([MDIA, MINF, STBL, STSD, VVC1, VVCC]), + BoxPath::from([MDIA, MINF, STBL, STSD, VVI1]), + BoxPath::from([MDIA, MINF, STBL, STSD, VVI1, VVCC]), + BoxPath::from([MDIA, MINF, STBL, STSD, AVS3]), + BoxPath::from([MDIA, MINF, STBL, STSD, AVS3, AV3C]), BoxPath::from([MDIA, MINF, STBL, STSD, AV01]), BoxPath::from([MDIA, MINF, STBL, STSD, AV01, AV1C]), BoxPath::from([MDIA, MINF, STBL, STSD, VP08]), @@ -1444,6 +1716,8 @@ fn track_probe_box_paths(options: ProbeOptions) -> Vec { BoxPath::from([MDIA, MINF, STBL, STSD, ENCV]), BoxPath::from([MDIA, MINF, STBL, STSD, ENCV, AVCC]), BoxPath::from([MDIA, MINF, STBL, STSD, ENCV, HVCC]), + BoxPath::from([MDIA, MINF, STBL, STSD, ENCV, VVCC]), + BoxPath::from([MDIA, MINF, STBL, STSD, ENCV, AV3C]), BoxPath::from([MDIA, MINF, STBL, STSD, ENCV, AV1C]), BoxPath::from([MDIA, MINF, STBL, STSD, ENCV, VPCC]), BoxPath::from([MDIA, MINF, STBL, STSD, ENCV, SINF, FRMA]), @@ -1455,6 +1729,20 @@ fn track_probe_box_paths(options: ProbeOptions) -> Vec { BoxPath::from([MDIA, MINF, STBL, STSD, OPUS, DOPS]), BoxPath::from([MDIA, MINF, STBL, STSD, AC_3]), BoxPath::from([MDIA, MINF, STBL, STSD, AC_3, DAC3]), + BoxPath::from([MDIA, MINF, STBL, STSD, EC_3]), + BoxPath::from([MDIA, MINF, STBL, STSD, EC_3, DEC3]), + BoxPath::from([MDIA, MINF, STBL, STSD, AC_4]), + BoxPath::from([MDIA, MINF, STBL, STSD, AC_4, DAC4]), + BoxPath::from([MDIA, MINF, STBL, STSD, FLAC]), + BoxPath::from([MDIA, MINF, STBL, STSD, FLAC, DFLA]), + BoxPath::from([MDIA, MINF, STBL, STSD, MHA1]), + BoxPath::from([MDIA, MINF, STBL, STSD, MHA1, MHAC]), + BoxPath::from([MDIA, MINF, STBL, STSD, MHA2]), + BoxPath::from([MDIA, MINF, STBL, STSD, MHA2, MHAC]), + BoxPath::from([MDIA, MINF, STBL, STSD, MHM1]), + BoxPath::from([MDIA, MINF, STBL, STSD, MHM1, MHAC]), + BoxPath::from([MDIA, MINF, STBL, STSD, MHM2]), + BoxPath::from([MDIA, MINF, STBL, STSD, MHM2, MHAC]), BoxPath::from([MDIA, MINF, STBL, STSD, IPCM]), BoxPath::from([MDIA, MINF, STBL, STSD, IPCM, PCMC]), BoxPath::from([MDIA, MINF, STBL, STSD, FPCM]), @@ -1464,12 +1752,18 @@ fn track_probe_box_paths(options: ProbeOptions) -> Vec { BoxPath::from([MDIA, MINF, STBL, STSD, ENCA, WAVE, ESDS]), BoxPath::from([MDIA, MINF, STBL, STSD, ENCA, DOPS]), BoxPath::from([MDIA, MINF, STBL, STSD, ENCA, DAC3]), + BoxPath::from([MDIA, MINF, STBL, STSD, ENCA, DEC3]), + BoxPath::from([MDIA, MINF, STBL, STSD, ENCA, DAC4]), + BoxPath::from([MDIA, MINF, STBL, STSD, ENCA, DFLA]), + BoxPath::from([MDIA, MINF, STBL, STSD, ENCA, MHAC]), BoxPath::from([MDIA, MINF, STBL, STSD, ENCA, PCMC]), BoxPath::from([MDIA, MINF, STBL, STSD, ENCA, SINF, FRMA]), BoxPath::from([MDIA, MINF, STBL, STSD, ENCA, SINF, SCHM]), BoxPath::from([MDIA, MINF, STBL, STSD, STPP]), BoxPath::from([MDIA, MINF, STBL, STSD, SBTT]), BoxPath::from([MDIA, MINF, STBL, STSD, WVTT]), + BoxPath::from([MDIA, MINF, STBL, STSD, EVTE]), + BoxPath::from([MDIA, MINF, STBL, STSD, EVTE, BTRT]), BoxPath::from([MDIA, MINF, STBL, STSD, WVTT, VTTC_CONFIG]), BoxPath::from([MDIA, MINF, STBL, STSD, WVTT, VLAB]), ]; @@ -1477,9 +1771,12 @@ fn track_probe_box_paths(options: ProbeOptions) -> Vec { for sample_entry in visual_sample_entries { paths.extend([ BoxPath::from([MDIA, MINF, STBL, STSD, sample_entry, BTRT]), + BoxPath::from([MDIA, MINF, STBL, STSD, sample_entry, CLAP]), + BoxPath::from([MDIA, MINF, STBL, STSD, sample_entry, COLL]), BoxPath::from([MDIA, MINF, STBL, STSD, sample_entry, COLR]), BoxPath::from([MDIA, MINF, STBL, STSD, sample_entry, PASP]), BoxPath::from([MDIA, MINF, STBL, STSD, sample_entry, FIEL]), + BoxPath::from([MDIA, MINF, STBL, STSD, sample_entry, SMDM]), ]); } @@ -1537,6 +1834,23 @@ where }) } +fn probe_trak_extended_media_characteristics( + reader: &mut R, + parent: &BoxInfo, + options: ProbeOptions, +) -> Result +where + R: Read + Seek, +{ + let track = probe_trak_rich_details(reader, parent, options)?; + Ok(ExtendedMediaCharacteristicsTrackInfo { + summary: track.summary, + codec_details: track.codec_details, + media_characteristics: track.media_characteristics, + visual_metadata: track.visual_metadata, + }) +} + fn probe_trak_rich_details( reader: &mut R, parent: &BoxInfo, @@ -1551,6 +1865,7 @@ where let mut track = DetailedTrackInfo::default(); let mut tkhd = None; let mut mdhd = None; + let mut elng = None; let mut visual_sample_entry = None; let mut avcc = None; let mut hvcc = None; @@ -1566,9 +1881,12 @@ where let mut webvtt_configuration = None; let mut webvtt_source_label = None; let mut btrt = None; + let mut clap = None; + let mut coll = None; let mut colr = None; let mut pasp = None; let mut fiel = None; + let mut smdm = None; let mut original_format = None; let mut stco = None; let mut co64 = None; @@ -1600,9 +1918,16 @@ where let payload = downcast_clone::(&extracted)?; track.summary.timescale = payload.timescale; track.summary.duration = payload.duration(); - track.language = Some(decode_language(payload.language)); + if elng.is_none() { + track.language = Some(decode_language(payload.language)); + } mdhd = Some(payload); } + ELNG => { + let payload = downcast_clone::(&extracted)?; + track.language = Some(payload.extended_language.clone()); + elng = Some(payload); + } HDLR => { let payload = downcast_clone::(&extracted)?; track.handler_type = Some(payload.handler_type); @@ -1629,6 +1954,18 @@ where track.sample_entry_type = Some(HVC1); visual_sample_entry = Some(downcast_clone::(&extracted)?); } + VVC1 => { + track.sample_entry_type = Some(VVC1); + visual_sample_entry = Some(downcast_clone::(&extracted)?); + } + VVI1 => { + track.sample_entry_type = Some(VVI1); + visual_sample_entry = Some(downcast_clone::(&extracted)?); + } + AVS3 => { + track.sample_entry_type = Some(AVS3); + visual_sample_entry = Some(downcast_clone::(&extracted)?); + } AV01 => { track.codec_family = TrackCodecFamily::Av1; track.sample_entry_type = Some(AV01); @@ -1681,6 +2018,34 @@ where track.sample_entry_type = Some(AC_3); audio_sample_entry = Some(downcast_clone::(&extracted)?); } + EC_3 => { + track.sample_entry_type = Some(EC_3); + audio_sample_entry = Some(downcast_clone::(&extracted)?); + } + AC_4 => { + track.sample_entry_type = Some(AC_4); + audio_sample_entry = Some(downcast_clone::(&extracted)?); + } + FLAC => { + track.sample_entry_type = Some(FLAC); + audio_sample_entry = Some(downcast_clone::(&extracted)?); + } + MHA1 => { + track.sample_entry_type = Some(MHA1); + audio_sample_entry = Some(downcast_clone::(&extracted)?); + } + MHA2 => { + track.sample_entry_type = Some(MHA2); + audio_sample_entry = Some(downcast_clone::(&extracted)?); + } + MHM1 => { + track.sample_entry_type = Some(MHM1); + audio_sample_entry = Some(downcast_clone::(&extracted)?); + } + MHM2 => { + track.sample_entry_type = Some(MHM2); + audio_sample_entry = Some(downcast_clone::(&extracted)?); + } DAC3 => { dac3 = Some(downcast_clone::(&extracted)?); } @@ -1709,6 +2074,10 @@ where text_subtitle_sample_entry = Some(downcast_clone::(&extracted)?); } + EVTE => { + track.sample_entry_type = Some(EVTE); + let _ = downcast_clone::(&extracted)?; + } WVTT => { track.codec_family = TrackCodecFamily::WebVtt; track.sample_entry_type = Some(WVTT); @@ -1722,6 +2091,12 @@ where BTRT => { btrt = Some(downcast_clone::(&extracted)?); } + CLAP => { + clap = Some(downcast_clone::(&extracted)?); + } + COLL => { + coll = Some(downcast_clone::(&extracted)?); + } COLR => { colr = Some(downcast_clone::(&extracted)?); } @@ -1731,6 +2106,9 @@ where FIEL => { fiel = Some(downcast_clone::(&extracted)?); } + SMDM => { + smdm = Some(downcast_clone::(&extracted)?); + } ESDS => { esds = Some(downcast_clone::(&extracted)?); } @@ -1908,17 +2286,23 @@ where webvtt_source_label: webvtt_source_label.as_ref(), }, ); - let media_characteristics = build_track_media_characteristics(&TrackMediaCharacteristicRefs { + let media_refs = TrackMediaCharacteristicRefs { btrt: btrt.as_ref(), + clap: clap.as_ref(), + coll: coll.as_ref(), colr: colr.as_ref(), pasp: pasp.as_ref(), fiel: fiel.as_ref(), - }); + smdm: smdm.as_ref(), + }; + let media_characteristics = build_track_media_characteristics(&media_refs); + let visual_metadata = build_track_visual_metadata(&media_refs); Ok(ParsedRichTrackInfo { summary: track, codec_details, media_characteristics, + visual_metadata, }) } @@ -2133,6 +2517,30 @@ fn build_track_media_characteristics( } } +fn build_track_visual_metadata(refs: &TrackMediaCharacteristicRefs<'_>) -> TrackVisualMetadata { + TrackVisualMetadata { + clean_aperture: refs.clap.map(track_clean_aperture_info), + content_light_level: refs.coll.map(|value| ContentLightLevelInfo { + max_cll: value.max_cll, + max_fall: value.max_fall, + }), + mastering_display: refs.smdm.map(track_mastering_display_info), + } +} + +fn track_clean_aperture_info(value: &Clap) -> CleanApertureInfo { + CleanApertureInfo { + width_numerator: value.clean_aperture_width_n, + width_denominator: value.clean_aperture_width_d, + height_numerator: value.clean_aperture_height_n, + height_denominator: value.clean_aperture_height_d, + horizontal_offset_numerator: value.horiz_off_n, + horizontal_offset_denominator: value.horiz_off_d, + vertical_offset_numerator: value.vert_off_n, + vertical_offset_denominator: value.vert_off_d, + } +} + fn track_color_info(value: &Colr) -> ColorInfo { let is_nclx = value.colour_type == COLR_NCLX; let stores_profile = matches!(value.colour_type, COLR_RICC | COLR_PROF); @@ -2147,6 +2555,21 @@ fn track_color_info(value: &Colr) -> ColorInfo { } } +fn track_mastering_display_info(value: &SmDm) -> MasteringDisplayInfo { + MasteringDisplayInfo { + primary_r_chromaticity_x: value.primary_r_chromaticity_x, + primary_r_chromaticity_y: value.primary_r_chromaticity_y, + primary_g_chromaticity_x: value.primary_g_chromaticity_x, + primary_g_chromaticity_y: value.primary_g_chromaticity_y, + primary_b_chromaticity_x: value.primary_b_chromaticity_x, + primary_b_chromaticity_y: value.primary_b_chromaticity_y, + white_point_chromaticity_x: value.white_point_chromaticity_x, + white_point_chromaticity_y: value.white_point_chromaticity_y, + luminance_max: value.luminance_max, + luminance_min: value.luminance_min, + } +} + fn track_field_order_info(value: &Fiel) -> FieldOrderInfo { FieldOrderInfo { field_count: value.field_count, diff --git a/src/rewrite.rs b/src/rewrite.rs index 9d96b52..51e4a74 100644 --- a/src/rewrite.rs +++ b/src/rewrite.rs @@ -10,7 +10,9 @@ use std::fmt; use std::io::{self, Cursor, Read, Seek, SeekFrom, Write}; use crate::FourCc; -use crate::boxes::iso14496_12::Ftyp; +use crate::boxes::iso14496_12::{ + Ftyp, VisualSampleEntry, split_box_children_with_optional_trailing_bytes, +}; use crate::boxes::metadata::Keys; use crate::boxes::{BoxLookupContext, BoxRegistry, default_registry}; use crate::codec::{CodecBox, CodecError, marshal_dyn, unmarshal, unmarshal_any_with_context}; @@ -313,10 +315,15 @@ where })?; let children_offset = info.offset() + info.header_size() + payload_read; - let children_size = info - .offset() - .saturating_add(info.size()) - .saturating_sub(children_offset); + let (children_size, trailing_bytes) = if payload.as_any().is::() { + visual_sample_entry_children_layout( + reader, + children_offset, + payload_size.saturating_sub(payload_read), + )? + } else { + (payload_size.saturating_sub(payload_read), Vec::new()) + }; reader.seek(SeekFrom::Start(children_offset))?; rewrite_sequence::( reader, @@ -329,6 +336,9 @@ where info.lookup_context().enter(info.box_type()), ), )?; + if !trailing_bytes.is_empty() { + writer.write_all(&trailing_bytes)?; + } info.seek_to_end(reader)?; writer.end_box()?; Ok(()) @@ -360,6 +370,35 @@ where Ok(()) } +fn visual_sample_entry_children_layout( + reader: &mut R, + extension_offset: u64, + extension_size: u64, +) -> Result<(u64, Vec), RewriteError> +where + R: Read + Seek, +{ + let checkpoint = reader.stream_position()?; + reader.seek(SeekFrom::Start(extension_offset))?; + let bytes = read_extension_bytes(reader, extension_size)?; + reader.seek(SeekFrom::Start(checkpoint))?; + + let child_len = split_box_children_with_optional_trailing_bytes(&bytes); + Ok((child_len as u64, bytes[child_len..].to_vec())) +} + +fn read_extension_bytes(reader: &mut R, extension_size: u64) -> Result, RewriteError> +where + R: Read, +{ + let extension_len = usize::try_from(extension_size).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "payload extension is too large") + })?; + let mut bytes = vec![0; extension_len]; + reader.read_exact(&mut bytes)?; + Ok(bytes) +} + fn decode_box(reader: &mut R, info: &BoxInfo) -> Result where R: Read + Seek, diff --git a/src/sidx.rs b/src/sidx.rs new file mode 100644 index 0000000..6e6e44d --- /dev/null +++ b/src/sidx.rs @@ -0,0 +1,1744 @@ +//! Top-level `sidx` helpers for fragmented MP4 files. +//! +//! These helpers keep `mp4forge`'s existing box extraction and rewrite surfaces unchanged while +//! exposing the file-level defaults needed to analyze fragmented files, build typed update plans, +//! and apply those plans without disturbing unrelated bytes. + +use std::error::Error; +use std::fmt; +use std::io::{self, Cursor, Read, Seek, SeekFrom, Write}; + +use crate::FourCc; +use crate::boxes::iso14496_12::{ + Mdhd, Sidx, SidxReference, TFHD_DEFAULT_SAMPLE_DURATION_PRESENT, + TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT, TRUN_SAMPLE_DURATION_PRESENT, Tfdt, Tfhd, Tkhd, + Trex, Trun, +}; +use crate::codec::{CodecBox, CodecError, ImmutableBox, MutableBox, marshal, unmarshal}; +use crate::extract::{ExtractError, extract_box_as, extract_boxes}; +use crate::header::{BoxInfo, HeaderError, LARGE_HEADER_SIZE, SMALL_HEADER_SIZE}; +use crate::walk::BoxPath; + +const MOOV: FourCc = FourCc::from_bytes(*b"moov"); +const MOOF: FourCc = FourCc::from_bytes(*b"moof"); +const MVEX: FourCc = FourCc::from_bytes(*b"mvex"); +const MDAT: FourCc = FourCc::from_bytes(*b"mdat"); +const STYP: FourCc = FourCc::from_bytes(*b"styp"); +const SIDX: FourCc = FourCc::from_bytes(*b"sidx"); +const TRAK: FourCc = FourCc::from_bytes(*b"trak"); +const TREX: FourCc = FourCc::from_bytes(*b"trex"); +const TKHD: FourCc = FourCc::from_bytes(*b"tkhd"); +const MDIA: FourCc = FourCc::from_bytes(*b"mdia"); +const MDHD: FourCc = FourCc::from_bytes(*b"mdhd"); +const HDLR: FourCc = FourCc::from_bytes(*b"hdlr"); +const TRAF: FourCc = FourCc::from_bytes(*b"traf"); +const TFHD: FourCc = FourCc::from_bytes(*b"tfhd"); +const TFDT: FourCc = FourCc::from_bytes(*b"tfdt"); +const TRUN: FourCc = FourCc::from_bytes(*b"trun"); +const EMSG: FourCc = FourCc::from_bytes(*b"emsg"); +const VIDE: FourCc = FourCc::from_bytes(*b"vide"); +const SOUN: FourCc = FourCc::from_bytes(*b"soun"); + +#[derive(Clone)] +struct InitTrackInfo { + track_id: u32, + handler_type: Option, + timescale: u32, +} + +struct InitAnalysis { + timing_track: SidxTimingTrackInfo, + trex: Trex, +} + +#[derive(Clone)] +struct SegmentAccumulator { + first_box: BoxInfo, + moofs: Vec, + size: u64, + segment_sidx_count: usize, +} + +#[derive(Clone)] +struct ExistingTopLevelSidxInternal { + public: ExistingTopLevelSidx, +} + +/// File-level analysis data needed to derive the default inputs for a top-level `sidx` refresh. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TopLevelSidxUpdateAnalysis { + /// Track chosen for timing calculations. + pub timing_track: SidxTimingTrackInfo, + /// Derived media-segment inputs that feed `sidx` entry construction. + pub segments: Vec, + /// Existing top-level `sidx` placement data plus the first insertion position for a new box. + pub placement: TopLevelSidxPlacement, +} + +/// Timing-track metadata derived from the fragmented init segment. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SidxTimingTrackInfo { + /// Track identifier selected for timing calculations. + pub track_id: u32, + /// Handler type from the selected track's `hdlr`, when present. + pub handler_type: Option, + /// Timescale from the selected track's `mdhd`. + pub timescale: u32, +} + +/// One grouped media-segment input used to build a top-level `sidx` entry. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SidxMediaSegment { + /// First top-level box covered by the grouped media segment. + pub first_box: BoxInfo, + /// File offset of the first covered `moof`. + pub first_moof_offset: u64, + /// Number of covered `moof` boxes in the grouped media segment. + pub moof_count: usize, + /// Number of covered fragments that contributed timing from the selected track. + pub timing_fragment_count: usize, + /// Presentation time for the grouped segment in the selected track timescale. + pub presentation_time: u64, + /// Base decode time for the grouped segment in the selected track timescale. + pub base_decode_time: u64, + /// Total duration contributed by the selected track across the grouped segment. + pub duration: u64, + /// Total serialized size of the grouped media segment in bytes. + pub size: u64, +} + +/// Placement data for replacing or inserting a top-level `sidx`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TopLevelSidxPlacement { + /// First top-level media box before which a new `sidx` should be inserted. + pub insertion_box: BoxInfo, + /// Existing file-level `sidx` boxes in top-level order. + pub existing_top_level_sidxs: Vec, +} + +/// Existing file-level `sidx` metadata reused by the refresh planner. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ExistingTopLevelSidx { + /// Header metadata for the serialized top-level `sidx`. + pub info: BoxInfo, + /// Absolute anchor point used to derive indexed segment start offsets. + pub anchor_point: u64, + /// Absolute start offset for each indexed media segment. + pub segment_starts: Vec, +} + +/// Planning options for the deterministic top-level `sidx` refresh builder. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct TopLevelSidxPlanOptions { + /// Whether the planner should build an insertion plan when no top-level `sidx` exists yet. + pub add_if_not_exists: bool, + /// Whether the planned version 1 `sidx` should preserve the first segment's non-zero earliest + /// presentation time. + pub non_zero_ept: bool, +} + +/// Deterministic top-level `sidx` refresh plan built from analyzed file data. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TopLevelSidxPlan { + /// Track selected for timing calculations in the planned `sidx`. + pub timing_track: SidxTimingTrackInfo, + /// Concrete version 1 `sidx` payload to write. + pub sidx: Sidx, + /// Whether the plan inserts a new top-level `sidx` or replaces one existing box. + pub action: TopLevelSidxPlanAction, + /// Top-level box before which the planned `sidx` should be written. + pub insertion_box: BoxInfo, + /// Expected serialized size of the planned `sidx` box, including its header. + pub encoded_box_size: u64, + /// Planned `sidx` entry coverage in file order. + pub entries: Vec, +} + +/// Top-level write action selected for a deterministic `sidx` refresh plan. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum TopLevelSidxPlanAction { + /// Insert a new top-level `sidx` before [`TopLevelSidxPlan::insertion_box`]. + Insert, + /// Replace one existing top-level `sidx` while keeping it before the covered media run. + Replace { + /// Existing top-level `sidx` selected as the replacement target. + existing: ExistingTopLevelSidx, + }, +} + +/// Planned coverage for one `sidx` entry. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TopLevelSidxEntryPlan { + /// One-based entry index inside the planned `sidx`. + pub entry_index: u32, + /// Absolute start offset of the grouped media run in the original file. + pub start_offset: u64, + /// Absolute end offset of the grouped media run in the original file. + pub end_offset: u64, + /// File-level grouped media-segment data that feeds this `sidx` entry. + pub segment: SidxMediaSegment, + /// Planned target-size value. + pub target_size: u32, + /// Planned `SubSegmentDuration` value. + pub subsegment_duration: u32, +} + +/// Final top-level `sidx` box produced by a rewrite helper. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AppliedTopLevelSidx { + /// Header metadata for the rewritten top-level `sidx`. + pub info: BoxInfo, + /// Rewritten typed `sidx` payload with final offsets and entry sizes. + pub sidx: Sidx, +} + +/// Errors raised while deriving top-level `sidx` update defaults. +#[derive(Debug)] +pub enum SidxAnalysisError { + Io(io::Error), + Header(HeaderError), + Codec(CodecError), + Extract(ExtractError), + /// The file does not expose a fragmented layout that can carry a top-level `sidx`. + NotFragmented, + /// The file is missing the root `moov` box needed to resolve track defaults. + MissingMovieBox, + /// The fragmented init segment does not expose the `mvex` defaults required for planning. + MissingMovieExtendsBox, + /// No track boxes were available inside the fragmented init segment. + MissingTracks, + /// No grouped media segments were available for analysis. + MissingMediaSegments, + /// A required child box was missing from a parsed container. + MissingRequiredChild { + /// Parent box type that should have contained the child. + parent_box_type: FourCc, + /// Absolute offset of the parent box. + parent_offset: u64, + /// Missing child box type. + child_box_type: FourCc, + }, + /// A container carried multiple matches for a child box that should be unique. + UnexpectedChildCount { + /// Parent box type that contained the duplicate children. + parent_box_type: FourCc, + /// Absolute offset of the parent box. + parent_offset: u64, + /// Child box type that appeared more than once. + child_box_type: FourCc, + /// Number of matched child boxes. + count: usize, + }, + /// No matching `trex` defaults were available for the selected timing track. + MissingTrackExtends { + /// Track identifier that needed a matching `trex`. + track_id: u32, + }, + /// A grouped media segment did not contain any `moof` boxes. + SegmentWithoutMovieFragment { + /// One-based segment index. + segment_index: usize, + /// Absolute start offset of the segment. + segment_offset: u64, + }, + /// A grouped media segment did not carry any fragments for the selected timing track. + MissingTimingTrackFragments { + /// One-based segment index. + segment_index: usize, + /// Timing-track identifier. + track_id: u32, + }, + /// A matching track fragment was missing its required `tfdt`. + MissingTrackFragmentDecodeTime { + /// One-based segment index. + segment_index: usize, + /// Absolute `moof` offset that carried the incomplete fragment. + moof_offset: u64, + /// Timing-track identifier. + track_id: u32, + }, + /// A typed `trun` payload declared a sample count that does not match its decoded entries. + TrunSampleCountMismatch { + /// Absolute `moof` offset that carried the invalid `trun`. + moof_offset: u64, + /// Timing-track identifier. + track_id: u32, + /// Declared `sample_count` from the `trun`. + declared: u32, + /// Decoded number of entry records. + actual: usize, + }, + /// The existing top-level `sidx` layout uses chained entries that this helper does not + /// model. + UnsupportedTopLevelSidxIndirectEntry { + /// Absolute offset of the unsupported top-level `sidx`. + sidx_offset: u64, + /// One-based entry index inside that `sidx`. + entry_index: usize, + }, + /// The typed `sidx` payload declared an entry count that does not match the decoded list. + SidxEntryCountMismatch { + /// Absolute offset of the invalid top-level `sidx`. + sidx_offset: u64, + /// Declared `entry_count` from the `sidx`. + declared: u16, + /// Decoded number of entries. + actual: usize, + }, + /// Derived arithmetic overflowed the helper's supported range. + NumericOverflow { + /// Human-readable name of the derived field that overflowed. + field_name: &'static str, + }, +} + +/// Errors raised while building a deterministic top-level `sidx` refresh plan. +#[derive(Debug)] +pub enum SidxPlanError { + Analysis(SidxAnalysisError), + Codec(CodecError), + /// More than one file-level top-level `sidx` would need coordinated replacement. + UnsupportedTopLevelSidxCount { + /// Number of file-level top-level `sidx` boxes discovered during analysis. + count: usize, + }, + /// The existing replacement target does not cover the same media start as the derived plan. + UnsupportedReplacementPlacement { + /// Absolute offset of the existing replacement target. + existing_offset: u64, + /// Absolute offset of the first covered media box. + media_start_offset: u64, + }, + /// The planned entry count does not fit in the typed `Sidx` model. + TooManyEntries { + /// Number of grouped media runs that would become `sidx` entries. + count: usize, + }, + /// One grouped run's serialized size exceeds the supported `sidx` field width. + SegmentSizeOverflow { + /// One-based grouped segment index. + segment_index: usize, + /// Actual grouped segment size in bytes. + size: u64, + }, + /// One grouped run's duration exceeds the supported `sidx` field width. + SegmentDurationOverflow { + /// One-based grouped segment index. + segment_index: usize, + /// Actual grouped segment duration in track timescale units. + duration: u64, + }, + /// The derived grouped-run end offset overflowed the helper's supported range. + EntryEndOffsetOverflow { + /// One-based grouped segment index. + segment_index: usize, + /// Absolute grouped-run start offset. + start_offset: u64, + /// Grouped-run size in bytes. + size: u64, + }, + /// The serialized `sidx` box size overflowed the helper's supported range. + EncodedBoxSizeOverflow, +} + +/// Errors raised while applying a deterministic top-level `sidx` rewrite plan. +#[derive(Debug)] +pub enum SidxRewriteError { + Io(io::Error), + Header(HeaderError), + Codec(CodecError), + /// The supplied plan did not contain any grouped entries to write. + EmptyPlanEntries, + /// The typed `sidx` payload and grouped entry coverage did not agree on entry counts. + PlannedEntryCountMismatch { + /// Declared `entry_count` stored in the typed `sidx`. + declared: u16, + /// Number of typed `sidx` entries in the plan payload. + sidx_entries: usize, + /// Number of grouped entry spans in the plan coverage. + plan_entries: usize, + }, + /// The supplied plan carried an unsupported `sidx` version. + UnsupportedSidxVersion { + /// Unsupported full-box version. + version: u8, + }, + /// A planned root box no longer matched the input bytes at the expected offset. + PlannedBoxMismatch { + /// Box type recorded in the plan. + expected_type: FourCc, + /// Absolute offset recorded in the plan. + expected_offset: u64, + /// Total box size recorded in the plan. + expected_size: u64, + /// Box type read from the input while validating the plan. + actual_type: FourCc, + /// Absolute offset read from the input while validating the plan. + actual_offset: u64, + /// Total box size read from the input while validating the plan. + actual_size: u64, + }, + /// The rewritten `sidx` box would extend past the first covered media span. + InvalidRewrittenLayout { + /// Absolute end offset of the rewritten `sidx`. + sidx_end_offset: u64, + /// Absolute start offset of the first covered media span after rewriting. + first_segment_start_offset: u64, + }, + /// One grouped entry span became invalid after rewriting. + InvalidEntrySpan { + /// One-based entry index inside the rewritten `sidx`. + entry_index: u32, + /// Rewritten grouped span start offset. + start_offset: u64, + /// Rewritten grouped span end offset. + end_offset: u64, + }, + /// One rewritten entry span overflowed the supported `target_size` field width. + TargetSizeOverflow { + /// One-based entry index inside the rewritten `sidx`. + entry_index: u32, + /// Rewritten grouped span size in bytes. + size: u64, + }, + /// The rewrite helper could not copy the requested byte range in full. + IncompleteCopy { + /// Number of bytes that should have been copied. + expected_size: u64, + /// Number of bytes that were actually copied. + actual_size: u64, + }, + /// Numeric overflow occurred while deriving the rewritten box layout. + NumericOverflow { + /// Derived field whose value overflowed. + field_name: &'static str, + }, + /// The serialized rewritten `sidx` box size overflowed the helper's supported range. + EncodedBoxSizeOverflow, +} + +impl fmt::Display for SidxAnalysisError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Io(error) => write!(f, "{error}"), + Self::Header(error) => write!(f, "{error}"), + Self::Codec(error) => write!(f, "{error}"), + Self::Extract(error) => write!(f, "{error}"), + Self::NotFragmented => f.write_str("input file is not fragmented"), + Self::MissingMovieBox => f.write_str("input file does not have a moov box"), + Self::MissingMovieExtendsBox => { + f.write_str("fragmented init segment does not have a moov/mvex layout") + } + Self::MissingTracks => { + f.write_str("fragmented init segment does not contain any tracks") + } + Self::MissingMediaSegments => { + f.write_str("input file does not have any media segments") + } + Self::MissingRequiredChild { + parent_box_type, + parent_offset, + child_box_type, + } => write!( + f, + "missing required {child_box_type} child inside {parent_box_type} at offset {parent_offset}" + ), + Self::UnexpectedChildCount { + parent_box_type, + parent_offset, + child_box_type, + count, + } => write!( + f, + "expected one {child_box_type} child inside {parent_box_type} at offset {parent_offset}, found {count}" + ), + Self::MissingTrackExtends { track_id } => { + write!(f, "no trex box found for track {track_id}") + } + Self::SegmentWithoutMovieFragment { + segment_index, + segment_offset, + } => write!( + f, + "segment {segment_index} at offset {segment_offset} does not contain a moof box" + ), + Self::MissingTimingTrackFragments { + segment_index, + track_id, + } => write!( + f, + "segment {segment_index} does not contain fragments for timing track {track_id}" + ), + Self::MissingTrackFragmentDecodeTime { + segment_index, + moof_offset, + track_id, + } => write!( + f, + "segment {segment_index} moof at offset {moof_offset} is missing tfdt for track {track_id}" + ), + Self::TrunSampleCountMismatch { + moof_offset, + track_id, + declared, + actual, + } => write!( + f, + "moof at offset {moof_offset} has a trun sample count mismatch for track {track_id}: declared {declared}, decoded {actual}" + ), + Self::UnsupportedTopLevelSidxIndirectEntry { + sidx_offset, + entry_index, + } => write!( + f, + "top-level sidx at offset {sidx_offset} uses unsupported type 1 at entry {entry_index}" + ), + Self::SidxEntryCountMismatch { + sidx_offset, + declared, + actual, + } => write!( + f, + "top-level sidx at offset {sidx_offset} declared {declared} entries but decoded {actual}" + ), + Self::NumericOverflow { field_name } => { + write!(f, "numeric overflow while deriving {field_name}") + } + } + } +} + +impl fmt::Display for SidxPlanError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Analysis(error) => write!(f, "{error}"), + Self::Codec(error) => write!(f, "{error}"), + Self::UnsupportedTopLevelSidxCount { count } => write!( + f, + "unsupported top-level sidx topology: expected at most one file-level top-level sidx, found {count}" + ), + Self::UnsupportedReplacementPlacement { + existing_offset, + media_start_offset, + } => write!( + f, + "unsupported top-level sidx replacement layout: existing sidx at offset {existing_offset} does not align with first media start {media_start_offset}" + ), + Self::TooManyEntries { count } => { + write!(f, "planned sidx entry count {count} does not fit in u16") + } + Self::SegmentSizeOverflow { + segment_index, + size, + } => write!( + f, + "segment {segment_index} size {size} does not fit in the 31-bit target-size field" + ), + Self::SegmentDurationOverflow { + segment_index, + duration, + } => write!( + f, + "segment {segment_index} duration {duration} does not fit in the 32-bit subsegment-duration field" + ), + Self::EntryEndOffsetOverflow { + segment_index, + start_offset, + size, + } => write!( + f, + "segment {segment_index} end offset overflowed while adding size {size} to start {start_offset}" + ), + Self::EncodedBoxSizeOverflow => { + f.write_str("encoded sidx box size overflowed the supported range") + } + } + } +} + +impl fmt::Display for SidxRewriteError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Io(error) => write!(f, "{error}"), + Self::Header(error) => write!(f, "{error}"), + Self::Codec(error) => write!(f, "{error}"), + Self::EmptyPlanEntries => { + f.write_str("planned top-level sidx rewrite does not contain any entries") + } + Self::PlannedEntryCountMismatch { + declared, + sidx_entries, + plan_entries, + } => write!( + f, + "planned top-level sidx entry counts disagree: declared {declared}, typed payload {sidx_entries}, grouped spans {plan_entries}" + ), + Self::UnsupportedSidxVersion { version } => { + write!( + f, + "planned top-level sidx uses unsupported version {version}" + ) + } + Self::PlannedBoxMismatch { + expected_type, + expected_offset, + expected_size, + actual_type, + actual_offset, + actual_size, + } => write!( + f, + "planned box mismatch at offset {expected_offset}: expected {expected_type} size {expected_size}, found {actual_type} at offset {actual_offset} size {actual_size}" + ), + Self::InvalidRewrittenLayout { + sidx_end_offset, + first_segment_start_offset, + } => write!( + f, + "rewritten top-level sidx would end at {sidx_end_offset} after the first covered segment starts at {first_segment_start_offset}" + ), + Self::InvalidEntrySpan { + entry_index, + start_offset, + end_offset, + } => write!( + f, + "rewritten entry {entry_index} ended at {end_offset} before its start {start_offset}" + ), + Self::TargetSizeOverflow { entry_index, size } => write!( + f, + "rewritten entry {entry_index} size {size} does not fit in the 31-bit target-size field" + ), + Self::IncompleteCopy { + expected_size, + actual_size, + } => write!( + f, + "failed to copy rewrite bytes: expected {expected_size} bytes, copied {actual_size}" + ), + Self::NumericOverflow { field_name } => { + write!(f, "numeric overflow while deriving {field_name}") + } + Self::EncodedBoxSizeOverflow => { + f.write_str("encoded rewritten sidx box size overflowed the supported range") + } + } + } +} + +impl Error for SidxAnalysisError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Io(error) => Some(error), + Self::Header(error) => Some(error), + Self::Codec(error) => Some(error), + Self::Extract(error) => Some(error), + Self::NotFragmented + | Self::MissingMovieBox + | Self::MissingMovieExtendsBox + | Self::MissingTracks + | Self::MissingMediaSegments + | Self::MissingRequiredChild { .. } + | Self::UnexpectedChildCount { .. } + | Self::MissingTrackExtends { .. } + | Self::SegmentWithoutMovieFragment { .. } + | Self::MissingTimingTrackFragments { .. } + | Self::MissingTrackFragmentDecodeTime { .. } + | Self::TrunSampleCountMismatch { .. } + | Self::UnsupportedTopLevelSidxIndirectEntry { .. } + | Self::SidxEntryCountMismatch { .. } + | Self::NumericOverflow { .. } => None, + } + } +} + +impl Error for SidxPlanError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Analysis(error) => Some(error), + Self::Codec(error) => Some(error), + Self::UnsupportedTopLevelSidxCount { .. } + | Self::UnsupportedReplacementPlacement { .. } + | Self::TooManyEntries { .. } + | Self::SegmentSizeOverflow { .. } + | Self::SegmentDurationOverflow { .. } + | Self::EntryEndOffsetOverflow { .. } + | Self::EncodedBoxSizeOverflow => None, + } + } +} + +impl Error for SidxRewriteError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Io(error) => Some(error), + Self::Header(error) => Some(error), + Self::Codec(error) => Some(error), + Self::EmptyPlanEntries + | Self::PlannedEntryCountMismatch { .. } + | Self::UnsupportedSidxVersion { .. } + | Self::PlannedBoxMismatch { .. } + | Self::InvalidRewrittenLayout { .. } + | Self::InvalidEntrySpan { .. } + | Self::TargetSizeOverflow { .. } + | Self::IncompleteCopy { .. } + | Self::NumericOverflow { .. } + | Self::EncodedBoxSizeOverflow => None, + } + } +} + +impl From for SidxAnalysisError { + fn from(value: io::Error) -> Self { + Self::Io(value) + } +} + +impl From for SidxAnalysisError { + fn from(value: HeaderError) -> Self { + Self::Header(value) + } +} + +impl From for SidxAnalysisError { + fn from(value: CodecError) -> Self { + Self::Codec(value) + } +} + +impl From for SidxAnalysisError { + fn from(value: ExtractError) -> Self { + Self::Extract(value) + } +} + +impl From for SidxPlanError { + fn from(value: SidxAnalysisError) -> Self { + Self::Analysis(value) + } +} + +impl From for SidxPlanError { + fn from(value: CodecError) -> Self { + Self::Codec(value) + } +} + +impl From for SidxRewriteError { + fn from(value: io::Error) -> Self { + Self::Io(value) + } +} + +impl From for SidxRewriteError { + fn from(value: HeaderError) -> Self { + Self::Header(value) + } +} + +impl From for SidxRewriteError { + fn from(value: CodecError) -> Self { + Self::Codec(value) + } +} + +/// Analyzes a fragmented file and returns the default inputs for a top-level `sidx` refresh. +/// +/// The derived behavior follows the current fragmented-file defaults: +/// - select a timing track by preferring `vide`, then `soun`, then the first track +/// - group media runs using top-level `styp` boxes or existing top-level `sidx` entries +/// - otherwise treat the first `moof`-started run as one grouped media segment +pub fn analyze_top_level_sidx_update( + reader: &mut R, +) -> Result +where + R: Read + Seek, +{ + let root_boxes = scan_root_boxes(reader)?; + let has_fragment_markers = root_boxes + .iter() + .any(|info| matches!(info.box_type(), MOOF | STYP | SIDX)); + + let moov = root_boxes + .iter() + .find(|info| info.box_type() == MOOV) + .copied() + .ok_or(SidxAnalysisError::MissingMovieBox)?; + + let has_mvex = !extract_boxes(reader, Some(&moov), &[BoxPath::from([MVEX])])?.is_empty(); + if !has_fragment_markers && !has_mvex { + return Err(SidxAnalysisError::NotFragmented); + } + if !has_mvex { + return Err(SidxAnalysisError::MissingMovieExtendsBox); + } + + let init = analyze_init_segment(reader, &moov)?; + let (segments, existing_top_level_sidxs) = group_media_segments(reader, &root_boxes)?; + if segments.is_empty() { + return Err(SidxAnalysisError::MissingMediaSegments); + } + + let mut analyzed_segments = Vec::with_capacity(segments.len()); + for (segment_index, segment) in segments.iter().enumerate() { + analyzed_segments.push(analyze_segment( + reader, + segment_index + 1, + segment, + init.timing_track.track_id, + &init.trex, + )?); + } + + Ok(TopLevelSidxUpdateAnalysis { + timing_track: init.timing_track, + placement: TopLevelSidxPlacement { + insertion_box: segments[0].first_box, + existing_top_level_sidxs: existing_top_level_sidxs + .into_iter() + .map(|entry| entry.public) + .collect(), + }, + segments: analyzed_segments, + }) +} + +/// Analyzes a fragmented byte slice and returns the default inputs for a top-level `sidx` +/// refresh. +pub fn analyze_top_level_sidx_update_bytes( + input: &[u8], +) -> Result { + let mut reader = Cursor::new(input); + analyze_top_level_sidx_update(&mut reader) +} + +/// Builds a deterministic top-level `sidx` refresh plan from analyzed file data. +/// +/// Returns `Ok(None)` when the file does not currently contain a top-level `sidx` and +/// `add_if_not_exists` is `false`, leaving the input unchanged. +pub fn build_top_level_sidx_plan( + analysis: &TopLevelSidxUpdateAnalysis, + options: TopLevelSidxPlanOptions, +) -> Result, SidxPlanError> { + if analysis.segments.is_empty() { + return Err(SidxAnalysisError::MissingMediaSegments.into()); + } + + let action = match analysis.placement.existing_top_level_sidxs.as_slice() { + [] if options.add_if_not_exists => TopLevelSidxPlanAction::Insert, + [] => return Ok(None), + [existing] => { + if existing.segment_starts.first().copied() + != Some(analysis.placement.insertion_box.offset()) + { + return Err(SidxPlanError::UnsupportedReplacementPlacement { + existing_offset: existing.info.offset(), + media_start_offset: analysis.placement.insertion_box.offset(), + }); + } + TopLevelSidxPlanAction::Replace { + existing: existing.clone(), + } + } + existing => { + return Err(SidxPlanError::UnsupportedTopLevelSidxCount { + count: existing.len(), + }); + } + }; + + let entry_count = + u16::try_from(analysis.segments.len()).map_err(|_| SidxPlanError::TooManyEntries { + count: analysis.segments.len(), + })?; + + let mut sidx = Sidx::default(); + sidx.set_version(1); + sidx.reference_id = 1; + sidx.timescale = analysis.timing_track.timescale; + sidx.earliest_presentation_time_v1 = if options.non_zero_ept { + analysis.segments[0].presentation_time + } else { + 0 + }; + sidx.first_offset_v1 = 0; + sidx.reference_count = entry_count; + + let mut entries = Vec::with_capacity(analysis.segments.len()); + let mut sidx_entries = Vec::with_capacity(analysis.segments.len()); + for (index, segment) in analysis.segments.iter().enumerate() { + let target_size = + u32::try_from(segment.size).map_err(|_| SidxPlanError::SegmentSizeOverflow { + segment_index: index + 1, + size: segment.size, + })?; + if target_size > 0x7fff_ffff { + return Err(SidxPlanError::SegmentSizeOverflow { + segment_index: index + 1, + size: segment.size, + }); + } + let subsegment_duration = u32::try_from(segment.duration).map_err(|_| { + SidxPlanError::SegmentDurationOverflow { + segment_index: index + 1, + duration: segment.duration, + } + })?; + let end_offset = segment.first_box.offset().checked_add(segment.size).ok_or( + SidxPlanError::EntryEndOffsetOverflow { + segment_index: index + 1, + start_offset: segment.first_box.offset(), + size: segment.size, + }, + )?; + + sidx_entries.push(SidxReference { + reference_type: false, + referenced_size: target_size, + subsegment_duration, + starts_with_sap: true, + sap_type: 1, + sap_delta_time: 0, + }); + entries.push(TopLevelSidxEntryPlan { + entry_index: (index + 1) as u32, + start_offset: segment.first_box.offset(), + end_offset, + segment: segment.clone(), + target_size, + subsegment_duration, + }); + } + sidx.references = sidx_entries; + + let payload_size = encoded_payload_size(&sidx)?; + let encoded_box_size = payload_size + .checked_add(box_header_size_for_payload(payload_size)) + .ok_or(SidxPlanError::EncodedBoxSizeOverflow)?; + + Ok(Some(TopLevelSidxPlan { + timing_track: analysis.timing_track.clone(), + sidx, + action, + insertion_box: analysis.placement.insertion_box, + encoded_box_size, + entries, + })) +} + +/// Analyzes a fragmented file and builds the deterministic top-level `sidx` refresh plan. +/// +/// Returns `Ok(None)` when `add_if_not_exists` is `false` and no file-level top-level `sidx` +/// exists yet. +pub fn plan_top_level_sidx_update( + reader: &mut R, + options: TopLevelSidxPlanOptions, +) -> Result, SidxPlanError> +where + R: Read + Seek, +{ + let analysis = analyze_top_level_sidx_update(reader)?; + build_top_level_sidx_plan(&analysis, options) +} + +/// Analyzes a fragmented byte slice and builds the deterministic top-level `sidx` refresh plan. +pub fn plan_top_level_sidx_update_bytes( + input: &[u8], + options: TopLevelSidxPlanOptions, +) -> Result, SidxPlanError> { + let mut reader = Cursor::new(input); + plan_top_level_sidx_update(&mut reader, options) +} + +/// Applies a deterministic top-level `sidx` plan to a fragmented file and writes the updated bytes +/// to `writer`. +/// +/// The helper only rewrites the planned top-level `sidx` span. All other bytes are copied through +/// verbatim. +pub fn apply_top_level_sidx_plan( + reader: &mut R, + mut writer: W, + plan: &TopLevelSidxPlan, +) -> Result +where + R: Read + Seek, + W: Write, +{ + validate_rewrite_plan(plan)?; + + validate_root_box(reader, &plan.insertion_box)?; + let (write_offset, removed_size) = match &plan.action { + TopLevelSidxPlanAction::Insert => (plan.insertion_box.offset(), 0), + TopLevelSidxPlanAction::Replace { existing } => { + validate_root_box(reader, &existing.info)?; + (existing.info.offset(), existing.info.size()) + } + }; + + let rewritten = build_rewritten_sidx(plan, write_offset, removed_size)?; + let input_end = reader.seek(SeekFrom::End(0))?; + let removed_end = checked_add_rewrite(write_offset, removed_size, "planned removed span end")?; + let trailing_size = + input_end + .checked_sub(removed_end) + .ok_or(SidxRewriteError::NumericOverflow { + field_name: "trailing rewrite bytes", + })?; + + copy_range_exact(reader, &mut writer, 0, write_offset)?; + writer.write_all(&rewritten.bytes)?; + copy_range_exact(reader, &mut writer, removed_end, trailing_size)?; + + Ok(rewritten.applied) +} + +/// Applies a deterministic top-level `sidx` plan to an in-memory MP4 byte slice and returns the +/// rewritten bytes. +pub fn apply_top_level_sidx_plan_bytes( + input: &[u8], + plan: &TopLevelSidxPlan, +) -> Result, SidxRewriteError> { + let mut reader = Cursor::new(input); + let mut writer = Vec::with_capacity(input.len().saturating_add(plan.encoded_box_size as usize)); + apply_top_level_sidx_plan(&mut reader, &mut writer, plan)?; + Ok(writer) +} + +fn scan_root_boxes(reader: &mut R) -> Result, SidxAnalysisError> +where + R: Read + Seek, +{ + let end = reader.seek(SeekFrom::End(0))?; + reader.seek(SeekFrom::Start(0))?; + + let mut boxes = Vec::new(); + while reader.stream_position()? < end { + let info = BoxInfo::read(reader)?; + boxes.push(info); + info.seek_to_end(reader)?; + } + + Ok(boxes) +} + +fn analyze_init_segment( + reader: &mut R, + moov: &BoxInfo, +) -> Result +where + R: Read + Seek, +{ + let mvex = require_single_child_info(reader, moov, MVEX)?; + let traks = extract_boxes(reader, Some(moov), &[BoxPath::from([TRAK])])?; + if traks.is_empty() { + return Err(SidxAnalysisError::MissingTracks); + } + + let mut tracks = Vec::with_capacity(traks.len()); + for trak in traks { + let tkhd = require_single_child_as::<_, Tkhd>(reader, &trak, TKHD)?; + let mdhd = require_single_nested_child_as::<_, Mdhd>(reader, &trak, MDIA, MDHD)?; + let handler_type = extract_optional_handler_type(reader, &trak)?; + tracks.push(InitTrackInfo { + track_id: tkhd.track_id, + handler_type, + timescale: mdhd.timescale, + }); + } + + let timing_track = select_timing_track(&tracks)?; + let trex = extract_box_as::<_, Trex>(reader, Some(&mvex), BoxPath::from([TREX]))? + .into_iter() + .find(|entry| entry.track_id == timing_track.track_id) + .ok_or(SidxAnalysisError::MissingTrackExtends { + track_id: timing_track.track_id, + })?; + + Ok(InitAnalysis { timing_track, trex }) +} + +fn select_timing_track(tracks: &[InitTrackInfo]) -> Result { + let track = tracks + .iter() + .find(|track| track.handler_type == Some(VIDE)) + .or_else(|| tracks.iter().find(|track| track.handler_type == Some(SOUN))) + .or_else(|| tracks.first()) + .ok_or(SidxAnalysisError::MissingTracks)?; + + Ok(SidxTimingTrackInfo { + track_id: track.track_id, + handler_type: track.handler_type, + timescale: track.timescale, + }) +} + +fn extract_optional_handler_type( + reader: &mut R, + trak: &BoxInfo, +) -> Result, SidxAnalysisError> +where + R: Read + Seek, +{ + let handlers = extract_box_as::<_, crate::boxes::iso14496_12::Hdlr>( + reader, + Some(trak), + BoxPath::from([MDIA, HDLR]), + )?; + match handlers.len() { + 0 => Ok(None), + 1 => Ok(Some(handlers[0].handler_type)), + count => Err(SidxAnalysisError::UnexpectedChildCount { + parent_box_type: trak.box_type(), + parent_offset: trak.offset(), + child_box_type: HDLR, + count, + }), + } +} + +fn require_single_child_info( + reader: &mut R, + parent: &BoxInfo, + child_box_type: FourCc, +) -> Result +where + R: Read + Seek, +{ + let infos = extract_boxes(reader, Some(parent), &[BoxPath::from([child_box_type])])?; + match infos.len() { + 0 => Err(SidxAnalysisError::MissingRequiredChild { + parent_box_type: parent.box_type(), + parent_offset: parent.offset(), + child_box_type, + }), + 1 => Ok(infos[0]), + count => Err(SidxAnalysisError::UnexpectedChildCount { + parent_box_type: parent.box_type(), + parent_offset: parent.offset(), + child_box_type, + count, + }), + } +} + +fn require_single_child_as( + reader: &mut R, + parent: &BoxInfo, + child_box_type: FourCc, +) -> Result +where + R: Read + Seek, + B: CodecBox + Clone + 'static, +{ + let boxes = extract_box_as::<_, B>(reader, Some(parent), BoxPath::from([child_box_type]))?; + match boxes.len() { + 0 => Err(SidxAnalysisError::MissingRequiredChild { + parent_box_type: parent.box_type(), + parent_offset: parent.offset(), + child_box_type, + }), + 1 => Ok(boxes.into_iter().next().unwrap()), + count => Err(SidxAnalysisError::UnexpectedChildCount { + parent_box_type: parent.box_type(), + parent_offset: parent.offset(), + child_box_type, + count, + }), + } +} + +fn require_single_nested_child_as( + reader: &mut R, + parent: &BoxInfo, + intermediate_box_type: FourCc, + child_box_type: FourCc, +) -> Result +where + R: Read + Seek, + B: CodecBox + Clone + 'static, +{ + let boxes = extract_box_as::<_, B>( + reader, + Some(parent), + BoxPath::from([intermediate_box_type, child_box_type]), + )?; + match boxes.len() { + 0 => Err(SidxAnalysisError::MissingRequiredChild { + parent_box_type: parent.box_type(), + parent_offset: parent.offset(), + child_box_type, + }), + 1 => Ok(boxes.into_iter().next().unwrap()), + count => Err(SidxAnalysisError::UnexpectedChildCount { + parent_box_type: parent.box_type(), + parent_offset: parent.offset(), + child_box_type, + count, + }), + } +} + +fn group_media_segments( + reader: &mut R, + root_boxes: &[BoxInfo], +) -> Result<(Vec, Vec), SidxAnalysisError> +where + R: Read + Seek, +{ + let mut segments = Vec::new(); + let mut existing_top_level_sidxs = Vec::new(); + let mut previous_box_type = None; + + for info in root_boxes { + match info.box_type() { + STYP => { + start_segment(&mut segments, *info); + add_segment_size( + segments.last_mut().unwrap(), + info.size(), + "media segment size", + )?; + } + SIDX => { + if segments.is_empty() && previous_box_type != Some(MDAT) { + let decoded = read_payload_as::<_, Sidx>(reader, info)?; + let internal = analyze_existing_top_level_sidx(info, &decoded)?; + existing_top_level_sidxs.push(internal); + } else if previous_box_type == Some(MDAT) { + start_segment(&mut segments, *info); + let segment = segments.last_mut().unwrap(); + segment.segment_sidx_count = 1; + add_segment_size(segment, info.size(), "media segment size")?; + } else if let Some(segment) = segments.last_mut() { + if segment.segment_sidx_count == 0 { + add_segment_size(segment, info.size(), "media segment size")?; + } + segment.segment_sidx_count += 1; + } + } + EMSG | MOOF => { + if should_start_segment(info.offset(), segments.len(), &existing_top_level_sidxs) { + start_segment(&mut segments, *info); + } + + if let Some(segment) = segments.last_mut() { + add_segment_size(segment, info.size(), "media segment size")?; + if info.box_type() == MOOF { + segment.moofs.push(*info); + } + } + } + MDAT => { + if let Some(segment) = segments.last_mut() { + add_segment_size(segment, info.size(), "media segment size")?; + } + } + _ => {} + } + + previous_box_type = Some(info.box_type()); + } + + Ok((segments, existing_top_level_sidxs)) +} + +fn start_segment(segments: &mut Vec, first_box: BoxInfo) { + segments.push(SegmentAccumulator { + first_box, + moofs: Vec::new(), + size: 0, + segment_sidx_count: 0, + }); +} + +fn should_start_segment( + box_offset: u64, + segment_count: usize, + existing_top_level_sidxs: &[ExistingTopLevelSidxInternal], +) -> bool { + if existing_top_level_sidxs.is_empty() { + return segment_count == 0; + } + + let mut next_segment_index = 0usize; + for sidx in existing_top_level_sidxs { + for segment_start in &sidx.public.segment_starts { + if next_segment_index == segment_count { + return box_offset == *segment_start; + } + next_segment_index += 1; + } + } + + false +} + +fn analyze_existing_top_level_sidx( + info: &BoxInfo, + sidx: &Sidx, +) -> Result { + if usize::from(sidx.reference_count) != sidx.references.len() { + return Err(SidxAnalysisError::SidxEntryCountMismatch { + sidx_offset: info.offset(), + declared: sidx.reference_count, + actual: sidx.references.len(), + }); + } + + let anchor_point = checked_add( + checked_add(info.offset(), info.size(), "top-level sidx end offset")?, + sidx.first_offset(), + "top-level sidx anchor point", + )?; + let mut current_start = anchor_point; + let mut segment_starts = Vec::with_capacity(sidx.references.len()); + + for (index, entry) in sidx.references.iter().enumerate() { + if entry.reference_type { + return Err(SidxAnalysisError::UnsupportedTopLevelSidxIndirectEntry { + sidx_offset: info.offset(), + entry_index: index + 1, + }); + } + segment_starts.push(current_start); + current_start = checked_add( + current_start, + u64::from(entry.referenced_size), + "top-level sidx segment start", + )?; + } + + Ok(ExistingTopLevelSidxInternal { + public: ExistingTopLevelSidx { + info: *info, + anchor_point, + segment_starts, + }, + }) +} + +fn analyze_segment( + reader: &mut R, + segment_index: usize, + segment: &SegmentAccumulator, + timing_track_id: u32, + trex: &Trex, +) -> Result +where + R: Read + Seek, +{ + let first_moof = + segment + .moofs + .first() + .copied() + .ok_or(SidxAnalysisError::SegmentWithoutMovieFragment { + segment_index, + segment_offset: segment.first_box.offset(), + })?; + + let mut base_decode_time = 0_u64; + let mut first_composition_time_offset = 0_i64; + let mut duration = 0_u64; + let mut timing_fragment_count = 0_usize; + let mut matched_any_fragment = false; + + for (fragment_index, moof) in segment.moofs.iter().enumerate() { + let trafs = extract_boxes(reader, Some(moof), &[BoxPath::from([TRAF])])?; + let mut matched_timing_fragment = false; + + for traf in trafs { + let tfhd = require_single_child_as::<_, Tfhd>(reader, &traf, TFHD)?; + if tfhd.track_id != timing_track_id { + continue; + } + + let tfdt = require_single_child_as::<_, Tfdt>(reader, &traf, TFDT).map_err( + |error| match error { + SidxAnalysisError::MissingRequiredChild { .. } => { + SidxAnalysisError::MissingTrackFragmentDecodeTime { + segment_index, + moof_offset: moof.offset(), + track_id: timing_track_id, + } + } + other => other, + }, + )?; + let truns = extract_box_as::<_, Trun>(reader, Some(&traf), BoxPath::from([TRUN]))?; + + if !matched_timing_fragment { + timing_fragment_count += 1; + matched_timing_fragment = true; + } + matched_any_fragment = true; + + if fragment_index == 0 { + base_decode_time = tfdt.base_media_decode_time(); + } + + for (trun_index, trun) in truns.iter().enumerate() { + validate_trun_sample_count(trun, moof, timing_track_id)?; + + if fragment_index == 0 && trun_index == 0 && trun.sample_count > 0 { + first_composition_time_offset = effective_first_composition_time_offset(trun)?; + } + + duration = checked_add( + duration, + effective_trun_duration(trun, &tfhd, trex), + "segment duration", + )?; + } + } + } + + if !matched_any_fragment { + return Err(SidxAnalysisError::MissingTimingTrackFragments { + segment_index, + track_id: timing_track_id, + }); + } + + let presentation_time = base_decode_time + .checked_add_signed(first_composition_time_offset) + .ok_or(SidxAnalysisError::NumericOverflow { + field_name: "segment presentation time", + })?; + + Ok(SidxMediaSegment { + first_box: segment.first_box, + first_moof_offset: first_moof.offset(), + moof_count: segment.moofs.len(), + timing_fragment_count, + presentation_time, + base_decode_time, + duration, + size: segment.size, + }) +} + +fn validate_trun_sample_count( + trun: &Trun, + moof: &BoxInfo, + timing_track_id: u32, +) -> Result<(), SidxAnalysisError> { + let per_sample_fields_present = trun.flags() + & (TRUN_SAMPLE_DURATION_PRESENT + | crate::boxes::iso14496_12::TRUN_SAMPLE_SIZE_PRESENT + | crate::boxes::iso14496_12::TRUN_SAMPLE_FLAGS_PRESENT + | TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT) + != 0; + let actual = trun.entries.len(); + + if per_sample_fields_present && actual != trun.sample_count as usize { + return Err(SidxAnalysisError::TrunSampleCountMismatch { + moof_offset: moof.offset(), + track_id: timing_track_id, + declared: trun.sample_count, + actual, + }); + } + if !per_sample_fields_present && actual != 0 { + return Err(SidxAnalysisError::TrunSampleCountMismatch { + moof_offset: moof.offset(), + track_id: timing_track_id, + declared: trun.sample_count, + actual, + }); + } + + Ok(()) +} + +fn effective_first_composition_time_offset(trun: &Trun) -> Result { + if trun.sample_count == 0 { + return Ok(0); + } + if trun.flags() & TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT == 0 { + return Ok(0); + } + + if trun.entries.is_empty() { + return Err(SidxAnalysisError::NumericOverflow { + field_name: "first composition time offset", + }); + } + + Ok(trun.sample_composition_time_offset(0)) +} + +fn effective_trun_duration(trun: &Trun, tfhd: &Tfhd, trex: &Trex) -> u64 { + if trun.flags() & TRUN_SAMPLE_DURATION_PRESENT != 0 { + return trun + .entries + .iter() + .map(|entry| u64::from(entry.sample_duration)) + .sum(); + } + + let default_sample_duration = if tfhd.flags() & TFHD_DEFAULT_SAMPLE_DURATION_PRESENT != 0 { + tfhd.default_sample_duration + } else { + trex.default_sample_duration + }; + u64::from(trun.sample_count) * u64::from(default_sample_duration) +} + +fn read_payload_as(reader: &mut R, info: &BoxInfo) -> Result +where + R: Read + Seek, + B: CodecBox + Default, +{ + info.seek_to_payload(reader)?; + let mut decoded = B::default(); + unmarshal(reader, info.payload_size()?, &mut decoded, None)?; + Ok(decoded) +} + +fn add_segment_size( + segment: &mut SegmentAccumulator, + size: u64, + field_name: &'static str, +) -> Result<(), SidxAnalysisError> { + segment.size = checked_add(segment.size, size, field_name)?; + Ok(()) +} + +fn checked_add(lhs: u64, rhs: u64, field_name: &'static str) -> Result { + lhs.checked_add(rhs) + .ok_or(SidxAnalysisError::NumericOverflow { field_name }) +} + +fn encoded_payload_size(sidx: &Sidx) -> Result { + let mut payload = Vec::new(); + marshal(&mut payload, sidx, None)?; + Ok(payload.len() as u64) +} + +fn box_header_size_for_payload(payload_size: u64) -> u64 { + if payload_size.saturating_add(SMALL_HEADER_SIZE) > u32::MAX as u64 { + LARGE_HEADER_SIZE + } else { + SMALL_HEADER_SIZE + } +} + +struct EncodedRewrittenSidx { + applied: AppliedTopLevelSidx, + bytes: Vec, +} + +fn validate_rewrite_plan(plan: &TopLevelSidxPlan) -> Result<(), SidxRewriteError> { + if plan.entries.is_empty() { + return Err(SidxRewriteError::EmptyPlanEntries); + } + + let sidx_entries = plan.sidx.references.len(); + if usize::from(plan.sidx.reference_count) != sidx_entries || sidx_entries != plan.entries.len() + { + return Err(SidxRewriteError::PlannedEntryCountMismatch { + declared: plan.sidx.reference_count, + sidx_entries, + plan_entries: plan.entries.len(), + }); + } + + match plan.sidx.version() { + 0 | 1 => Ok(()), + version => Err(SidxRewriteError::UnsupportedSidxVersion { version }), + } +} + +fn validate_root_box(reader: &mut R, expected: &BoxInfo) -> Result<(), SidxRewriteError> +where + R: Read + Seek, +{ + reader.seek(SeekFrom::Start(expected.offset()))?; + let actual = BoxInfo::read(reader)?; + if actual.box_type() != expected.box_type() || actual.size() != expected.size() { + return Err(SidxRewriteError::PlannedBoxMismatch { + expected_type: expected.box_type(), + expected_offset: expected.offset(), + expected_size: expected.size(), + actual_type: actual.box_type(), + actual_offset: actual.offset(), + actual_size: actual.size(), + }); + } + + Ok(()) +} + +fn build_rewritten_sidx( + plan: &TopLevelSidxPlan, + write_offset: u64, + removed_size: u64, +) -> Result { + let removed_end = checked_add_rewrite(write_offset, removed_size, "planned removed span end")?; + let (_, initial_info) = encode_sidx_box(&plan.sidx)?; + let mut encoded_box_size = initial_info.size(); + let mut last = None; + + // Serialize until the header width stabilizes, then derive offsets from that final size. + for _ in 0..3 { + let mut sidx = plan.sidx.clone(); + let first_segment_start = shift_offset_after_rewrite( + plan.entries[0].start_offset, + removed_end, + encoded_box_size, + removed_size, + "rewritten first segment start offset", + )?; + let sidx_end = + checked_add_rewrite(write_offset, encoded_box_size, "rewritten sidx end offset")?; + let first_offset = first_segment_start.checked_sub(sidx_end).ok_or( + SidxRewriteError::InvalidRewrittenLayout { + sidx_end_offset: sidx_end, + first_segment_start_offset: first_segment_start, + }, + )?; + set_sidx_first_offset(&mut sidx, first_offset)?; + + for (index, entry) in plan.entries.iter().enumerate() { + let start_offset = shift_offset_after_rewrite( + entry.start_offset, + removed_end, + encoded_box_size, + removed_size, + "rewritten entry start offset", + )?; + let end_offset = shift_offset_after_rewrite( + entry.end_offset, + removed_end, + encoded_box_size, + removed_size, + "rewritten entry end offset", + )?; + let size = + end_offset + .checked_sub(start_offset) + .ok_or(SidxRewriteError::InvalidEntrySpan { + entry_index: entry.entry_index, + start_offset, + end_offset, + })?; + if size > 0x7fff_ffff { + return Err(SidxRewriteError::TargetSizeOverflow { + entry_index: entry.entry_index, + size, + }); + } + sidx.references[index].referenced_size = + u32::try_from(size).map_err(|_| SidxRewriteError::TargetSizeOverflow { + entry_index: entry.entry_index, + size, + })?; + } + + let (bytes, info) = encode_sidx_box(&sidx)?; + let applied = AppliedTopLevelSidx { + info: info.with_offset(write_offset), + sidx, + }; + let actual_size = applied.info.size(); + last = Some(EncodedRewrittenSidx { applied, bytes }); + if actual_size == encoded_box_size { + return Ok(last.unwrap()); + } + encoded_box_size = actual_size; + } + + Ok(last.unwrap()) +} + +fn shift_offset_after_rewrite( + offset: u64, + removed_end: u64, + inserted_size: u64, + removed_size: u64, + field_name: &'static str, +) -> Result { + if offset < removed_end { + return Ok(offset); + } + + if inserted_size >= removed_size { + offset + .checked_add(inserted_size - removed_size) + .ok_or(SidxRewriteError::NumericOverflow { field_name }) + } else { + offset + .checked_sub(removed_size - inserted_size) + .ok_or(SidxRewriteError::NumericOverflow { field_name }) + } +} + +fn set_sidx_first_offset(sidx: &mut Sidx, first_offset: u64) -> Result<(), SidxRewriteError> { + match sidx.version() { + 0 => { + sidx.first_offset_v0 = + u32::try_from(first_offset).map_err(|_| SidxRewriteError::NumericOverflow { + field_name: "rewritten first offset", + })?; + Ok(()) + } + 1 => { + sidx.first_offset_v1 = first_offset; + Ok(()) + } + version => Err(SidxRewriteError::UnsupportedSidxVersion { version }), + } +} + +fn encode_sidx_box(sidx: &Sidx) -> Result<(Vec, BoxInfo), SidxRewriteError> { + let mut payload = Vec::new(); + marshal(&mut payload, sidx, None)?; + + let payload_size = payload.len() as u64; + let header_size = box_header_size_for_payload(payload_size); + let total_size = payload_size + .checked_add(header_size) + .ok_or(SidxRewriteError::EncodedBoxSizeOverflow)?; + let info = BoxInfo::new(SIDX, total_size).with_header_size(header_size); + + let mut bytes = info.encode(); + bytes.extend_from_slice(&payload); + Ok((bytes, info)) +} + +fn copy_range_exact( + reader: &mut R, + writer: &mut W, + start: u64, + len: u64, +) -> Result<(), SidxRewriteError> +where + R: Read + Seek, + W: Write, +{ + if len == 0 { + return Ok(()); + } + + reader.seek(SeekFrom::Start(start))?; + let mut limited = reader.take(len); + let copied = io::copy(&mut limited, writer)?; + if copied != len { + return Err(SidxRewriteError::IncompleteCopy { + expected_size: len, + actual_size: copied, + }); + } + + Ok(()) +} + +fn checked_add_rewrite( + lhs: u64, + rhs: u64, + field_name: &'static str, +) -> Result { + lhs.checked_add(rhs) + .ok_or(SidxRewriteError::NumericOverflow { field_name }) +} diff --git a/src/walk.rs b/src/walk.rs index 4bef1ac..c7aa0c5 100644 --- a/src/walk.rs +++ b/src/walk.rs @@ -7,7 +7,9 @@ use std::ops::Deref; use std::str::FromStr; use crate::FourCc; -use crate::boxes::iso14496_12::Ftyp; +use crate::boxes::iso14496_12::{ + Ftyp, VisualSampleEntry, split_box_children_with_optional_trailing_bytes, +}; use crate::boxes::metadata::Keys; use crate::boxes::{BoxLookupContext, BoxRegistry, default_registry}; use crate::codec::{CodecError, DynCodecBox, unmarshal, unmarshal_any_with_context}; @@ -251,7 +253,13 @@ pub struct WalkHandle<'a, R> { info: BoxInfo, path: BoxPath, descendant_lookup_context: BoxLookupContext, - children_offset: Option, + children_layout: Option, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct ChildrenLayout { + offset: u64, + size: u64, } impl<'a, R> WalkHandle<'a, R> @@ -291,7 +299,13 @@ where self.info.lookup_context(), None, )?; - self.children_offset = Some(self.info.offset() + self.info.header_size() + read); + self.children_layout = Some(children_layout_for_payload( + self.reader, + &self.info, + payload_size, + read, + boxed.as_ref(), + )?); Ok((boxed, read)) } @@ -306,13 +320,17 @@ where io::copy(&mut limited, writer).map_err(WalkError::Io) } - fn ensure_children_offset(&mut self) -> Result { - if let Some(children_offset) = self.children_offset { - return Ok(children_offset); + fn ensure_children_layout(&mut self) -> Result { + if let Some(children_layout) = self.children_layout { + return Ok(children_layout); } - let (_, read) = self.read_payload()?; - Ok(self.info.offset() + self.info.header_size() + read) + self.read_payload()?; + if let Some(children_layout) = self.children_layout { + Ok(children_layout) + } else { + unreachable!("read_payload always computes children layout") + } } } @@ -461,22 +479,20 @@ where info: *info, path, descendant_lookup_context, - children_offset: None, + children_layout: None, }; let control = visitor(&mut handle)?; if matches!(control, WalkControl::Descend) { - let children_offset = handle.ensure_children_offset()?; - let children_size = handle - .info - .offset() - .saturating_add(handle.info.size()) - .saturating_sub(children_offset); + let children_layout = handle.ensure_children_layout()?; + handle + .reader + .seek(SeekFrom::Start(children_layout.offset))?; walk_sequence( handle.reader, handle.registry, visitor, - children_size, + children_layout.size, false, &handle.path, handle.descendant_lookup_context, @@ -487,6 +503,57 @@ where Ok(()) } +fn children_layout_for_payload( + reader: &mut R, + info: &BoxInfo, + payload_size: u64, + payload_read: u64, + payload: &dyn DynCodecBox, +) -> Result +where + R: Read + Seek, +{ + let offset = info.offset() + info.header_size() + payload_read; + let size = if payload.as_any().is::() { + visual_sample_entry_child_payload_size( + reader, + offset, + payload_size.saturating_sub(payload_read), + )? + } else { + payload_size.saturating_sub(payload_read) + }; + + Ok(ChildrenLayout { offset, size }) +} + +fn visual_sample_entry_child_payload_size( + reader: &mut R, + extension_offset: u64, + extension_size: u64, +) -> Result +where + R: Read + Seek, +{ + let checkpoint = reader.stream_position()?; + reader.seek(SeekFrom::Start(extension_offset))?; + let bytes = read_extension_bytes(reader, extension_size)?; + reader.seek(SeekFrom::Start(checkpoint))?; + Ok(split_box_children_with_optional_trailing_bytes(&bytes) as u64) +} + +fn read_extension_bytes(reader: &mut R, extension_size: u64) -> Result, WalkError> +where + R: Read, +{ + let extension_len = usize::try_from(extension_size).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "payload extension is too large") + })?; + let mut bytes = vec![0; extension_len]; + reader.read_exact(&mut bytes)?; + Ok(bytes) +} + fn inspect_context_carriers( reader: &mut R, info: &mut BoxInfo, diff --git a/src/writer.rs b/src/writer.rs index 22a077c..8308500 100644 --- a/src/writer.rs +++ b/src/writer.rs @@ -25,12 +25,12 @@ impl Writer { } } - /// Returns a shared reference to the underlying writer. + /// Returns a shared view of the underlying writer. pub const fn get_ref(&self) -> &W { &self.writer } - /// Returns a mutable reference to the underlying writer. + /// Returns a mutable view of the underlying writer. pub fn get_mut(&mut self) -> &mut W { &mut self.writer } diff --git a/tests/box_catalog_avs3.rs b/tests/box_catalog_avs3.rs new file mode 100644 index 0000000..fdd0701 --- /dev/null +++ b/tests/box_catalog_avs3.rs @@ -0,0 +1,199 @@ +use std::any::type_name; +use std::fmt::Debug; +use std::io::Cursor; + +use mp4forge::FourCc; +use mp4forge::boxes::avs3::Av3c; +use mp4forge::boxes::iso14496_12::{SampleEntry, VisualSampleEntry}; +use mp4forge::boxes::{AnyTypeBox, default_registry}; +use mp4forge::codec::{CodecBox, marshal, unmarshal, unmarshal_any}; +use mp4forge::stringify::stringify; + +fn assert_box_roundtrip(src: T, payload: &[u8], expected: &str) +where + T: CodecBox + Default + PartialEq + Debug + 'static, +{ + let mut encoded = Vec::new(); + let written = marshal(&mut encoded, &src, None).unwrap(); + assert_eq!( + written, + payload.len() as u64, + "marshal length for {}", + type_name::() + ); + assert_eq!(encoded, payload, "marshal bytes for {}", type_name::()); + + let mut decoded = T::default(); + let mut reader = Cursor::new(payload.to_vec()); + let read = unmarshal(&mut reader, payload.len() as u64, &mut decoded, None).unwrap(); + assert_eq!( + read, + payload.len() as u64, + "unmarshal length for {}", + type_name::() + ); + assert_eq!(decoded, src, "unmarshal value for {}", type_name::()); + + let registry = default_registry(); + let mut any_reader = Cursor::new(payload.to_vec()); + let (any_box, any_read) = unmarshal_any( + &mut any_reader, + payload.len() as u64, + src.box_type(), + ®istry, + None, + ) + .unwrap(); + assert_eq!( + any_read, + payload.len() as u64, + "registry unmarshal length for {}", + type_name::() + ); + assert_eq!(any_box.as_any().downcast_ref::().unwrap(), &src); + + assert_eq!(stringify(&src, None).unwrap(), expected); +} + +fn assert_any_box_roundtrip(src: T, payload: &[u8], expected: &str) +where + T: CodecBox + AnyTypeBox + Default + PartialEq + Debug + 'static, +{ + let mut encoded = Vec::new(); + let written = marshal(&mut encoded, &src, None).unwrap(); + assert_eq!( + written, + payload.len() as u64, + "marshal length for {}", + type_name::() + ); + assert_eq!(encoded, payload, "marshal bytes for {}", type_name::()); + + let mut decoded = T::default(); + decoded.set_box_type(src.box_type()); + let mut reader = Cursor::new(payload.to_vec()); + let read = unmarshal(&mut reader, payload.len() as u64, &mut decoded, None).unwrap(); + assert_eq!( + read, + payload.len() as u64, + "unmarshal length for {}", + type_name::() + ); + assert_eq!(decoded, src, "unmarshal value for {}", type_name::()); + + let registry = default_registry(); + let mut any_reader = Cursor::new(payload.to_vec()); + let (any_box, any_read) = unmarshal_any( + &mut any_reader, + payload.len() as u64, + src.box_type(), + ®istry, + None, + ) + .unwrap(); + assert_eq!( + any_read, + payload.len() as u64, + "registry unmarshal length for {}", + type_name::() + ); + assert_eq!(any_box.as_any().downcast_ref::().unwrap(), &src); + + assert_eq!(stringify(&src, None).unwrap(), expected); +} + +#[test] +fn avs3_catalog_roundtrips() { + assert_any_box_roundtrip( + VisualSampleEntry { + sample_entry: SampleEntry { + box_type: FourCc::from_bytes(*b"avs3"), + data_reference_index: 0x1234, + }, + pre_defined: 0x0101, + pre_defined2: [0x01000001, 0x01000002, 0x01000003], + width: 0x0102, + height: 0x0103, + horizresolution: 0x01000004, + vertresolution: 0x01000005, + reserved2: 0x01000006, + frame_count: 0x0104, + compressorname: [ + 8, b'a', b'v', b's', b'3', b'.', b't', b'e', b's', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + depth: 0x0105, + pre_defined3: 1001, + }, + &[ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x01, 0x01, 0x00, 0x00, 0x02, 0x01, 0x00, 0x00, 0x03, 0x01, 0x02, 0x01, 0x03, + 0x01, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, 0x06, 0x01, 0x04, + 0x08, b'a', b'v', b's', b'3', b'.', b't', b'e', b's', 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x05, 0x03, 0xe9, + ], + "DataReferenceIndex=4660 PreDefined=257 PreDefined2=[16777217, 16777218, 16777219] Width=258 Height=259 Horizresolution=16777220 Vertresolution=16777221 FrameCount=260 Compressorname=\"avs3.tes\" Depth=261 PreDefined3=1001", + ); + + assert_box_roundtrip( + Av3c { + configuration_version: 1, + sequence_header_length: 4, + sequence_header: vec![0x01, 0x02, 0x03, 0x04], + library_dependency_idc: 2, + }, + &[0x01, 0x00, 0x04, 0x01, 0x02, 0x03, 0x04, 0xfe], + "ConfigurationVersion=1 SequenceHeaderLength=4 SequenceHeader=[0x1, 0x2, 0x3, 0x4] LibraryDependencyIDC=0x2", + ); +} + +#[test] +fn built_in_registry_reports_supported_versions_for_landed_avs3_types() { + let registry = default_registry(); + + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"avs3")), + Some(&[][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"av3c")), + Some(&[][..]) + ); + assert!(registry.is_supported_version(FourCc::from_bytes(*b"avs3"), 9)); + assert!(registry.is_supported_version(FourCc::from_bytes(*b"av3c"), 9)); + assert!(registry.is_registered(FourCc::from_bytes(*b"avs3"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"av3c"))); +} + +#[test] +fn av3c_rejects_sequence_header_length_mismatch_during_marshal() { + let av3c = Av3c { + configuration_version: 1, + sequence_header_length: 5, + sequence_header: vec![0x01, 0x02, 0x03, 0x04], + library_dependency_idc: 2, + }; + + let error = marshal(&mut Vec::new(), &av3c, None).unwrap_err(); + assert_eq!( + error.to_string(), + "invalid field value for SequenceHeader: length does not match SequenceHeaderLength" + ); +} + +#[test] +fn av3c_rejects_reserved_bit_mismatches_during_unmarshal() { + let mut decoded = Av3c::default(); + let error = unmarshal( + &mut Cursor::new(vec![0x01, 0x00, 0x00, 0x02]), + 4, + &mut decoded, + None, + ) + .unwrap_err(); + assert_eq!( + error.to_string(), + "constant mismatch for field Reserved: expected 63" + ); +} diff --git a/tests/box_catalog_etsi_ts_102_366.rs b/tests/box_catalog_etsi_ts_102_366.rs index ffba37b..3ecf6bf 100644 --- a/tests/box_catalog_etsi_ts_102_366.rs +++ b/tests/box_catalog_etsi_ts_102_366.rs @@ -3,7 +3,7 @@ use std::fmt::Debug; use std::io::Cursor; use mp4forge::FourCc; -use mp4forge::boxes::etsi_ts_102_366::Dac3; +use mp4forge::boxes::etsi_ts_102_366::{Dac3, Dec3, Ec3Substream}; use mp4forge::boxes::iso14496_12::{AudioSampleEntry, SampleEntry}; use mp4forge::boxes::{AnyTypeBox, default_registry}; use mp4forge::codec::{CodecBox, marshal, unmarshal, unmarshal_any}; @@ -130,6 +130,32 @@ fn etsi_ts_102_366_catalog_roundtrips() { ], "DataReferenceIndex=1 EntryVersion=0 ChannelCount=6 SampleSize=16 PreDefined=0 SampleRate=48000", ); + assert_any_box_roundtrip( + AudioSampleEntry { + sample_entry: SampleEntry { + box_type: FourCc::from_bytes(*b"ec-3"), + data_reference_index: 1, + }, + entry_version: 0, + channel_count: 6, + sample_size: 16, + pre_defined: 0, + sample_rate: 48_000 << 16, + quicktime_data: Vec::new(), + }, + &[ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x01, // + 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x06, // + 0x00, 0x10, // + 0x00, 0x00, // + 0x00, 0x00, // + 0xbb, 0x80, 0x00, 0x00, + ], + "DataReferenceIndex=1 EntryVersion=0 ChannelCount=6 SampleSize=16 PreDefined=0 SampleRate=48000", + ); assert_box_roundtrip( Dac3 { @@ -143,6 +169,21 @@ fn etsi_ts_102_366_catalog_roundtrips() { &[0x10, 0x3c, 0xe0], "Fscod=0x0 Bsid=0x8 Bsmod=0x0 Acmod=0x7 LfeOn=0x1 BitRateCode=0x7", ); + assert_box_roundtrip( + Dec3 { + data_rate: 448, + num_ind_sub: 0, + ec3_substreams: vec![Ec3Substream { + fscod: 2, + bsid: 0x10, + acmod: 4, + ..Ec3Substream::default() + }], + reserved: Vec::new(), + }, + &[0x0e, 0x00, 0xa0, 0x08, 0x00], + "DataRate=448 NumIndSub=0 EC3Subs=[{FSCod=0x2 BSID=0x10 ASVC=0x0 BSMod=0x0 ACMod=0x4 LFEOn=0x0 NumDepSub=0x0 ChanLoc=0x0}]", + ); } #[test] @@ -157,10 +198,22 @@ fn built_in_registry_reports_supported_versions_for_landed_etsi_ts_102_366_types registry.supported_versions(FourCc::from_bytes(*b"dac3")), Some(&[][..]) ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"ec-3")), + Some(&[][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"dec3")), + Some(&[][..]) + ); assert!(registry.is_supported_version(FourCc::from_bytes(*b"ac-3"), 9)); assert!(registry.is_supported_version(FourCc::from_bytes(*b"dac3"), 9)); + assert!(registry.is_supported_version(FourCc::from_bytes(*b"ec-3"), 9)); + assert!(registry.is_supported_version(FourCc::from_bytes(*b"dec3"), 9)); assert!(registry.is_registered(FourCc::from_bytes(*b"ac-3"))); assert!(registry.is_registered(FourCc::from_bytes(*b"dac3"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"ec-3"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"dec3"))); } #[test] @@ -192,3 +245,40 @@ fn dac3_rejects_out_of_range_packed_field_values_when_encoding() { "numeric value does not fit field Bsid with width 5" ); } + +#[test] +fn dec3_rejects_non_zero_reserved_bits_when_decoding() { + let mut decoded = Dec3::default(); + let error = unmarshal( + &mut Cursor::new(vec![0x0e, 0x00, 0xa1, 0x08, 0x00]), + 5, + &mut decoded, + None, + ) + .unwrap_err(); + assert_eq!( + error.to_string(), + "invalid field value for EC3Subs: substream reserved bit is not zero" + ); +} + +#[test] +fn dec3_rejects_num_ind_sub_mismatch_during_marshal() { + let dec3 = Dec3 { + data_rate: 448, + num_ind_sub: 1, + ec3_substreams: vec![Ec3Substream { + fscod: 2, + bsid: 0x10, + acmod: 4, + ..Ec3Substream::default() + }], + reserved: Vec::new(), + }; + + let error = marshal(&mut Vec::new(), &dec3, None).unwrap_err(); + assert_eq!( + error.to_string(), + "invalid field value for EC3Subs: num_ind_sub does not match the parsed substream count" + ); +} diff --git a/tests/box_catalog_etsi_ts_103_190.rs b/tests/box_catalog_etsi_ts_103_190.rs new file mode 100644 index 0000000..9093fbf --- /dev/null +++ b/tests/box_catalog_etsi_ts_103_190.rs @@ -0,0 +1,163 @@ +use std::any::type_name; +use std::fmt::Debug; +use std::io::Cursor; + +use mp4forge::FourCc; +use mp4forge::boxes::etsi_ts_103_190::Dac4; +use mp4forge::boxes::iso14496_12::{AudioSampleEntry, SampleEntry}; +use mp4forge::boxes::{AnyTypeBox, default_registry}; +use mp4forge::codec::{CodecBox, marshal, unmarshal, unmarshal_any}; +use mp4forge::stringify::stringify; + +fn assert_box_roundtrip(src: T, payload: &[u8], expected: &str) +where + T: CodecBox + Default + PartialEq + Debug + 'static, +{ + let mut encoded = Vec::new(); + let written = marshal(&mut encoded, &src, None).unwrap(); + assert_eq!( + written, + payload.len() as u64, + "marshal length for {}", + type_name::() + ); + assert_eq!(encoded, payload, "marshal bytes for {}", type_name::()); + + let mut decoded = T::default(); + let mut reader = Cursor::new(payload.to_vec()); + let read = unmarshal(&mut reader, payload.len() as u64, &mut decoded, None).unwrap(); + assert_eq!( + read, + payload.len() as u64, + "unmarshal length for {}", + type_name::() + ); + assert_eq!(decoded, src, "unmarshal value for {}", type_name::()); + + let registry = default_registry(); + let mut any_reader = Cursor::new(payload.to_vec()); + let (any_box, any_read) = unmarshal_any( + &mut any_reader, + payload.len() as u64, + src.box_type(), + ®istry, + None, + ) + .unwrap(); + assert_eq!( + any_read, + payload.len() as u64, + "registry unmarshal length for {}", + type_name::() + ); + assert_eq!(any_box.as_any().downcast_ref::().unwrap(), &src); + + assert_eq!(stringify(&src, None).unwrap(), expected); +} + +fn assert_any_box_roundtrip(src: T, payload: &[u8], expected: &str) +where + T: CodecBox + AnyTypeBox + Default + PartialEq + Debug + 'static, +{ + let mut encoded = Vec::new(); + let written = marshal(&mut encoded, &src, None).unwrap(); + assert_eq!( + written, + payload.len() as u64, + "marshal length for {}", + type_name::() + ); + assert_eq!(encoded, payload, "marshal bytes for {}", type_name::()); + + let mut decoded = T::default(); + decoded.set_box_type(src.box_type()); + let mut reader = Cursor::new(payload.to_vec()); + let read = unmarshal(&mut reader, payload.len() as u64, &mut decoded, None).unwrap(); + assert_eq!( + read, + payload.len() as u64, + "unmarshal length for {}", + type_name::() + ); + assert_eq!(decoded, src, "unmarshal value for {}", type_name::()); + + let registry = default_registry(); + let mut any_reader = Cursor::new(payload.to_vec()); + let (any_box, any_read) = unmarshal_any( + &mut any_reader, + payload.len() as u64, + src.box_type(), + ®istry, + None, + ) + .unwrap(); + assert_eq!( + any_read, + payload.len() as u64, + "registry unmarshal length for {}", + type_name::() + ); + assert_eq!(any_box.as_any().downcast_ref::().unwrap(), &src); + + assert_eq!(stringify(&src, None).unwrap(), expected); +} + +#[test] +fn etsi_ts_103_190_catalog_roundtrips() { + assert_any_box_roundtrip( + AudioSampleEntry { + sample_entry: SampleEntry { + box_type: FourCc::from_bytes(*b"ac-4"), + data_reference_index: 1, + }, + entry_version: 0, + channel_count: 2, + sample_size: 16, + pre_defined: 0, + sample_rate: 48_000 << 16, + quicktime_data: Vec::new(), + }, + &[ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x01, // + 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x02, // + 0x00, 0x10, // + 0x00, 0x00, // + 0x00, 0x00, // + 0xbb, 0x80, 0x00, 0x00, + ], + "DataReferenceIndex=1 EntryVersion=0 ChannelCount=2 SampleSize=16 PreDefined=0 SampleRate=48000", + ); + + assert_box_roundtrip( + Dac4 { + data: vec![ + 0x22, 0x00, 0x80, 0x01, 0xf4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + }, + &[ + 0x22, 0x00, 0x80, 0x01, 0xf4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + "Data=[0x22, 0x0, 0x80, 0x1, 0xf4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]", + ); +} + +#[test] +fn built_in_registry_reports_supported_versions_for_landed_etsi_ts_103_190_types() { + let registry = default_registry(); + + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"ac-4")), + Some(&[][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"dac4")), + Some(&[][..]) + ); + assert!(registry.is_supported_version(FourCc::from_bytes(*b"ac-4"), 9)); + assert!(registry.is_supported_version(FourCc::from_bytes(*b"dac4"), 9)); + assert!(registry.is_registered(FourCc::from_bytes(*b"ac-4"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"dac4"))); +} diff --git a/tests/box_catalog_flac.rs b/tests/box_catalog_flac.rs new file mode 100644 index 0000000..6dc3c05 --- /dev/null +++ b/tests/box_catalog_flac.rs @@ -0,0 +1,205 @@ +use std::any::type_name; +use std::fmt::Debug; +use std::io::Cursor; + +use mp4forge::FourCc; +use mp4forge::boxes::flac::{DfLa, FlacMetadataBlock}; +use mp4forge::boxes::iso14496_12::{AudioSampleEntry, SampleEntry}; +use mp4forge::boxes::{AnyTypeBox, default_registry}; +use mp4forge::codec::{CodecBox, marshal, unmarshal, unmarshal_any}; +use mp4forge::stringify::stringify; + +fn assert_box_roundtrip(src: T, payload: &[u8], expected: &str) +where + T: CodecBox + Default + PartialEq + Debug + 'static, +{ + let mut encoded = Vec::new(); + let written = marshal(&mut encoded, &src, None).unwrap(); + assert_eq!( + written, + payload.len() as u64, + "marshal length for {}", + type_name::() + ); + assert_eq!(encoded, payload, "marshal bytes for {}", type_name::()); + + let mut decoded = T::default(); + let mut reader = Cursor::new(payload.to_vec()); + let read = unmarshal(&mut reader, payload.len() as u64, &mut decoded, None).unwrap(); + assert_eq!( + read, + payload.len() as u64, + "unmarshal length for {}", + type_name::() + ); + assert_eq!(decoded, src, "unmarshal value for {}", type_name::()); + + let registry = default_registry(); + let mut any_reader = Cursor::new(payload.to_vec()); + let (any_box, any_read) = unmarshal_any( + &mut any_reader, + payload.len() as u64, + src.box_type(), + ®istry, + None, + ) + .unwrap(); + assert_eq!( + any_read, + payload.len() as u64, + "registry unmarshal length for {}", + type_name::() + ); + assert_eq!(any_box.as_any().downcast_ref::().unwrap(), &src); + + assert_eq!(stringify(&src, None).unwrap(), expected); +} + +fn assert_any_box_roundtrip(src: T, payload: &[u8], expected: &str) +where + T: CodecBox + AnyTypeBox + Default + PartialEq + Debug + 'static, +{ + let mut encoded = Vec::new(); + let written = marshal(&mut encoded, &src, None).unwrap(); + assert_eq!( + written, + payload.len() as u64, + "marshal length for {}", + type_name::() + ); + assert_eq!(encoded, payload, "marshal bytes for {}", type_name::()); + + let mut decoded = T::default(); + decoded.set_box_type(src.box_type()); + let mut reader = Cursor::new(payload.to_vec()); + let read = unmarshal(&mut reader, payload.len() as u64, &mut decoded, None).unwrap(); + assert_eq!( + read, + payload.len() as u64, + "unmarshal length for {}", + type_name::() + ); + assert_eq!(decoded, src, "unmarshal value for {}", type_name::()); + + let registry = default_registry(); + let mut any_reader = Cursor::new(payload.to_vec()); + let (any_box, any_read) = unmarshal_any( + &mut any_reader, + payload.len() as u64, + src.box_type(), + ®istry, + None, + ) + .unwrap(); + assert_eq!( + any_read, + payload.len() as u64, + "registry unmarshal length for {}", + type_name::() + ); + assert_eq!(any_box.as_any().downcast_ref::().unwrap(), &src); + + assert_eq!(stringify(&src, None).unwrap(), expected); +} + +#[test] +fn flac_catalog_roundtrips() { + assert_any_box_roundtrip( + AudioSampleEntry { + sample_entry: SampleEntry { + box_type: FourCc::from_bytes(*b"fLaC"), + data_reference_index: 1, + }, + entry_version: 0, + channel_count: 2, + sample_size: 16, + pre_defined: 0, + sample_rate: 48_000 << 16, + quicktime_data: Vec::new(), + }, + &[ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x01, // + 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x02, // + 0x00, 0x10, // + 0x00, 0x00, // + 0x00, 0x00, // + 0xbb, 0x80, 0x00, 0x00, + ], + "DataReferenceIndex=1 EntryVersion=0 ChannelCount=2 SampleSize=16 PreDefined=0 SampleRate=48000", + ); + + let block_data: Vec = (1..=34).collect(); + let mut dfla = DfLa::default(); + dfla.metadata_blocks = vec![FlacMetadataBlock { + last_metadata_block_flag: true, + block_type: 0, + length: 34, + block_data: block_data.clone(), + }]; + assert_box_roundtrip( + dfla, + &[ + 0x00, 0x00, 0x00, 0x00, // + 0x80, 0x00, 0x00, 0x22, // + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, + 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, + ], + "Version=0 Flags=0x000000 MetadataBlocks=[{LastMetadataBlockFlag=true BlockType=0 Length=34 BlockData=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22]}]", + ); +} + +#[test] +fn built_in_registry_reports_supported_versions_for_landed_flac_types() { + let registry = default_registry(); + + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"fLaC")), + Some(&[][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"dfLa")), + Some(&[0][..]) + ); + assert!(registry.is_supported_version(FourCc::from_bytes(*b"fLaC"), 9)); + assert!(registry.is_supported_version(FourCc::from_bytes(*b"dfLa"), 0)); + assert!(!registry.is_supported_version(FourCc::from_bytes(*b"dfLa"), 1)); + assert!(registry.is_registered(FourCc::from_bytes(*b"fLaC"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"dfLa"))); +} + +#[test] +fn dfla_rejects_block_length_mismatch_during_marshal() { + let mut dfla = DfLa::default(); + dfla.metadata_blocks = vec![FlacMetadataBlock { + last_metadata_block_flag: true, + block_type: 0, + length: 34, + block_data: vec![0x01, 0x02, 0x03], + }]; + + let error = marshal(&mut Vec::new(), &dfla, None).unwrap_err(); + assert_eq!( + error.to_string(), + "invalid field value for MetadataBlocks: block length does not match BlockData length" + ); +} + +#[test] +fn dfla_rejects_missing_final_metadata_flag_during_unmarshal() { + let mut decoded = DfLa::default(); + let error = unmarshal( + &mut Cursor::new(vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + 8, + &mut decoded, + None, + ) + .unwrap_err(); + assert_eq!( + error.to_string(), + "invalid field value for MetadataBlocks: final metadata block flag must be set" + ); +} diff --git a/tests/box_catalog_iso14496_12.rs b/tests/box_catalog_iso14496_12.rs index fa4b5d6..4b4063a 100644 --- a/tests/box_catalog_iso14496_12.rs +++ b/tests/box_catalog_iso14496_12.rs @@ -1,24 +1,37 @@ use std::any::type_name; use std::fmt::Debug; use std::io::Cursor; +use std::time::{Duration, UNIX_EPOCH}; use mp4forge::FourCc; use mp4forge::boxes::iso14496_12::{ - AVCDecoderConfiguration, AVCParameterSet, AlternativeStartupEntry, AlternativeStartupEntryL, - AlternativeStartupEntryOpt, AudioSampleEntry, Btrt, Co64, Colr, Cslg, Ctts, CttsEntry, Dinf, - Dref, Edts, Elst, ElstEntry, Emsg, Fiel, Free, Frma, Ftyp, HEVCDecoderConfiguration, HEVCNalu, - HEVCNaluArray, Hdlr, Mdat, Mdhd, Mdia, Mehd, Meta, Mfhd, Mfra, Mfro, Minf, Moof, Moov, Mvex, - Mvhd, Pasp, Saio, Saiz, SampleEntry, Sbgp, SbgpEntry, Schi, Schm, Sdtp, SdtpSampleElem, Sgpd, - Sidx, SidxReference, Sinf, Skip, Smhd, Stbl, Stco, Stsc, StscEntry, Stsd, Stss, Stsz, Stts, - SttsEntry, Styp, TFHD_BASE_DATA_OFFSET_PRESENT, TFHD_DEFAULT_SAMPLE_DURATION_PRESENT, - TRUN_DATA_OFFSET_PRESENT, TRUN_FIRST_SAMPLE_FLAGS_PRESENT, - TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT, TRUN_SAMPLE_DURATION_PRESENT, - TRUN_SAMPLE_SIZE_PRESENT, TemporalLevelEntry, TextSubtitleSampleEntry, Tfdt, Tfhd, Tfra, - TfraEntry, Tkhd, Traf, Trak, Trep, Trex, Trun, TrunEntry, Udta, VisualRandomAccessEntry, - VisualSampleEntry, Vmhd, Wave, XMLSubtitleSampleEntry, + AVCDecoderConfiguration, AVCParameterSet, AlbumLoudnessInfo, AlternativeStartupEntry, + AlternativeStartupEntryL, AlternativeStartupEntryOpt, AudioSampleEntry, Btrt, Cdat, Cdsc, Clap, + Co64, CoLL, Colr, Cslg, Ctts, CttsEntry, Dinf, Dpnd, Dref, Edts, Elng, Elst, ElstEntry, Emeb, + Emib, Emsg, EventMessageSampleEntry, Fiel, Font, Free, Frma, Ftyp, HEVCDecoderConfiguration, + HEVCNalu, HEVCNaluArray, Hdlr, Hind, Hint, Ipir, Kind, Leva, LevaLevel, LoudnessEntry, + LoudnessMeasurement, Ludt, Mdat, Mdhd, Mdia, Mehd, Meta, Mfhd, Mfra, Mfro, Mime, Minf, Moof, + Moov, Mpod, Mvex, Mvhd, Nmhd, PRFT_NTP_UNIX_EPOCH_OFFSET_SECONDS, + PRFT_TIME_ARBITRARY_CONSISTENT, PRFT_TIME_CAPTURED, PRFT_TIME_ENCODER_INPUT, + PRFT_TIME_ENCODER_OUTPUT, PRFT_TIME_MOOF_FINALIZED, PRFT_TIME_MOOF_WRITTEN, Pasp, Prft, Saio, + Saiz, SampleEntry, Sbgp, SbgpEntry, Schi, Schm, Sdtp, SdtpSampleElem, SeigEntry, SeigEntryL, + Sgpd, Sidx, SidxReference, Silb, SilbEntry, Sinf, Skip, SmDm, Smhd, SphericalVideoV1Metadata, + Ssix, SsixRange, SsixSubsegment, Stbl, Stco, Sthd, Stsc, StscEntry, Stsd, Stss, Stsz, Stts, + SttsEntry, Styp, Subs, SubsEntry, SubsSample, Subt, Sync, TFHD_BASE_DATA_OFFSET_PRESENT, + TFHD_DEFAULT_SAMPLE_DURATION_PRESENT, TRUN_DATA_OFFSET_PRESENT, + TRUN_FIRST_SAMPLE_FLAGS_PRESENT, TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT, + TRUN_SAMPLE_DURATION_PRESENT, TRUN_SAMPLE_SIZE_PRESENT, TemporalLevelEntry, + TextSubtitleSampleEntry, Tfdt, Tfhd, Tfra, TfraEntry, Tkhd, TrackLoudnessInfo, Traf, Trak, + Tref, Trep, Trex, Trun, TrunEntry, UUID_FRAGMENT_ABSOLUTE_TIMING, UUID_FRAGMENT_RUN_TABLE, + UUID_SAMPLE_ENCRYPTION, UUID_SPHERICAL_VIDEO_V1, Udta, Uuid, UuidFragmentAbsoluteTiming, + UuidFragmentRunEntry, UuidFragmentRunTable, UuidPayload, Vdep, VisualRandomAccessEntry, + VisualSampleEntry, Vmhd, Vplx, Wave, XMLSubtitleSampleEntry, }; +use mp4forge::boxes::iso23001_7::{SENC_USE_SUBSAMPLE_ENCRYPTION, Senc, SencSample, SencSubsample}; use mp4forge::boxes::{AnyTypeBox, default_registry}; -use mp4forge::codec::{CodecBox, ImmutableBox, MutableBox, marshal, unmarshal, unmarshal_any}; +use mp4forge::codec::{ + CodecBox, CodecError, ImmutableBox, MutableBox, marshal, unmarshal, unmarshal_any, +}; use mp4forge::stringify::stringify; fn assert_box_roundtrip(src: T, payload: &[u8], expected: &str) @@ -230,6 +243,20 @@ fn core_iso14496_12_catalog_roundtrips() { mfro.set_version(0); mfro.size = 0x12345678; + let mut prft_v0 = Prft::default(); + prft_v0.set_version(0); + prft_v0.set_flags(0x000001); + prft_v0.reference_track_id = 0x12345678; + prft_v0.ntp_timestamp = 0x0000000102030405; + prft_v0.media_time_v0 = 0x23456789; + + let mut prft_v1 = Prft::default(); + prft_v1.set_version(1); + prft_v1.set_flags(0x000018); + prft_v1.reference_track_id = 0x89abcdef; + prft_v1.ntp_timestamp = 0x000000060708090a; + prft_v1.media_time_v1 = 0x0000000b0c0d0e0f; + let mut mvhd_v0 = Mvhd::default(); mvhd_v0.set_version(0); mvhd_v0.creation_time_v0 = 0x01234567; @@ -254,6 +281,12 @@ fn core_iso14496_12_catalog_roundtrips() { smhd.set_version(0); smhd.balance = 0x0123; + let mut sthd = Sthd::default(); + sthd.set_version(0); + + let mut nmhd = Nmhd::default(); + nmhd.set_version(0); + let mut stco = Stco::default(); stco.set_version(0); stco.entry_count = 2; @@ -490,7 +523,50 @@ fn core_iso14496_12_catalog_roundtrips() { assert_box_roundtrip(Stbl, &[], ""); assert_box_roundtrip(Traf, &[], ""); assert_box_roundtrip(Trak, &[], ""); + assert_box_roundtrip(Tref, &[], ""); assert_box_roundtrip(Udta, &[], ""); + macro_rules! assert_tref_child_roundtrip { + ($value:expr) => { + assert_box_roundtrip( + $value, + &[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef], + "TrackIDs=[19088743, 2309737967]", + ); + }; + } + assert_tref_child_roundtrip!(Cdsc { + track_ids: vec![0x01234567, 0x89abcdef], + }); + assert_tref_child_roundtrip!(Dpnd { + track_ids: vec![0x01234567, 0x89abcdef], + }); + assert_tref_child_roundtrip!(Font { + track_ids: vec![0x01234567, 0x89abcdef], + }); + assert_tref_child_roundtrip!(Hind { + track_ids: vec![0x01234567, 0x89abcdef], + }); + assert_tref_child_roundtrip!(Hint { + track_ids: vec![0x01234567, 0x89abcdef], + }); + assert_tref_child_roundtrip!(Ipir { + track_ids: vec![0x01234567, 0x89abcdef], + }); + assert_tref_child_roundtrip!(Mpod { + track_ids: vec![0x01234567, 0x89abcdef], + }); + assert_tref_child_roundtrip!(Subt { + track_ids: vec![0x01234567, 0x89abcdef], + }); + assert_tref_child_roundtrip!(Sync { + track_ids: vec![0x01234567, 0x89abcdef], + }); + assert_tref_child_roundtrip!(Vdep { + track_ids: vec![0x01234567, 0x89abcdef], + }); + assert_tref_child_roundtrip!(Vplx { + track_ids: vec![0x01234567, 0x89abcdef], + }); assert_box_roundtrip( dref, &[0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78], @@ -561,6 +637,22 @@ fn core_iso14496_12_catalog_roundtrips() { &[0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78], "Version=0 Flags=0x000000 Size=305419896", ); + assert_box_roundtrip( + prft_v0, + &[ + 0x00, 0x00, 0x00, 0x01, 0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, + 0x04, 0x05, 0x23, 0x45, 0x67, 0x89, + ], + "Version=0 Flags=0x000001 ReferenceTrackID=305419896 NTPTimestamp=4328719365 MediaTimeV0=591751049", + ); + assert_box_roundtrip( + prft_v1, + &[ + 0x01, 0x00, 0x00, 0x18, 0x89, 0xab, 0xcd, 0xef, 0x00, 0x00, 0x00, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x00, 0x00, 0x00, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + ], + "Version=1 Flags=0x000018 ReferenceTrackID=2309737967 NTPTimestamp=25887770890 MediaTimeV1=47446822415", + ); assert_box_roundtrip( mdhd_v0, &[ @@ -606,6 +698,8 @@ fn core_iso14496_12_catalog_roundtrips() { &[0x00, 0x00, 0x00, 0x00, 0x01, 0x23, 0x00, 0x00], "Version=0 Flags=0x000000 Balance=1.137", ); + assert_box_roundtrip(sthd, &[0x00, 0x00, 0x00, 0x00], "Version=0 Flags=0x000000"); + assert_box_roundtrip(nmhd, &[0x00, 0x00, 0x00, 0x00], "Version=0 Flags=0x000000"); assert_box_roundtrip( stco, &[ @@ -884,6 +978,14 @@ fn additional_iso14496_12_catalog_roundtrips() { num_total_samples: 0xcdef, }, ]; + let seig_kid_a = [ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, + 0xef, + ]; + let seig_kid_b = [ + 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe, 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, + 0xfe, + ]; let mut sgpd_roll_v1 = Sgpd::default(); sgpd_roll_v1.set_version(1); @@ -1002,6 +1104,94 @@ fn additional_iso14496_12_catalog_roundtrips() { sgpd_roll_v2.entry_count = 2; sgpd_roll_v2.roll_distances = vec![0x1111, 0x2222]; + let mut sgpd_seig_v1 = Sgpd::default(); + sgpd_seig_v1.set_version(1); + sgpd_seig_v1.grouping_type = FourCc::from_bytes(*b"seig"); + sgpd_seig_v1.default_length = 20; + sgpd_seig_v1.entry_count = 2; + sgpd_seig_v1.seig_entries = vec![ + SeigEntry { + crypt_byte_block: 0x0a, + skip_byte_block: 0x0b, + is_protected: 1, + per_sample_iv_size: 8, + kid: seig_kid_a, + ..SeigEntry::default() + }, + SeigEntry { + reserved: 2, + crypt_byte_block: 0x01, + skip_byte_block: 0x02, + kid: seig_kid_b, + ..SeigEntry::default() + }, + ]; + + let mut sgpd_seig_len_v1 = Sgpd::default(); + sgpd_seig_len_v1.set_version(1); + sgpd_seig_len_v1.grouping_type = FourCc::from_bytes(*b"seig"); + sgpd_seig_len_v1.default_length = 0; + sgpd_seig_len_v1.entry_count = 2; + sgpd_seig_len_v1.seig_entries_l = vec![ + SeigEntryL { + description_length: 25, + seig_entry: SeigEntry { + reserved: 3, + crypt_byte_block: 0x04, + skip_byte_block: 0x05, + is_protected: 1, + kid: seig_kid_a, + constant_iv_size: 4, + constant_iv: vec![0x01, 0x23, 0x45, 0x67], + ..SeigEntry::default() + }, + }, + SeigEntryL { + description_length: 20, + seig_entry: SeigEntry { + reserved: 1, + crypt_byte_block: 0x06, + skip_byte_block: 0x07, + is_protected: 1, + per_sample_iv_size: 8, + kid: seig_kid_b, + ..SeigEntry::default() + }, + }, + ]; + + let mut sgpd_seig_v2 = Sgpd::default(); + sgpd_seig_v2.set_version(2); + sgpd_seig_v2.grouping_type = FourCc::from_bytes(*b"seig"); + sgpd_seig_v2.default_sample_description_index = 5; + sgpd_seig_v2.entry_count = 2; + sgpd_seig_v2.seig_entries = vec![ + SeigEntry { + reserved: 3, + crypt_byte_block: 0x04, + skip_byte_block: 0x05, + is_protected: 1, + kid: seig_kid_a, + constant_iv_size: 4, + constant_iv: vec![0x01, 0x23, 0x45, 0x67], + ..SeigEntry::default() + }, + SeigEntry { + reserved: 2, + crypt_byte_block: 0x01, + skip_byte_block: 0x02, + kid: seig_kid_b, + ..SeigEntry::default() + }, + ]; + + let mut sgpd_unknown_v1 = Sgpd::default(); + sgpd_unknown_v1.set_version(1); + sgpd_unknown_v1.grouping_type = FourCc::from_bytes(*b"unkn"); + sgpd_unknown_v1.default_length = 3; + sgpd_unknown_v1.entry_count = 2; + sgpd_unknown_v1.unsupported = vec![0xaa, 0xbb, 0xcc, 0x11, 0x22, 0x33]; + let mut sidx_v0 = Sidx::default(); sidx_v0.set_version(0); sidx_v0.reference_id = 0x01234567; @@ -1238,6 +1428,46 @@ fn additional_iso14496_12_catalog_roundtrips() { ], "Version=2 Flags=0x000000 GroupingType=\"roll\" DefaultSampleDescriptionIndex=5 EntryCount=2 RollDistances=[4369, 8738]", ); + assert_box_roundtrip( + sgpd_seig_v1, + &[ + 0x01, 0x00, 0x00, 0x00, b's', b'e', b'i', b'g', 0x00, 0x00, 0x00, 0x14, 0x00, 0x00, + 0x00, 0x02, 0x00, 0xab, 0x01, 0x08, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x02, 0x12, 0x00, 0x00, 0x10, 0x32, + 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe, 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe, + ], + "Version=1 Flags=0x000000 GroupingType=\"seig\" DefaultLength=20 EntryCount=2 SeigEntries=[{Reserved=0 CryptByteBlock=10 SkipByteBlock=11 IsProtected=1 PerSampleIVSize=8 KID=01234567-89ab-cdef-0123-456789abcdef}, {Reserved=2 CryptByteBlock=1 SkipByteBlock=2 IsProtected=0 PerSampleIVSize=0 KID=10325476-98ba-dcfe-1032-547698badcfe}]", + ); + assert_box_roundtrip( + sgpd_seig_len_v1, + &[ + 0x01, 0x00, 0x00, 0x00, b's', b'e', b'i', b'g', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x19, 0x03, 0x45, 0x01, 0x00, 0x01, 0x23, 0x45, 0x67, + 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x04, 0x01, + 0x23, 0x45, 0x67, 0x00, 0x00, 0x00, 0x14, 0x01, 0x67, 0x01, 0x08, 0x10, 0x32, 0x54, + 0x76, 0x98, 0xba, 0xdc, 0xfe, 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe, + ], + "Version=1 Flags=0x000000 GroupingType=\"seig\" DefaultLength=0 EntryCount=2 SeigEntriesL=[{DescriptionLength=25 Reserved=3 CryptByteBlock=4 SkipByteBlock=5 IsProtected=1 PerSampleIVSize=0 KID=01234567-89ab-cdef-0123-456789abcdef ConstantIVSize=4 ConstantIV=[0x1, 0x23, 0x45, 0x67]}, {DescriptionLength=20 Reserved=1 CryptByteBlock=6 SkipByteBlock=7 IsProtected=1 PerSampleIVSize=8 KID=10325476-98ba-dcfe-1032-547698badcfe}]", + ); + assert_box_roundtrip( + sgpd_seig_v2, + &[ + 0x02, 0x00, 0x00, 0x00, b's', b'e', b'i', b'g', 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, + 0x00, 0x02, 0x03, 0x45, 0x01, 0x00, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x04, 0x01, 0x23, 0x45, 0x67, 0x02, + 0x12, 0x00, 0x00, 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe, 0x10, 0x32, 0x54, + 0x76, 0x98, 0xba, 0xdc, 0xfe, + ], + "Version=2 Flags=0x000000 GroupingType=\"seig\" DefaultSampleDescriptionIndex=5 EntryCount=2 SeigEntries=[{Reserved=3 CryptByteBlock=4 SkipByteBlock=5 IsProtected=1 PerSampleIVSize=0 KID=01234567-89ab-cdef-0123-456789abcdef ConstantIVSize=4 ConstantIV=[0x1, 0x23, 0x45, 0x67]}, {Reserved=2 CryptByteBlock=1 SkipByteBlock=2 IsProtected=0 PerSampleIVSize=0 KID=10325476-98ba-dcfe-1032-547698badcfe}]", + ); + assert_box_roundtrip( + sgpd_unknown_v1, + &[ + 0x01, 0x00, 0x00, 0x00, b'u', b'n', b'k', b'n', 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x02, 0xaa, 0xbb, 0xcc, 0x11, 0x22, 0x33, + ], + "Version=1 Flags=0x000000 GroupingType=\"unkn\" DefaultLength=3 EntryCount=2 Unsupported=[0xaa, 0xbb, 0xcc, 0x11, 0x22, 0x33]", + ); assert_box_roundtrip( sidx_v0, &[ @@ -1538,6 +1768,39 @@ fn sample_entry_and_leaf_iso14496_12_catalog_roundtrips() { mime_format: String::from("bar/baz"), }; + let evte = EventMessageSampleEntry { + sample_entry: SampleEntry { + box_type: FourCc::from_bytes(*b"evte"), + data_reference_index: 0x1234, + }, + }; + + let mut silb = Silb::default(); + silb.set_version(0); + silb.scheme_count = 2; + silb.schemes = vec![ + SilbEntry { + scheme_id_uri: String::from("urn:test"), + value: String::from("one"), + at_least_one_flag: false, + }, + SilbEntry { + scheme_id_uri: String::from("urn:alt"), + value: String::from("two"), + at_least_one_flag: true, + }, + ]; + silb.other_schemes_flag = true; + + let mut emib = Emib::default(); + emib.set_version(0); + emib.presentation_time_delta = -1_000; + emib.event_duration = 2_000; + emib.id = 0x1234; + emib.scheme_id_uri = String::from("urn:test"); + emib.value = String::from("2"); + emib.message_data = b"abc".to_vec(); + assert_box_roundtrip( Btrt { buffer_size_db: 0x12345678, @@ -1549,6 +1812,35 @@ fn sample_entry_and_leaf_iso14496_12_catalog_roundtrips() { ], "BufferSizeDB=305419896 MaxBitrate=878082202 AvgBitrate=1450744508", ); + assert_box_roundtrip( + Clap { + clean_aperture_width_n: 0x01234567, + clean_aperture_width_d: 0x89abcdef, + clean_aperture_height_n: 0x10203040, + clean_aperture_height_d: 0x50607080, + horiz_off_n: 0x11223344, + horiz_off_d: 0x55667788, + vert_off_n: 0x99aabbcc, + vert_off_d: 0xddeeff00, + }, + &[ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, + 0x70, 0x80, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, + 0xdd, 0xee, 0xff, 0x00, + ], + "CleanApertureWidthN=19088743 CleanApertureWidthD=2309737967 CleanApertureHeightN=270544960 CleanApertureHeightD=1348497536 HorizOffN=287454020 HorizOffD=1432778632 VertOffN=2578103244 VertOffD=3723427584", + ); + assert_box_roundtrip( + { + let mut coll = CoLL::default(); + coll.set_version(0); + coll.max_cll = 0x1234; + coll.max_fall = 0x5678; + coll + }, + &[0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78], + "Version=0 Flags=0x000000 MaxCLL=4660 MaxFALL=22136", + ); assert_box_roundtrip( Colr { colour_type: FourCc::from_bytes(*b"nclx"), @@ -1623,6 +1915,28 @@ fn sample_entry_and_leaf_iso14496_12_catalog_roundtrips() { &[0x01, 0x23, 0x45, 0x67, 0x23, 0x45, 0x67, 0x89], "HSpacing=19088743 VSpacing=591751049", ); + assert_box_roundtrip( + { + let mut smdm = SmDm::default(); + smdm.set_version(0); + smdm.primary_r_chromaticity_x = 0x0123; + smdm.primary_r_chromaticity_y = 0x2345; + smdm.primary_g_chromaticity_x = 0x4567; + smdm.primary_g_chromaticity_y = 0x6789; + smdm.primary_b_chromaticity_x = 0x89ab; + smdm.primary_b_chromaticity_y = 0xabcd; + smdm.white_point_chromaticity_x = 0xcdef; + smdm.white_point_chromaticity_y = 0x1357; + smdm.luminance_max = 0x89abcdef; + smdm.luminance_min = 0x10203040; + smdm + }, + &[ + 0x00, 0x00, 0x00, 0x00, 0x01, 0x23, 0x23, 0x45, 0x45, 0x67, 0x67, 0x89, 0x89, 0xab, + 0xab, 0xcd, 0xcd, 0xef, 0x13, 0x57, 0x89, 0xab, 0xcd, 0xef, 0x10, 0x20, 0x30, 0x40, + ], + "Version=0 Flags=0x000000 PrimaryRChromaticityX=291 PrimaryRChromaticityY=9029 PrimaryGChromaticityX=17767 PrimaryGChromaticityY=26505 PrimaryBChromaticityX=35243 PrimaryBChromaticityY=43981 WhitePointChromaticityX=52719 WhitePointChromaticityY=4951 LuminanceMax=2309737967 LuminanceMin=270544960", + ); assert_box_roundtrip( schm, &[ @@ -1710,6 +2024,30 @@ fn sample_entry_and_leaf_iso14496_12_catalog_roundtrips() { ], "DataReferenceIndex=4660 ContentEncoding=\"foo\" MIMEFormat=\"bar/baz\"", ); + assert_box_roundtrip( + evte, + &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34], + "DataReferenceIndex=4660", + ); + assert_box_roundtrip( + silb, + &[ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, b'u', b'r', b'n', b':', b't', b'e', + b's', b't', 0x00, b'o', b'n', b'e', 0x00, 0x00, b'u', b'r', b'n', b':', b'a', b'l', + b't', 0x00, b't', b'w', b'o', 0x00, 0x01, 0x01, + ], + "Version=0 Flags=0x000000 SchemeCount=2 Schemes=[{SchemeIdUri=\"urn:test\" Value=\"one\" AtLeastOneFlag=false}, {SchemeIdUri=\"urn:alt\" Value=\"two\" AtLeastOneFlag=true}] OtherSchemesFlag=true", + ); + assert_box_roundtrip( + emib, + &[ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xfc, 0x18, 0x00, 0x00, 0x07, 0xd0, 0x00, 0x00, 0x12, 0x34, b'u', b'r', b'n', b':', + b't', b'e', b's', b't', 0x00, b'2', 0x00, b'a', b'b', b'c', + ], + "Version=0 Flags=0x000000 PresentationTimeDelta=-1000 EventDuration=2000 Id=4660 SchemeIdUri=\"urn:test\" Value=\"2\" MessageData=\"abc\"", + ); + assert_box_roundtrip(Emeb, &[], ""); assert_any_box_roundtrip( visual, &[ @@ -1754,7 +2092,165 @@ fn sample_entry_and_leaf_iso14496_12_catalog_roundtrips() { } #[test] -fn irregular_decode_helpers_match_reference_behavior() { +fn compact_metadata_iso14496_12_catalog_roundtrips() { + let mut elng = Elng::default(); + elng.extended_language = "en-US".into(); + + let mut subs = Subs::default(); + subs.entry_count = 1; + subs.entries = vec![SubsEntry { + sample_delta: 100, + subsample_count: 1, + subsamples: vec![SubsSample { + subsample_size: 0x1234, + subsample_priority: 2, + discardable: 1, + codec_specific_parameters: 0x12345678, + }], + }]; + + let mut ssix = Ssix::default(); + ssix.subsegment_count = 2; + ssix.subsegments = vec![ + SsixSubsegment { + range_count: 1, + ranges: vec![SsixRange { + level: 2, + range_size: 0x012345, + }], + }, + SsixSubsegment { + range_count: 2, + ranges: vec![ + SsixRange { + level: 4, + range_size: 0x10, + }, + SsixRange { + level: 6, + range_size: 0x00ab_cdef, + }, + ], + }, + ]; + + let mut leva = Leva::default(); + leva.level_count = 4; + leva.levels = vec![ + LevaLevel { + track_id: 1, + padding_flag: true, + assignment_type: 0, + grouping_type: u32::from_be_bytes(*b"roll"), + ..LevaLevel::default() + }, + LevaLevel { + track_id: 2, + assignment_type: 1, + grouping_type: u32::from_be_bytes(*b"tele"), + grouping_type_parameter: 9, + ..LevaLevel::default() + }, + LevaLevel { + track_id: 3, + assignment_type: 4, + sub_track_id: 17, + ..LevaLevel::default() + }, + LevaLevel { + track_id: 4, + padding_flag: true, + assignment_type: 3, + ..LevaLevel::default() + }, + ]; + + assert_box_roundtrip( + elng, + &[0x00, 0x00, 0x00, 0x00, b'e', b'n', b'-', b'U', b'S', 0x00], + "Version=0 Flags=0x000000 ExtendedLanguage=\"en-US\"", + ); + assert_box_roundtrip( + subs, + &[ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x01, + 0x12, 0x34, 0x02, 0x01, 0x12, 0x34, 0x56, 0x78, + ], + "Version=0 Flags=0x000000 EntryCount=1 Entries=[{SampleDelta=100 SubsampleCount=1 Subsamples=[{SubsampleSize=4660 SubsamplePriority=2 Discardable=1 CodecSpecificParameters=305419896}]}]", + ); + assert_box_roundtrip( + ssix, + &[ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x02, 0x01, + 0x23, 0x45, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x00, 0x10, 0x06, 0xab, 0xcd, 0xef, + ], + "Version=0 Flags=0x000000 SubsegmentCount=2 Subsegments=[{RangeCount=1 Ranges=[{Level=2 RangeSize=74565}]}, {RangeCount=2 Ranges=[{Level=4 RangeSize=16}, {Level=6 RangeSize=11259375}]}]", + ); + assert_box_roundtrip( + leva, + &[ + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x80, b'r', b'o', b'l', b'l', + 0x00, 0x00, 0x00, 0x02, 0x01, b't', b'e', b'l', b'e', 0x00, 0x00, 0x00, 0x09, 0x00, + 0x00, 0x00, 0x03, 0x04, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x04, 0x83, + ], + "Version=0 Flags=0x000000 LevelCount=4 Levels=[{TrackID=1 PaddingFlag=true AssignmentType=0 GroupingType=0x726f6c6c}, {TrackID=2 PaddingFlag=false AssignmentType=1 GroupingType=0x74656c65 GroupingTypeParameter=9}, {TrackID=3 PaddingFlag=false AssignmentType=4 SubTrackID=17}, {TrackID=4 PaddingFlag=true AssignmentType=3}]", + ); +} + +#[test] +fn compact_track_payload_metadata_iso14496_12_catalog_roundtrips() { + let mut kind = Kind::default(); + kind.set_version(0); + kind.set_flags(0x000005); + kind.scheme_uri = "urn:test".into(); + kind.value = "main".into(); + + let mut mime = Mime::default(); + mime.set_version(0); + mime.set_flags(0x000006); + mime.content_type = "text/plain".into(); + + let mut mime_without_zero = Mime::default(); + mime_without_zero.set_version(0); + mime_without_zero.set_flags(0x000001); + mime_without_zero.content_type = "application/ttml+xml".into(); + mime_without_zero.lacks_zero_termination = true; + + assert_box_roundtrip( + kind, + &[ + 0x00, 0x00, 0x00, 0x05, b'u', b'r', b'n', b':', b't', b'e', b's', b't', 0x00, b'm', + b'a', b'i', b'n', 0x00, + ], + "Version=0 Flags=0x000005 SchemeURI=\"urn:test\" Value=\"main\"", + ); + assert_box_roundtrip( + mime, + &[ + 0x00, 0x00, 0x00, 0x06, b't', b'e', b'x', b't', b'/', b'p', b'l', b'a', b'i', b'n', + 0x00, + ], + "Version=0 Flags=0x000006 ContentType=\"text/plain\"", + ); + assert_box_roundtrip( + mime_without_zero, + &[ + 0x00, 0x00, 0x00, 0x01, b'a', b'p', b'p', b'l', b'i', b'c', b'a', b't', b'i', b'o', + b'n', b'/', b't', b't', b'm', b'l', b'+', b'x', b'm', b'l', + ], + "Version=0 Flags=0x000001 ContentType=\"application/ttml+xml\" LacksZeroTermination=true", + ); + assert_box_roundtrip( + Cdat { + data: vec![0xde, 0xad, 0xbe, 0xef], + }, + &[0xde, 0xad, 0xbe, 0xef], + "Data=[0xde, 0xad, 0xbe, 0xef]", + ); +} + +#[test] +fn irregular_decode_helpers_match_expected_behavior() { let handler_cases = [ ([0x00, 0x00, 0x00, 0x00], b"abema".as_slice(), "abema"), ([0x00, 0x00, 0x00, 0x00], b"".as_slice(), ""), @@ -1803,6 +2299,25 @@ fn irregular_decode_helpers_match_reference_behavior() { assert_eq!(meta.flags(), 0); } +#[test] +fn elng_preserves_payloads_without_full_box_header_bytes() { + let payload = [b'd', b'k', 0x00]; + let mut decoded = Elng::default(); + let mut reader = Cursor::new(payload.to_vec()); + let read = unmarshal(&mut reader, payload.len() as u64, &mut decoded, None).unwrap(); + assert_eq!(read, payload.len() as u64); + assert_eq!(decoded.extended_language, "dk"); + assert_eq!( + stringify(&decoded, None).unwrap(), + "Version=0 Flags=0x000000 ExtendedLanguage=\"dk\"" + ); + + let mut encoded = Vec::new(); + let written = marshal(&mut encoded, &decoded, None).unwrap(); + assert_eq!(written, payload.len() as u64); + assert_eq!(encoded, payload); +} + #[test] fn counted_payload_validation_rejects_truncated_sbgp_entries() { let payload = [ @@ -1818,6 +2333,592 @@ fn counted_payload_validation_rejects_truncated_sbgp_entries() { ); } +#[test] +fn compact_metadata_validation_rejects_malformed_payloads() { + let subs_payload = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x01, 0x12, + 0x34, 0x02, + ]; + let mut subs = Subs::default(); + let mut subs_reader = Cursor::new(subs_payload); + let subs_error = + unmarshal(&mut subs_reader, subs_payload.len() as u64, &mut subs, None).unwrap_err(); + assert_eq!( + subs_error.to_string(), + "invalid field value for Entries: subsample payload is truncated" + ); + + let mut ssix = Ssix::default(); + ssix.subsegment_count = 1; + ssix.subsegments = vec![SsixSubsegment { + range_count: 1, + ranges: vec![SsixRange { + level: 1, + range_size: 0x01ff_ffff, + }], + }]; + let ssix_error = marshal(&mut Vec::new(), &ssix, None).unwrap_err(); + assert_eq!( + ssix_error.to_string(), + "invalid field value for Subsegments: range size does not fit in 24 bits" + ); + + let leva_payload = [0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05]; + let mut leva = Leva::default(); + let mut leva_reader = Cursor::new(leva_payload); + let leva_error = + unmarshal(&mut leva_reader, leva_payload.len() as u64, &mut leva, None).unwrap_err(); + assert_eq!( + leva_error.to_string(), + "invalid field value for Levels: assignment type uses a reserved layout" + ); + + let kind_payload = [ + 0x00, 0x00, 0x00, 0x00, b'u', b'r', b'n', b':', b't', b'e', b's', b't', 0x00, b'm', b'a', + b'i', b'n', + ]; + let mut kind = Kind::default(); + let mut kind_reader = Cursor::new(kind_payload); + let kind_error = + unmarshal(&mut kind_reader, kind_payload.len() as u64, &mut kind, None).unwrap_err(); + assert_eq!( + kind_error.to_string(), + "invalid field value for Value: string is not NUL-terminated" + ); + + let mime_payload = [0x00, 0x00, 0x00, 0x00]; + let mut mime = Mime::default(); + let mut mime_reader = Cursor::new(mime_payload); + let mime_error = + unmarshal(&mut mime_reader, mime_payload.len() as u64, &mut mime, None).unwrap_err(); + assert_eq!( + mime_error.to_string(), + "invalid field value for Payload: payload is too short" + ); + + let mut non_terminated_empty_mime = Mime::default(); + non_terminated_empty_mime.set_version(0); + non_terminated_empty_mime.lacks_zero_termination = true; + let mime_encode_error = marshal(&mut Vec::new(), &non_terminated_empty_mime, None).unwrap_err(); + assert_eq!( + mime_encode_error.to_string(), + "invalid field value for ContentType: non-terminated payload must not be empty" + ); +} + +#[test] +fn loudness_and_uuid_boxes_roundtrip() { + assert_box_roundtrip(Ludt, &[], ""); + + let mut tlou = TrackLoudnessInfo::default(); + tlou.set_version(1); + tlou.entries = vec![LoudnessEntry { + eq_set_id: 7, + downmix_id: 12, + drc_set_id: 18, + bs_sample_peak_level: 528, + bs_true_peak_level: 801, + measurement_system_for_tp: 4, + reliability_for_tp: 6, + measurements: vec![ + LoudnessMeasurement { + method_definition: 7, + method_value: 8, + measurement_system: 9, + reliability: 10, + }, + LoudnessMeasurement { + method_definition: 11, + method_value: 12, + measurement_system: 13, + reliability: 14, + }, + ], + }]; + assert_box_roundtrip( + tlou, + &[ + 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x03, 0x12, 0x21, 0x03, 0x21, 0x46, 0x02, 0x07, + 0x08, 0x9a, 0x0b, 0x0c, 0xde, + ], + "Version=1 Flags=0x000000 Entries=[{EQSetID=7 DownmixID=12 DRCSetID=18 BsSamplePeakLevel=528 BsTruePeakLevel=801 MeasurementSystemForTP=4 ReliabilityForTP=6 Measurements=[{MethodDefinition=7 MethodValue=8 MeasurementSystem=9 Reliability=10}, {MethodDefinition=11 MethodValue=12 MeasurementSystem=13 Reliability=14}]}]", + ); + + let mut alou = AlbumLoudnessInfo::default(); + alou.set_version(0); + alou.entries = vec![LoudnessEntry { + downmix_id: 9, + drc_set_id: 17, + bs_sample_peak_level: 274, + bs_true_peak_level: 291, + measurement_system_for_tp: 2, + reliability_for_tp: 3, + measurements: vec![LoudnessMeasurement { + method_definition: 1, + method_value: 2, + measurement_system: 4, + reliability: 5, + }], + ..LoudnessEntry::default() + }]; + assert_box_roundtrip( + alou, + &[ + 0x00, 0x00, 0x00, 0x00, 0x02, 0x51, 0x11, 0x21, 0x23, 0x23, 0x01, 0x01, 0x02, 0x45, + ], + "Version=0 Flags=0x000000 Entries=[{DownmixID=9 DRCSetID=17 BsSamplePeakLevel=274 BsTruePeakLevel=291 MeasurementSystemForTP=2 ReliabilityForTP=3 Measurements=[{MethodDefinition=1 MethodValue=2 MeasurementSystem=4 Reliability=5}]}]", + ); + + assert_box_roundtrip( + Uuid { + user_type: UUID_SPHERICAL_VIDEO_V1, + payload: UuidPayload::SphericalVideoV1(SphericalVideoV1Metadata { + xml_data: b"S".to_vec(), + }), + }, + &[ + 0xff, 0xcc, 0x82, 0x63, 0xf8, 0x55, 0x4a, 0x93, 0x88, 0x14, 0x58, 0x7a, 0x02, 0x52, + 0x1f, 0xdd, 0x3c, 0x72, 0x64, 0x66, 0x3e, 0x53, 0x3c, 0x2f, 0x72, 0x64, 0x66, 0x3e, + ], + "UserType=ffcc8263-f855-4a93-8814-587a02521fdd XMLData=\"S\"", + ); + + assert_box_roundtrip( + Uuid { + user_type: UUID_FRAGMENT_ABSOLUTE_TIMING, + payload: UuidPayload::FragmentAbsoluteTiming(UuidFragmentAbsoluteTiming { + version: 0, + flags: 0, + fragment_absolute_time: 0x1234_5678, + fragment_absolute_duration: 0x9abc_def0, + }), + }, + &[ + 0x6d, 0x1d, 0x9b, 0x05, 0x42, 0xd5, 0x44, 0xe6, 0x80, 0xe2, 0x14, 0x1d, 0xaf, 0xf7, + 0x57, 0xb2, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, + ], + "UserType=6d1d9b05-42d5-44e6-80e2-141daff757b2 Version=0 Flags=0x000000 FragmentAbsoluteTime=305419896 FragmentAbsoluteDuration=2596069104", + ); + + assert_box_roundtrip( + Uuid { + user_type: UUID_FRAGMENT_ABSOLUTE_TIMING, + payload: UuidPayload::FragmentAbsoluteTiming(UuidFragmentAbsoluteTiming { + version: 1, + flags: 0, + fragment_absolute_time: 0x0001_05c6_49bd_a400, + fragment_absolute_duration: 0x0000_0000_0005_4600, + }), + }, + &[ + 0x6d, 0x1d, 0x9b, 0x05, 0x42, 0xd5, 0x44, 0xe6, 0x80, 0xe2, 0x14, 0x1d, 0xaf, 0xf7, + 0x57, 0xb2, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x05, 0xc6, 0x49, 0xbd, 0xa4, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x46, 0x00, + ], + "UserType=6d1d9b05-42d5-44e6-80e2-141daff757b2 Version=1 Flags=0x000000 FragmentAbsoluteTime=287824175539200 FragmentAbsoluteDuration=345600", + ); + + assert_box_roundtrip( + Uuid { + user_type: UUID_FRAGMENT_RUN_TABLE, + payload: UuidPayload::FragmentRunTable(UuidFragmentRunTable { + version: 0, + flags: 0, + fragment_count: 2, + entries: vec![ + UuidFragmentRunEntry { + fragment_absolute_time: 16, + fragment_absolute_duration: 32, + }, + UuidFragmentRunEntry { + fragment_absolute_time: 48, + fragment_absolute_duration: 64, + }, + ], + }), + }, + &[ + 0xd4, 0x80, 0x7e, 0xf2, 0xca, 0x39, 0x46, 0x95, 0x8e, 0x54, 0x26, 0xcb, 0x9e, 0x46, + 0xa7, 0x9f, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x40, + ], + "UserType=d4807ef2-ca39-4695-8e54-26cb9e46a79f Version=0 Flags=0x000000 FragmentCount=2 Entries=[{FragmentAbsoluteTime=16 FragmentAbsoluteDuration=32}, {FragmentAbsoluteTime=48 FragmentAbsoluteDuration=64}]", + ); + + assert_box_roundtrip( + Uuid { + user_type: UUID_FRAGMENT_RUN_TABLE, + payload: UuidPayload::FragmentRunTable(UuidFragmentRunTable { + version: 1, + flags: 0, + fragment_count: 1, + entries: vec![UuidFragmentRunEntry { + fragment_absolute_time: 0x0001_05c6_49c2_ea00, + fragment_absolute_duration: 0x0000_0000_0005_4600, + }], + }), + }, + &[ + 0xd4, 0x80, 0x7e, 0xf2, 0xca, 0x39, 0x46, 0x95, 0x8e, 0x54, 0x26, 0xcb, 0x9e, 0x46, + 0xa7, 0x9f, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x05, 0xc6, 0x49, 0xc2, 0xea, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x46, 0x00, + ], + "UserType=d4807ef2-ca39-4695-8e54-26cb9e46a79f Version=1 Flags=0x000000 FragmentCount=1 Entries=[{FragmentAbsoluteTime=287824175884800 FragmentAbsoluteDuration=345600}]", + ); + + let mut legacy_sample_encryption = Senc::default(); + legacy_sample_encryption.set_version(0); + legacy_sample_encryption.set_flags(SENC_USE_SUBSAMPLE_ENCRYPTION); + legacy_sample_encryption.sample_count = 1; + legacy_sample_encryption.samples = vec![SencSample { + initialization_vector: vec![1, 2, 3, 4, 5, 6, 7, 8], + subsamples: vec![SencSubsample { + bytes_of_clear_data: 5, + bytes_of_protected_data: 16, + }], + }]; + assert_box_roundtrip( + Uuid { + user_type: UUID_SAMPLE_ENCRYPTION, + payload: UuidPayload::SampleEncryption(legacy_sample_encryption), + }, + &[ + 0xa2, 0x39, 0x4f, 0x52, 0x5a, 0x9b, 0x4f, 0x14, 0xa2, 0x44, 0x6c, 0x42, 0x7c, 0x64, + 0x8d, 0xf4, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x00, 0x01, 0x00, 0x05, 0x00, 0x00, 0x00, 0x10, + ], + "UserType=a2394f52-5a9b-4f14-a244-6c427c648df4 Version=0 Flags=0x000002 SampleCount=1 Samples=[{InitializationVector=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8] Subsamples=[{BytesOfClearData=5 BytesOfProtectedData=16}]}]", + ); + + assert_box_roundtrip( + Uuid { + user_type: [ + 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, + 0xcd, 0xef, + ], + payload: UuidPayload::Raw(vec![0xde, 0xad, 0xbe]), + }, + &[ + 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, + 0xcd, 0xef, 0xde, 0xad, 0xbe, + ], + "UserType=10325476-98ba-dcfe-0123-456789abcdef RawPayload=[0xde, 0xad, 0xbe]", + ); +} + +#[test] +fn loudness_and_uuid_validation_rejects_malformed_payloads() { + let tlou_payload = [0x01, 0x00, 0x00, 0x00, 0x40]; + let mut tlou = TrackLoudnessInfo::default(); + let mut tlou_reader = Cursor::new(tlou_payload); + let tlou_error = + unmarshal(&mut tlou_reader, tlou_payload.len() as u64, &mut tlou, None).unwrap_err(); + assert_eq!( + tlou_error.to_string(), + "invalid field value for Entries: loudness info type is not supported" + ); + + let uuid_payload = [0x01, 0x02, 0x03]; + let mut uuid = Uuid::default(); + let mut uuid_reader = Cursor::new(uuid_payload); + let uuid_error = + unmarshal(&mut uuid_reader, uuid_payload.len() as u64, &mut uuid, None).unwrap_err(); + assert_eq!( + uuid_error.to_string(), + "invalid field value for Payload: payload is too short" + ); + + let fragment_timing_payload = [ + 0x6d, 0x1d, 0x9b, 0x05, 0x42, 0xd5, 0x44, 0xe6, 0x80, 0xe2, 0x14, 0x1d, 0xaf, 0xf7, 0x57, + 0xb2, 0x01, 0x00, 0x00, 0x00, 0x00, + ]; + let mut fragment_timing = Uuid::default(); + let mut fragment_timing_reader = Cursor::new(fragment_timing_payload); + let fragment_timing_error = unmarshal( + &mut fragment_timing_reader, + fragment_timing_payload.len() as u64, + &mut fragment_timing, + None, + ) + .unwrap_err(); + assert_eq!( + fragment_timing_error.to_string(), + "invalid field value for Payload: fragment timing payload length does not match version 1" + ); + + let fragment_run_payload = [ + 0xd4, 0x80, 0x7e, 0xf2, 0xca, 0x39, 0x46, 0x95, 0x8e, 0x54, 0x26, 0xcb, 0x9e, 0x46, 0xa7, + 0x9f, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, + ]; + let mut fragment_run = Uuid::default(); + let mut fragment_run_reader = Cursor::new(fragment_run_payload); + let fragment_run_error = unmarshal( + &mut fragment_run_reader, + fragment_run_payload.len() as u64, + &mut fragment_run, + None, + ) + .unwrap_err(); + assert_eq!( + fragment_run_error.to_string(), + "invalid field value for Payload: fragment run table payload length does not match the fragment count" + ); + + let sample_encryption_payload = [ + 0xa2, 0x39, 0x4f, 0x52, 0x5a, 0x9b, 0x4f, 0x14, 0xa2, 0x44, 0x6c, 0x42, 0x7c, 0x64, 0x8d, + 0xf4, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + let mut sample_encryption = Uuid::default(); + let mut sample_encryption_reader = Cursor::new(sample_encryption_payload); + let sample_encryption_error = unmarshal( + &mut sample_encryption_reader, + sample_encryption_payload.len() as u64, + &mut sample_encryption, + None, + ) + .unwrap_err(); + assert_eq!( + sample_encryption_error.to_string(), + "invalid field value for Payload: sample encryption payload version is not supported" + ); +} + +#[test] +fn event_message_validation_rejects_malformed_payloads() { + let silb_payload = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, b'u', b'r', b'n', b':', b't', b'e', b's', + b't', 0x00, b'o', b'n', b'e', 0x00, 0x00, + ]; + let mut silb = Silb::default(); + let mut silb_reader = Cursor::new(silb_payload); + let silb_error = + unmarshal(&mut silb_reader, silb_payload.len() as u64, &mut silb, None).unwrap_err(); + assert_eq!( + silb_error.to_string(), + "invalid field value for Schemes: scheme flag payload is truncated" + ); + + let emib_payload = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc, + 0x18, 0x00, 0x00, 0x07, 0xd0, 0x00, 0x00, 0x12, 0x34, b'u', b'r', b'n', b':', b't', b'e', + b's', b't', 0x00, b'2', 0x00, b'a', b'b', b'c', + ]; + let mut emib = Emib::default(); + let mut emib_reader = Cursor::new(emib_payload); + let emib_error = + unmarshal(&mut emib_reader, emib_payload.len() as u64, &mut emib, None).unwrap_err(); + assert_eq!( + emib_error.to_string(), + "invalid field value for Reserved: reserved field must be zero" + ); + + let emeb_payload = [0x00]; + let mut emeb = Emeb; + let mut emeb_reader = Cursor::new(emeb_payload); + let emeb_error = + unmarshal(&mut emeb_reader, emeb_payload.len() as u64, &mut emeb, None).unwrap_err(); + assert_eq!( + emeb_error.to_string(), + "invalid field value for Payload: payload must be empty" + ); +} + +#[test] +fn sgpd_seig_rejects_default_length_mismatch_during_marshal() { + let mut sgpd = Sgpd::default(); + sgpd.set_version(1); + sgpd.grouping_type = FourCc::from_bytes(*b"seig"); + sgpd.default_length = 20; + sgpd.entry_count = 1; + sgpd.seig_entries = vec![SeigEntry { + crypt_byte_block: 1, + skip_byte_block: 2, + is_protected: 1, + kid: [ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, + 0xcd, 0xef, + ], + constant_iv_size: 4, + constant_iv: vec![0x01, 0x23, 0x45, 0x67], + ..SeigEntry::default() + }]; + + let error = marshal(&mut Vec::new(), &sgpd, None).unwrap_err(); + assert_eq!( + error.to_string(), + "invalid field value for SeigEntries: seig entry does not match the default length" + ); +} + +#[test] +fn prft_validation_rejects_unsupported_versions_and_truncated_payloads() { + let mut prft = Prft::default(); + + let unsupported_payload = [ + 0x02, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x23, 0x45, 0x67, 0x89, + ]; + let error = unmarshal( + &mut Cursor::new(unsupported_payload), + unsupported_payload.len() as u64, + &mut prft, + None, + ) + .unwrap_err(); + match error { + CodecError::UnsupportedVersion { box_type, version } => { + assert_eq!(box_type, FourCc::from_bytes(*b"prft")); + assert_eq!(version, 2); + } + other => panic!("unexpected error: {other}"), + } + + let truncated_payload = [ + 0x00, 0x00, 0x00, 0x01, 0x12, 0x34, 0x56, 0x78, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x23, 0x45, 0x67, + ]; + let error = unmarshal( + &mut Cursor::new(truncated_payload), + truncated_payload.len() as u64, + &mut prft, + None, + ) + .unwrap_err(); + assert!(matches!(error, CodecError::Io(_))); +} + +#[test] +fn prft_helpers_surface_timestamp_parts_unix_time_and_known_flag_meanings() { + let seconds = PRFT_NTP_UNIX_EPOCH_OFFSET_SECONDS + 1_234; + let fraction = 0x8000_0000; + + let mut prft = Prft::default(); + prft.set_version(1); + prft.set_flags(PRFT_TIME_CAPTURED); + prft.reference_track_id = 3; + prft.ntp_timestamp = (seconds << 32) | u64::from(fraction); + prft.media_time_v1 = 42; + + assert_eq!(prft.media_time(), 42); + assert_eq!(prft.ntp_seconds(), seconds as u32); + assert_eq!(prft.ntp_fraction(), fraction); + assert_eq!(prft.ntp_fraction_nanos(), 500_000_000); + assert_eq!(prft.unix_seconds(), Some(1_234)); + assert_eq!( + prft.unix_time(), + Some(UNIX_EPOCH + Duration::new(1_234, 500_000_000)) + ); + assert_eq!(prft.flag_meaning(), Some("time_captured")); + + assert_eq!( + Prft::known_flag_meaning(PRFT_TIME_ENCODER_INPUT), + Some("time_encoder_input") + ); + assert_eq!( + Prft::known_flag_meaning(PRFT_TIME_ENCODER_OUTPUT), + Some("time_encoder_output") + ); + assert_eq!( + Prft::known_flag_meaning(PRFT_TIME_MOOF_FINALIZED), + Some("time_moof_finalized") + ); + assert_eq!( + Prft::known_flag_meaning(PRFT_TIME_MOOF_WRITTEN), + Some("time_moof_written") + ); + assert_eq!( + Prft::known_flag_meaning(PRFT_TIME_ARBITRARY_CONSISTENT), + Some("time_arbitrary_consistent") + ); + assert_eq!( + Prft::known_flag_meaning(PRFT_TIME_CAPTURED), + Some("time_captured") + ); + assert_eq!(Prft::known_flag_meaning(0x000003), None); + + let mut before_unix = Prft::default(); + before_unix.ntp_timestamp = 0x0000_0001_0000_0000; + assert_eq!(before_unix.unix_seconds(), None); + assert_eq!(before_unix.unix_time(), None); +} + +#[test] +fn subtitle_media_header_boxes_reject_unsupported_versions_and_truncated_payloads() { + for (box_type, mut decoder) in [ + ( + FourCc::from_bytes(*b"sthd"), + EitherEmptyFullBox::Sthd(Sthd::default()), + ), + ( + FourCc::from_bytes(*b"nmhd"), + EitherEmptyFullBox::Nmhd(Nmhd::default()), + ), + ] { + let unsupported_payload = [0x01, 0x00, 0x00, 0x00]; + let error = decoder.unmarshal_payload(&unsupported_payload).unwrap_err(); + match error { + CodecError::UnsupportedVersion { + box_type: actual_box_type, + version, + } => { + assert_eq!(actual_box_type, box_type); + assert_eq!(version, 1); + } + other => panic!("unexpected error: {other}"), + } + + let truncated_payload = [0x00, 0x00, 0x00]; + let error = decoder.unmarshal_payload(&truncated_payload).unwrap_err(); + assert!(matches!(error, CodecError::Io(_))); + } +} + +enum EitherEmptyFullBox { + Sthd(Sthd), + Nmhd(Nmhd), +} + +impl EitherEmptyFullBox { + fn unmarshal_payload(&mut self, payload: &[u8]) -> Result<(), CodecError> { + match self { + Self::Sthd(box_value) => { + unmarshal( + &mut Cursor::new(payload), + payload.len() as u64, + box_value, + None, + )?; + } + Self::Nmhd(box_value) => { + unmarshal( + &mut Cursor::new(payload), + payload.len() as u64, + box_value, + None, + )?; + } + } + Ok(()) + } +} + +#[test] +fn tref_child_validation_rejects_misaligned_payloads() { + let payload = [0x01, 0x23, 0x45, 0x67, 0x89]; + let mut cdsc = Cdsc::default(); + let error = unmarshal( + &mut Cursor::new(payload), + payload.len() as u64, + &mut cdsc, + None, + ) + .unwrap_err(); + assert_eq!( + error.to_string(), + "invalid field value for TrackIDs: value does not align with entry size" + ); +} + #[test] fn built_in_registry_reports_supported_versions_for_landed_types() { let registry = default_registry(); @@ -1834,14 +2935,70 @@ fn built_in_registry_reports_supported_versions_for_landed_types() { registry.supported_versions(FourCc::from_bytes(*b"meta")), Some(&[0][..]) ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"elng")), + Some(&[0][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"alou")), + Some(&[0, 1][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"cdat")), + Some(&[][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"leva")), + Some(&[0][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"ludt")), + Some(&[][..]) + ); assert_eq!( registry.supported_versions(FourCc::from_bytes(*b"saio")), Some(&[0, 1][..]) ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"prft")), + Some(&[0, 1][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"sthd")), + Some(&[0][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"nmhd")), + Some(&[0][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"CoLL")), + Some(&[0][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"kind")), + Some(&[0][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"mime")), + Some(&[0][..]) + ); assert_eq!( registry.supported_versions(FourCc::from_bytes(*b"sgpd")), Some(&[1, 2][..]) ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"SmDm")), + Some(&[0][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"ssix")), + Some(&[0][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"subs")), + Some(&[0, 1][..]) + ); assert_eq!( registry.supported_versions(FourCc::from_bytes(*b"tfra")), Some(&[0, 1][..]) @@ -1850,21 +3007,74 @@ fn built_in_registry_reports_supported_versions_for_landed_types() { registry.supported_versions(FourCc::from_bytes(*b"emsg")), Some(&[0, 1][..]) ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"emib")), + Some(&[0][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"silb")), + Some(&[0][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"tref")), + Some(&[][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"tlou")), + Some(&[0, 1][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"uuid")), + Some(&[][..]) + ); assert!(registry.is_registered(FourCc::from_bytes(*b"ftyp"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"alou"))); assert!(registry.is_registered(FourCc::from_bytes(*b"avcC"))); assert!(registry.is_registered(FourCc::from_bytes(*b"btrt"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"cdat"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"clap"))); assert!(registry.is_registered(FourCc::from_bytes(*b"colr"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"CoLL"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"elng"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"emeb"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"emib"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"evte"))); assert!(registry.is_registered(FourCc::from_bytes(*b"hdlr"))); assert!(registry.is_registered(FourCc::from_bytes(*b"hvcC"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"kind"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"leva"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"ludt"))); assert!(registry.is_registered(FourCc::from_bytes(*b"avc1"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"mime"))); assert!(registry.is_registered(FourCc::from_bytes(*b"mp4a"))); assert!(registry.is_registered(FourCc::from_bytes(*b"pasp"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"prft"))); assert!(registry.is_registered(FourCc::from_bytes(*b"schm"))); assert!(registry.is_registered(FourCc::from_bytes(*b"sbtt"))); assert!(registry.is_registered(FourCc::from_bytes(*b"sidx"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"silb"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"SmDm"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"ssix"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"sthd"))); assert!(registry.is_registered(FourCc::from_bytes(*b"stpp"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"sync"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"subt"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"subs"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"nmhd"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"tref"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"tlou"))); assert!(registry.is_registered(FourCc::from_bytes(*b"trun"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"vdep"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"vplx"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"uuid"))); assert!(registry.is_registered(FourCc::from_bytes(*b"wave"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"cdsc"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"dpnd"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"font"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"hind"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"hint"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"ipir"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"mpod"))); } #[test] diff --git a/tests/box_catalog_iso14496_15.rs b/tests/box_catalog_iso14496_15.rs new file mode 100644 index 0000000..b653fef --- /dev/null +++ b/tests/box_catalog_iso14496_15.rs @@ -0,0 +1,214 @@ +use std::any::type_name; +use std::fmt::Debug; +use std::io::Cursor; + +use mp4forge::FourCc; +use mp4forge::boxes::iso14496_12::{SampleEntry, VisualSampleEntry}; +use mp4forge::boxes::iso14496_15::VVCDecoderConfiguration; +use mp4forge::boxes::{AnyTypeBox, default_registry}; +use mp4forge::codec::{CodecBox, MutableBox, marshal, unmarshal, unmarshal_any}; +use mp4forge::stringify::stringify; + +fn assert_box_roundtrip(src: T, payload: &[u8], expected: &str) +where + T: CodecBox + Default + PartialEq + Debug + 'static, +{ + let mut encoded = Vec::new(); + let written = marshal(&mut encoded, &src, None).unwrap(); + assert_eq!( + written, + payload.len() as u64, + "marshal length for {}", + type_name::() + ); + assert_eq!(encoded, payload, "marshal bytes for {}", type_name::()); + + let mut decoded = T::default(); + let mut reader = Cursor::new(payload.to_vec()); + let read = unmarshal(&mut reader, payload.len() as u64, &mut decoded, None).unwrap(); + assert_eq!( + read, + payload.len() as u64, + "unmarshal length for {}", + type_name::() + ); + assert_eq!(decoded, src, "unmarshal value for {}", type_name::()); + + let registry = default_registry(); + let mut any_reader = Cursor::new(payload.to_vec()); + let (any_box, any_read) = unmarshal_any( + &mut any_reader, + payload.len() as u64, + src.box_type(), + ®istry, + None, + ) + .unwrap(); + assert_eq!( + any_read, + payload.len() as u64, + "registry unmarshal length for {}", + type_name::() + ); + assert_eq!(any_box.as_any().downcast_ref::().unwrap(), &src); + + assert_eq!(stringify(&src, None).unwrap(), expected); +} + +fn assert_any_box_roundtrip(src: T, payload: &[u8], expected: &str) +where + T: CodecBox + AnyTypeBox + Default + PartialEq + Debug + 'static, +{ + let mut encoded = Vec::new(); + let written = marshal(&mut encoded, &src, None).unwrap(); + assert_eq!( + written, + payload.len() as u64, + "marshal length for {}", + type_name::() + ); + assert_eq!(encoded, payload, "marshal bytes for {}", type_name::()); + + let mut decoded = T::default(); + decoded.set_box_type(src.box_type()); + let mut reader = Cursor::new(payload.to_vec()); + let read = unmarshal(&mut reader, payload.len() as u64, &mut decoded, None).unwrap(); + assert_eq!( + read, + payload.len() as u64, + "unmarshal length for {}", + type_name::() + ); + assert_eq!(decoded, src, "unmarshal value for {}", type_name::()); + + let registry = default_registry(); + let mut any_reader = Cursor::new(payload.to_vec()); + let (any_box, any_read) = unmarshal_any( + &mut any_reader, + payload.len() as u64, + src.box_type(), + ®istry, + None, + ) + .unwrap(); + assert_eq!( + any_read, + payload.len() as u64, + "registry unmarshal length for {}", + type_name::() + ); + assert_eq!(any_box.as_any().downcast_ref::().unwrap(), &src); + + assert_eq!(stringify(&src, None).unwrap(), expected); +} + +#[test] +fn iso14496_15_catalog_roundtrips() { + assert_any_box_roundtrip( + VisualSampleEntry { + sample_entry: SampleEntry { + box_type: FourCc::from_bytes(*b"vvc1"), + data_reference_index: 1, + }, + width: 640, + height: 360, + horizresolution: 72 << 16, + vertresolution: 72 << 16, + frame_count: 1, + depth: 0x0018, + pre_defined3: -1, + ..VisualSampleEntry::default() + }, + &[ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x01, // + 0x00, 0x00, // + 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, // + 0x02, 0x80, // + 0x01, 0x68, // + 0x00, 0x48, 0x00, 0x00, // + 0x00, 0x48, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x01, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x18, // + 0xff, 0xff, + ], + "DataReferenceIndex=1 PreDefined=0 PreDefined2=[0, 0, 0] Width=640 Height=360 Horizresolution=4718592 Vertresolution=4718592 FrameCount=1 Compressorname=\"\" Depth=24 PreDefined3=-1", + ); + assert_any_box_roundtrip( + VisualSampleEntry { + sample_entry: SampleEntry { + box_type: FourCc::from_bytes(*b"vvi1"), + data_reference_index: 1, + }, + width: 640, + height: 360, + horizresolution: 72 << 16, + vertresolution: 72 << 16, + frame_count: 1, + depth: 0x0018, + pre_defined3: -1, + ..VisualSampleEntry::default() + }, + &[ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x01, // + 0x00, 0x00, // + 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, // + 0x02, 0x80, // + 0x01, 0x68, // + 0x00, 0x48, 0x00, 0x00, // + 0x00, 0x48, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x01, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x18, // + 0xff, 0xff, + ], + "DataReferenceIndex=1 PreDefined=0 PreDefined2=[0, 0, 0] Width=640 Height=360 Horizresolution=4718592 Vertresolution=4718592 FrameCount=1 Compressorname=\"\" Depth=24 PreDefined3=-1", + ); + assert_box_roundtrip( + { + let mut vvcc = VVCDecoderConfiguration::default(); + vvcc.set_version(0); + vvcc.decoder_configuration_record = vec![0x01, 0x23, 0x45, 0x67, 0x89]; + vvcc + }, + &[0x00, 0x00, 0x00, 0x00, 0x01, 0x23, 0x45, 0x67, 0x89], + "Version=0 Flags=0x000000 DecoderConfigurationRecord=[0x1, 0x23, 0x45, 0x67, 0x89]", + ); +} + +#[test] +fn built_in_registry_reports_supported_versions_for_landed_iso14496_15_types() { + let registry = default_registry(); + + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"vvc1")), + Some(&[][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"vvi1")), + Some(&[][..]) + ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"vvcC")), + Some(&[0][..]) + ); + assert!(registry.is_registered(FourCc::from_bytes(*b"vvc1"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"vvi1"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"vvcC"))); +} diff --git a/tests/box_catalog_iso23001_7.rs b/tests/box_catalog_iso23001_7.rs index f846e1d..8a15353 100644 --- a/tests/box_catalog_iso23001_7.rs +++ b/tests/box_catalog_iso23001_7.rs @@ -4,7 +4,9 @@ use std::io::Cursor; use mp4forge::FourCc; use mp4forge::boxes::default_registry; -use mp4forge::boxes::iso23001_7::{Pssh, PsshKid, Tenc}; +use mp4forge::boxes::iso23001_7::{ + Pssh, PsshKid, SENC_USE_SUBSAMPLE_ENCRYPTION, Senc, SencSample, SencSubsample, Tenc, +}; use mp4forge::codec::{CodecBox, MutableBox, marshal, unmarshal, unmarshal_any}; use mp4forge::stringify::stringify; @@ -114,6 +116,67 @@ fn protection_catalog_roundtrips() { "Version=1 Flags=0x000000 SystemID=01020304-0506-0708-090a-0b0c0d0e0f10 KIDCount=2 KIDs=[11121314-1516-1718-191a-1b1c1d1e1f10, 21222324-2526-2728-292a-2b2c2d2e2f20] DataSize=5 Data=[0x21, 0x22, 0x23, 0x24, 0x25]", ); + let mut senc = Senc::default(); + senc.set_version(0); + senc.sample_count = 2; + senc.samples = vec![ + SencSample { + initialization_vector: vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], + subsamples: Vec::new(), + }, + SencSample { + initialization_vector: vec![0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18], + subsamples: Vec::new(), + }, + ]; + + assert_box_roundtrip( + senc, + &[ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, + ], + "Version=0 Flags=0x000000 SampleCount=2 Samples=[{InitializationVector=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]}, {InitializationVector=[0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18]}]", + ); + + let mut senc_subsamples = Senc::default(); + senc_subsamples.set_version(0); + senc_subsamples.set_flags(SENC_USE_SUBSAMPLE_ENCRYPTION); + senc_subsamples.sample_count = 2; + senc_subsamples.samples = vec![ + SencSample { + initialization_vector: vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], + subsamples: vec![ + SencSubsample { + bytes_of_clear_data: 1, + bytes_of_protected_data: 2, + }, + SencSubsample { + bytes_of_clear_data: 3, + bytes_of_protected_data: 4, + }, + ], + }, + SencSample { + initialization_vector: vec![0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18], + subsamples: vec![SencSubsample { + bytes_of_clear_data: 5, + bytes_of_protected_data: 6, + }], + }, + ]; + + assert_box_roundtrip( + senc_subsamples, + &[ + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x04, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x00, 0x01, 0x00, 0x05, + 0x00, 0x00, 0x00, 0x06, + ], + "Version=0 Flags=0x000002 SampleCount=2 Samples=[{InitializationVector=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8] Subsamples=[{BytesOfClearData=1 BytesOfProtectedData=2}, {BytesOfClearData=3 BytesOfProtectedData=4}]}, {InitializationVector=[0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18] Subsamples=[{BytesOfClearData=5 BytesOfProtectedData=6}]}]", + ); + let mut tenc_constant_iv = Tenc::default(); tenc_constant_iv.set_version(1); tenc_constant_iv.reserved = 0x00; @@ -180,11 +243,16 @@ fn built_in_registry_reports_supported_versions_for_landed_protection_types() { registry.supported_versions(FourCc::from_bytes(*b"pssh")), Some(&[0, 1][..]) ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"senc")), + Some(&[0][..]) + ); assert_eq!( registry.supported_versions(FourCc::from_bytes(*b"tenc")), Some(&[0, 1][..]) ); assert!(registry.is_registered(FourCc::from_bytes(*b"pssh"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"senc"))); assert!(registry.is_registered(FourCc::from_bytes(*b"tenc"))); } @@ -230,3 +298,55 @@ fn tenc_rejects_constant_iv_length_mismatch_during_marshal() { "invalid element count for field DefaultConstantIV: expected 4, got 3" ); } + +#[test] +fn senc_rejects_sample_count_mismatch_during_marshal() { + let mut senc = Senc::default(); + senc.set_version(0); + senc.sample_count = 2; + senc.samples = vec![SencSample { + initialization_vector: vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], + subsamples: Vec::new(), + }]; + + let error = marshal(&mut Vec::new(), &senc, None).unwrap_err(); + assert_eq!( + error.to_string(), + "invalid element count for field Samples: expected 2, got 1" + ); +} + +#[test] +fn senc_rejects_subsample_records_without_flag_during_marshal() { + let mut senc = Senc::default(); + senc.set_version(0); + senc.sample_count = 1; + senc.samples = vec![SencSample { + initialization_vector: vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08], + subsamples: vec![SencSubsample { + bytes_of_clear_data: 1, + bytes_of_protected_data: 2, + }], + }]; + + let error = marshal(&mut Vec::new(), &senc, None).unwrap_err(); + assert_eq!( + error.to_string(), + "invalid field value for Samples: subsample records require the UseSubSampleEncryption flag" + ); +} + +#[test] +fn senc_rejects_unsupported_versions_during_unmarshal() { + let payload = [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + let mut decoded = Senc::default(); + let error = unmarshal( + &mut Cursor::new(payload), + payload.len() as u64, + &mut decoded, + None, + ) + .unwrap_err(); + + assert_eq!(error.to_string(), "unsupported box version 1 for type senc"); +} diff --git a/tests/box_catalog_metadata.rs b/tests/box_catalog_metadata.rs index 6d61e17..5d1eb12 100644 --- a/tests/box_catalog_metadata.rs +++ b/tests/box_catalog_metadata.rs @@ -9,11 +9,11 @@ use mp4forge::boxes::metadata::{ DATA_TYPE_FLOAT32_BIG_ENDIAN, DATA_TYPE_FLOAT64_BIG_ENDIAN, DATA_TYPE_SIGNED_INT_BIG_ENDIAN, DATA_TYPE_STRING_JPEG, DATA_TYPE_STRING_MAC, DATA_TYPE_STRING_UTF8, DATA_TYPE_STRING_UTF16, Data, DateData, DescriptionData, DiskNumberData, EncodingToolData, EpisodeGuidData, - GaplessPlaybackData, GenreData, GenreIdData, GroupingData, Ilst, IlstMetaContainer, Key, Keys, - LegacyGenreData, MediaTypeData, NameData, NumberedMetadataItem, PlaylistIdData, PodcastData, - PodcastUrlData, PurchaseDateData, RatingData, SfIdData, SortAlbumArtistData, SortAlbumData, - SortArtistData, SortComposerData, SortNameData, SortShowData, StringData, TempoData, - TrackNumberData, TvEpisodeData, TvEpisodeIdData, TvNetworkNameData, TvSeasonData, + GaplessPlaybackData, GenreData, GenreIdData, GroupingData, Id32, Ilst, IlstMetaContainer, Key, + Keys, LegacyGenreData, MediaTypeData, NameData, NumberedMetadataItem, PlaylistIdData, + PodcastData, PodcastUrlData, PurchaseDateData, RatingData, SfIdData, SortAlbumArtistData, + SortAlbumData, SortArtistData, SortComposerData, SortNameData, SortShowData, StringData, + TempoData, TrackNumberData, TvEpisodeData, TvEpisodeIdData, TvNetworkNameData, TvSeasonData, TvShowNameData, WriterData, }; use mp4forge::boxes::{AnyTypeBox, default_registry}; @@ -929,6 +929,29 @@ fn keys_reject_truncated_entry_payloads() { ); } +#[test] +fn id32_roundtrips_and_validates_language_codes() { + let mut id32 = Id32::default(); + id32.set_version(0); + id32.language = "eng".to_string(); + id32.id3v2_data = vec![0x49, 0x44, 0x33]; + + assert_box_roundtrip_with_registry( + id32, + &[0x00, 0x00, 0x00, 0x00, 0x15, 0xc7, 0x49, 0x44, 0x33], + "Version=0 Flags=0x000000 Language=\"eng\" ID3v2Data=[0x49, 0x44, 0x33]", + ); + + let payload = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + let mut decoded = Id32::default(); + let mut reader = Cursor::new(payload); + let error = unmarshal(&mut reader, payload.len() as u64, &mut decoded, None).unwrap_err(); + assert_eq!( + error.to_string(), + "invalid field value for Language: language code uses an out-of-range character value" + ); +} + #[test] fn built_in_registry_reports_context_free_metadata_types() { let registry = default_registry(); @@ -937,12 +960,17 @@ fn built_in_registry_reports_context_free_metadata_types() { registry.supported_versions(FourCc::from_bytes(*b"ilst")), Some(&[][..]) ); + assert_eq!( + registry.supported_versions(FourCc::from_bytes(*b"ID32")), + Some(&[0][..]) + ); assert_eq!( registry.supported_versions(FourCc::from_bytes(*b"keys")), Some(&[][..]) ); assert!(registry.is_registered(FourCc::from_bytes(*b"ilst"))); + assert!(registry.is_registered(FourCc::from_bytes(*b"ID32"))); assert!(registry.is_registered(FourCc::from_bytes(*b"keys"))); assert!(!registry.is_registered(FourCc::from_bytes(*b"data"))); assert!(!registry.is_registered(FourCc::from_bytes(*b"----"))); diff --git a/tests/box_catalog_mpeg_h.rs b/tests/box_catalog_mpeg_h.rs new file mode 100644 index 0000000..62b4441 --- /dev/null +++ b/tests/box_catalog_mpeg_h.rs @@ -0,0 +1,174 @@ +use std::any::type_name; +use std::fmt::Debug; +use std::io::Cursor; + +use mp4forge::FourCc; +use mp4forge::boxes::iso14496_12::{AudioSampleEntry, SampleEntry}; +use mp4forge::boxes::mpeg_h::MhaC; +use mp4forge::boxes::{AnyTypeBox, default_registry}; +use mp4forge::codec::{CodecBox, marshal, unmarshal, unmarshal_any}; +use mp4forge::stringify::stringify; + +fn assert_box_roundtrip(src: T, payload: &[u8], expected: &str) +where + T: CodecBox + Default + PartialEq + Debug + 'static, +{ + let mut encoded = Vec::new(); + let written = marshal(&mut encoded, &src, None).unwrap(); + assert_eq!( + written, + payload.len() as u64, + "marshal length for {}", + type_name::() + ); + assert_eq!(encoded, payload, "marshal bytes for {}", type_name::()); + + let mut decoded = T::default(); + let mut reader = Cursor::new(payload.to_vec()); + let read = unmarshal(&mut reader, payload.len() as u64, &mut decoded, None).unwrap(); + assert_eq!( + read, + payload.len() as u64, + "unmarshal length for {}", + type_name::() + ); + assert_eq!(decoded, src, "unmarshal value for {}", type_name::()); + + let registry = default_registry(); + let mut any_reader = Cursor::new(payload.to_vec()); + let (any_box, any_read) = unmarshal_any( + &mut any_reader, + payload.len() as u64, + src.box_type(), + ®istry, + None, + ) + .unwrap(); + assert_eq!( + any_read, + payload.len() as u64, + "registry unmarshal length for {}", + type_name::() + ); + assert_eq!(any_box.as_any().downcast_ref::().unwrap(), &src); + + assert_eq!(stringify(&src, None).unwrap(), expected); +} + +fn assert_any_box_roundtrip(src: T, payload: &[u8], expected: &str) +where + T: CodecBox + AnyTypeBox + Default + PartialEq + Debug + 'static, +{ + let mut encoded = Vec::new(); + let written = marshal(&mut encoded, &src, None).unwrap(); + assert_eq!( + written, + payload.len() as u64, + "marshal length for {}", + type_name::() + ); + assert_eq!(encoded, payload, "marshal bytes for {}", type_name::()); + + let mut decoded = T::default(); + decoded.set_box_type(src.box_type()); + let mut reader = Cursor::new(payload.to_vec()); + let read = unmarshal(&mut reader, payload.len() as u64, &mut decoded, None).unwrap(); + assert_eq!( + read, + payload.len() as u64, + "unmarshal length for {}", + type_name::() + ); + assert_eq!(decoded, src, "unmarshal value for {}", type_name::()); + + let registry = default_registry(); + let mut any_reader = Cursor::new(payload.to_vec()); + let (any_box, any_read) = unmarshal_any( + &mut any_reader, + payload.len() as u64, + src.box_type(), + ®istry, + None, + ) + .unwrap(); + assert_eq!( + any_read, + payload.len() as u64, + "registry unmarshal length for {}", + type_name::() + ); + assert_eq!(any_box.as_any().downcast_ref::().unwrap(), &src); + + assert_eq!(stringify(&src, None).unwrap(), expected); +} + +#[test] +fn mpeg_h_catalog_roundtrips() { + assert_any_box_roundtrip( + AudioSampleEntry { + sample_entry: SampleEntry { + box_type: FourCc::from_bytes(*b"mha1"), + data_reference_index: 1, + }, + entry_version: 0, + channel_count: 2, + sample_size: 16, + pre_defined: 0, + sample_rate: 48_000 << 16, + quicktime_data: Vec::new(), + }, + &[ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x01, // + 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x02, // + 0x00, 0x10, // + 0x00, 0x00, // + 0x00, 0x00, // + 0xbb, 0x80, 0x00, 0x00, + ], + "DataReferenceIndex=1 EntryVersion=0 ChannelCount=2 SampleSize=16 PreDefined=0 SampleRate=48000", + ); + + assert_box_roundtrip( + MhaC { + config_version: 1, + mpeg_h_3da_profile_level_indication: 12, + reference_channel_layout: 6, + mpeg_h_3da_config_length: 4, + mpeg_h_3da_config: vec![0x01, 0x02, 0x03, 0x04], + }, + &[0x01, 0x0c, 0x06, 0x00, 0x04, 0x01, 0x02, 0x03, 0x04], + "ConfigVersion=1 MpegH3DAProfileLevelIndication=12 ReferenceChannelLayout=6 MpegH3DAConfigLength=4 MpegH3DAConfig=[0x1, 0x2, 0x3, 0x4]", + ); +} + +#[test] +fn built_in_registry_reports_supported_versions_for_landed_mpeg_h_types() { + let registry = default_registry(); + + for box_type in ["mha1", "mha2", "mhm1", "mhm2", "mhaC"] { + let fourcc = FourCc::from_bytes(box_type.as_bytes().try_into().unwrap()); + assert_eq!(registry.supported_versions(fourcc), Some(&[][..])); + assert!(registry.is_supported_version(fourcc, 9)); + assert!(registry.is_registered(fourcc)); + } +} + +#[test] +fn mhac_rejects_config_length_mismatch_during_marshal() { + let mhac = MhaC { + config_version: 1, + mpeg_h_3da_profile_level_indication: 12, + reference_channel_layout: 6, + mpeg_h_3da_config_length: 5, + mpeg_h_3da_config: vec![0x01, 0x02, 0x03, 0x04], + }; + + let error = marshal(&mut Vec::new(), &mhac, None).unwrap_err(); + assert_eq!( + error.to_string(), + "invalid field value for MpegH3DAConfig: length does not match MpegH3DAConfigLength" + ); +} diff --git a/tests/cli_dump.rs b/tests/cli_dump.rs index 172c7aa..64bf52e 100644 --- a/tests/cli_dump.rs +++ b/tests/cli_dump.rs @@ -439,7 +439,7 @@ fn field_structured_dump_report_prefers_supported_fields_over_legacy_leaf_omissi } #[test] -fn dump_command_accepts_go_style_long_options() { +fn dump_command_accepts_double_dash_long_options() { let fixture = fixture_path("sample.mp4"); let args = vec![ "--full".to_string(), diff --git a/tests/cli_edit.rs b/tests/cli_edit.rs index 83405fd..acd0f88 100644 --- a/tests/cli_edit.rs +++ b/tests/cli_edit.rs @@ -66,7 +66,7 @@ fn edit_command_validates_argument_shape() { } #[test] -fn edit_command_accepts_go_style_long_options() { +fn edit_command_accepts_double_dash_long_options() { let input = build_edit_input_file(); let input_path = write_temp_file("edit-long-options-input", &input); let output_path = write_temp_file("edit-long-options-output", &[]); diff --git a/tests/cli_extract.rs b/tests/cli_extract.rs index 5b3bc23..fed52de 100644 --- a/tests/cli_extract.rs +++ b/tests/cli_extract.rs @@ -108,7 +108,7 @@ fn extract_command_rejects_invalid_path_arguments() { } #[test] -fn extract_command_matches_shared_fixture_reference_sizes() { +fn extract_command_matches_shared_fixture_expected_sizes() { let cases = [ ("sample.mp4", "ftyp", fourcc("ftyp"), 1_usize, 32_usize), ("sample.mp4", "mdhd", fourcc("mdhd"), 2, 64), @@ -148,7 +148,7 @@ fn extract_command_matches_shared_fixture_reference_sizes() { } #[test] -fn extract_command_matches_shared_fixture_reference_paths() { +fn extract_command_matches_shared_fixture_expected_paths() { let args = vec![ "--path".to_string(), "moov/*/mdia/mdhd".to_string(), diff --git a/tests/encryption.rs b/tests/encryption.rs new file mode 100644 index 0000000..4f644b7 --- /dev/null +++ b/tests/encryption.rs @@ -0,0 +1,286 @@ +use std::io::Cursor; + +use mp4forge::boxes::iso14496_12::{Saiz, Sbgp, SbgpEntry, SeigEntry, Sgpd}; +use mp4forge::boxes::iso23001_7::{ + SENC_USE_SUBSAMPLE_ENCRYPTION, Senc, SencSample, SencSubsample, Tenc, +}; +use mp4forge::codec::MutableBox; +use mp4forge::encryption::{ + ResolveSampleEncryptionError, ResolvedSampleEncryptionSource, SampleEncryptionContext, + resolve_sample_encryption, +}; +use mp4forge::extract::extract_box_as; +use mp4forge::walk::BoxPath; + +mod support; + +use support::{build_encrypted_fragmented_video_file, fourcc}; + +#[test] +fn resolve_sample_encryption_uses_fragment_local_seig_from_extracted_boxes() { + let file = build_encrypted_fragmented_video_file(); + + let tenc = extract_box_as::<_, Tenc>( + &mut Cursor::new(file.clone()), + None, + BoxPath::from([ + fourcc("moov"), + fourcc("trak"), + fourcc("mdia"), + fourcc("minf"), + fourcc("stbl"), + fourcc("stsd"), + fourcc("encv"), + fourcc("sinf"), + fourcc("schi"), + fourcc("tenc"), + ]), + ) + .unwrap(); + let saiz = extract_box_as::<_, Saiz>( + &mut Cursor::new(file.clone()), + None, + BoxPath::from([fourcc("moof"), fourcc("traf"), fourcc("saiz")]), + ) + .unwrap(); + let senc = extract_box_as::<_, Senc>( + &mut Cursor::new(file.clone()), + None, + BoxPath::from([fourcc("moof"), fourcc("traf"), fourcc("senc")]), + ) + .unwrap(); + let sgpd = extract_box_as::<_, Sgpd>( + &mut Cursor::new(file.clone()), + None, + BoxPath::from([fourcc("moof"), fourcc("traf"), fourcc("sgpd")]), + ) + .unwrap(); + let sbgp = extract_box_as::<_, Sbgp>( + &mut Cursor::new(file), + None, + BoxPath::from([fourcc("moof"), fourcc("traf"), fourcc("sbgp")]), + ) + .unwrap(); + + let resolved = resolve_sample_encryption( + &senc[0], + SampleEncryptionContext { + tenc: Some(&tenc[0]), + sgpd: Some(&sgpd[0]), + sbgp: Some(&sbgp[0]), + saiz: Some(&saiz[0]), + }, + ) + .unwrap(); + + assert!(resolved.uses_subsample_encryption); + assert_eq!(resolved.samples.len(), 1); + + let sample = &resolved.samples[0]; + assert_eq!(sample.sample_index, 1); + assert!(sample.is_protected); + assert_eq!(sample.crypt_byte_block, 1); + assert_eq!(sample.skip_byte_block, 9); + assert_eq!(sample.per_sample_iv_size, Some(8)); + assert_eq!(sample.initialization_vector, &[1, 2, 3, 4, 5, 6, 7, 8]); + assert_eq!( + sample.effective_initialization_vector(), + &[1, 2, 3, 4, 5, 6, 7, 8] + ); + assert_eq!(sample.subsamples.len(), 1); + assert_eq!(sample.auxiliary_info_size, 16); + assert!(matches!( + sample.metadata_source, + ResolvedSampleEncryptionSource::SampleGroupDescription { + group_description_index: 65_537, + description_index: 1, + fragment_local: true, + } + )); +} + +#[test] +fn resolve_sample_encryption_falls_back_to_tenc_when_group_mapping_runs_out() { + let tenc = sample_tenc(); + let mut senc = Senc::default(); + senc.sample_count = 2; + senc.samples = vec![ + sample_with_iv([1, 2, 3, 4, 5, 6, 7, 8]), + sample_with_iv([8, 7, 6, 5, 4, 3, 2, 1]), + ]; + let mut sgpd = Sgpd::default(); + sgpd.grouping_type = fourcc("seig"); + sgpd.entry_count = 1; + sgpd.seig_entries = vec![SeigEntry { + crypt_byte_block: 5, + skip_byte_block: 3, + is_protected: 1, + per_sample_iv_size: 8, + kid: [0xaa; 16], + ..SeigEntry::default() + }]; + let mut sbgp = Sbgp::default(); + sbgp.grouping_type = u32::from_be_bytes(*b"seig"); + sbgp.entry_count = 1; + sbgp.entries = vec![SbgpEntry { + sample_count: 1, + group_description_index: 1, + }]; + + let resolved = resolve_sample_encryption( + &senc, + SampleEncryptionContext { + tenc: Some(&tenc), + sgpd: Some(&sgpd), + sbgp: Some(&sbgp), + saiz: None, + }, + ) + .unwrap(); + + assert_eq!(resolved.samples.len(), 2); + assert!(matches!( + resolved.samples[0].metadata_source, + ResolvedSampleEncryptionSource::SampleGroupDescription { + group_description_index: 1, + description_index: 1, + fragment_local: false, + } + )); + assert_eq!(resolved.samples[0].crypt_byte_block, 5); + assert_eq!(resolved.samples[0].skip_byte_block, 3); + assert_eq!(resolved.samples[0].kid, [0xaa; 16]); + + assert_eq!( + resolved.samples[1].metadata_source, + ResolvedSampleEncryptionSource::TrackEncryptionBox + ); + assert_eq!(resolved.samples[1].crypt_byte_block, 1); + assert_eq!(resolved.samples[1].skip_byte_block, 9); + assert_eq!(resolved.samples[1].kid, [0x11; 16]); +} + +#[test] +fn resolve_sample_encryption_reports_missing_fragment_local_description() { + let tenc = sample_tenc(); + let mut senc = Senc::default(); + senc.sample_count = 1; + senc.samples = vec![sample_with_iv([1, 2, 3, 4, 5, 6, 7, 8])]; + let mut sbgp = Sbgp::default(); + sbgp.grouping_type = u32::from_be_bytes(*b"seig"); + sbgp.entry_count = 1; + sbgp.entries = vec![SbgpEntry { + sample_count: 1, + group_description_index: 65_537, + }]; + + let error = resolve_sample_encryption( + &senc, + SampleEncryptionContext { + tenc: Some(&tenc), + sgpd: None, + sbgp: Some(&sbgp), + saiz: None, + }, + ) + .unwrap_err(); + + assert_eq!( + error, + ResolveSampleEncryptionError::MissingSampleGroupDescription { + sample_index: 1, + group_description_index: 65_537, + description_index: 1, + fragment_local: true, + } + ); +} + +#[test] +fn resolve_sample_encryption_validates_inline_iv_sizes_against_defaults() { + let tenc = sample_tenc(); + let mut senc = Senc::default(); + senc.sample_count = 1; + senc.samples = vec![SencSample { + initialization_vector: vec![1, 2, 3, 4, 5, 6, 7], + subsamples: Vec::new(), + }]; + + let error = resolve_sample_encryption( + &senc, + SampleEncryptionContext { + tenc: Some(&tenc), + sgpd: None, + sbgp: None, + saiz: None, + }, + ) + .unwrap_err(); + + assert_eq!( + error, + ResolveSampleEncryptionError::SampleInitializationVectorSizeMismatch { + sample_index: 1, + expected: 8, + actual: 7, + } + ); +} + +#[test] +fn resolve_sample_encryption_validates_saiz_sample_sizes() { + let tenc = sample_tenc(); + let mut senc = Senc::default(); + senc.set_version(0); + senc.set_flags(SENC_USE_SUBSAMPLE_ENCRYPTION); + senc.sample_count = 1; + senc.samples = vec![SencSample { + initialization_vector: vec![1, 2, 3, 4, 5, 6, 7, 8], + subsamples: vec![SencSubsample { + bytes_of_clear_data: 32, + bytes_of_protected_data: 480, + }], + }]; + + let mut saiz = Saiz::default(); + saiz.sample_count = 1; + saiz.sample_info_size = vec![15]; + + let error = resolve_sample_encryption( + &senc, + SampleEncryptionContext { + tenc: Some(&tenc), + sgpd: None, + sbgp: None, + saiz: Some(&saiz), + }, + ) + .unwrap_err(); + + assert_eq!( + error, + ResolveSampleEncryptionError::SaizSampleInfoSizeMismatch { + sample_index: 1, + expected: 16, + actual: 15, + } + ); +} + +fn sample_tenc() -> Tenc { + let mut tenc = Tenc::default(); + tenc.set_version(1); + tenc.default_crypt_byte_block = 1; + tenc.default_skip_byte_block = 9; + tenc.default_is_protected = 1; + tenc.default_per_sample_iv_size = 8; + tenc.default_kid = [0x11; 16]; + tenc +} + +fn sample_with_iv(iv: [u8; 8]) -> SencSample { + SencSample { + initialization_vector: iv.to_vec(), + subsamples: Vec::new(), + } +} diff --git a/tests/extract.rs b/tests/extract.rs index a20c1f1..ae6681c 100644 --- a/tests/extract.rs +++ b/tests/extract.rs @@ -1,7 +1,12 @@ use std::io::Cursor; use mp4forge::boxes::AnyTypeBox; -use mp4forge::boxes::iso14496_12::{Ftyp, Meta, Moov, Tkhd, Trak, Udta}; +use mp4forge::boxes::iso14496_12::{ + Cdsc, Elng, Emeb, Emib, EventMessageSampleEntry, Ftyp, Leva, LevaLevel, Mdia, Meta, Minf, Moov, + Mvex, Saio, Saiz, Sbgp, Sgpd, Silb, Ssix, SsixRange, SsixSubsegment, Stbl, Subs, SubsEntry, + SubsSample, Tkhd, Trak, Tref, Trep, Udta, +}; +use mp4forge::boxes::iso23001_7::{Senc, Tenc}; use mp4forge::boxes::metadata::{ DATA_TYPE_STRING_UTF8, Data, Ilst, Key, Keys, NumberedMetadataItem, }; @@ -17,7 +22,9 @@ use mp4forge::{BoxInfo, FourCc}; mod support; -use support::fixture_path; +use support::{ + build_encrypted_fragmented_video_file, build_event_message_movie_file, fixture_path, +}; #[test] fn extract_boxes_match_exact_wildcard_and_relative_paths() { @@ -194,6 +201,47 @@ fn extract_box_payload_bytes_preserve_exact_container_payload_bytes() { assert_eq!(extracted, vec![leaf]); } +#[test] +fn extract_box_as_decodes_known_tref_children_and_preserves_unknown_ones_as_raw_bytes() { + let cdsc = encode_supported_box( + &Cdsc { + track_ids: vec![9, 11], + }, + &[], + ); + let unknown = encode_raw_box(fourcc("zzzz"), &[0xaa, 0xbb, 0xcc, 0xdd]); + let tref = encode_supported_box(&Tref, &[cdsc.clone(), unknown.clone()].concat()); + let trak = encode_supported_box(&Trak, &tref); + let moov = encode_supported_box(&Moov, &trak); + + let extracted_cdsc = extract_box_as::<_, Cdsc>( + &mut Cursor::new(moov.clone()), + None, + BoxPath::from([ + fourcc("moov"), + fourcc("trak"), + fourcc("tref"), + fourcc("cdsc"), + ]), + ) + .unwrap(); + assert_eq!(extracted_cdsc.len(), 1); + assert_eq!(extracted_cdsc[0].track_ids, vec![9, 11]); + + let extracted_unknown = extract_box_bytes( + &mut Cursor::new(moov), + None, + BoxPath::from([ + fourcc("moov"), + fourcc("trak"), + fourcc("tref"), + fourcc("zzzz"), + ]), + ) + .unwrap(); + assert_eq!(extracted_unknown, vec![unknown]); +} + #[test] fn extract_box_as_bytes_returns_typed_payloads_without_cursor() { let mut tkhd_a = Tkhd::default(); @@ -220,6 +268,281 @@ fn extract_box_as_bytes_returns_typed_payloads_without_cursor() { ); } +#[test] +fn extract_box_as_decodes_fragmented_encrypted_metadata_boxes() { + let file = build_encrypted_fragmented_video_file(); + + let tenc = extract_box_as::<_, Tenc>( + &mut Cursor::new(file.clone()), + None, + BoxPath::from([ + fourcc("moov"), + fourcc("trak"), + fourcc("mdia"), + fourcc("minf"), + fourcc("stbl"), + fourcc("stsd"), + fourcc("encv"), + fourcc("sinf"), + fourcc("schi"), + fourcc("tenc"), + ]), + ) + .unwrap(); + assert_eq!(tenc.len(), 1); + assert_eq!(tenc[0].default_is_protected, 1); + assert_eq!(tenc[0].default_per_sample_iv_size, 8); + assert_eq!( + tenc[0].default_kid, + [ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, + 0xdc, 0xfe, + ] + ); + + let saiz = extract_box_as::<_, Saiz>( + &mut Cursor::new(file.clone()), + None, + BoxPath::from([fourcc("moof"), fourcc("traf"), fourcc("saiz")]), + ) + .unwrap(); + assert_eq!(saiz.len(), 1); + assert_eq!(saiz[0].sample_count, 1); + assert_eq!(saiz[0].sample_info_size, vec![16]); + + let saio = extract_box_as::<_, Saio>( + &mut Cursor::new(file.clone()), + None, + BoxPath::from([fourcc("moof"), fourcc("traf"), fourcc("saio")]), + ) + .unwrap(); + assert_eq!(saio.len(), 1); + assert_eq!(saio[0].entry_count, 1); + assert_eq!(saio[0].offset(0), 0); + + let senc = extract_box_as::<_, Senc>( + &mut Cursor::new(file.clone()), + None, + BoxPath::from([fourcc("moof"), fourcc("traf"), fourcc("senc")]), + ) + .unwrap(); + assert_eq!(senc.len(), 1); + assert!(senc[0].uses_subsample_encryption()); + assert_eq!(senc[0].sample_count, 1); + assert_eq!( + senc[0].samples[0].initialization_vector, + vec![1, 2, 3, 4, 5, 6, 7, 8] + ); + assert_eq!(senc[0].samples[0].subsamples.len(), 1); + assert_eq!(senc[0].samples[0].subsamples[0].bytes_of_clear_data, 32); + assert_eq!( + senc[0].samples[0].subsamples[0].bytes_of_protected_data, + 480 + ); + + let sgpd = extract_box_as::<_, Sgpd>( + &mut Cursor::new(file.clone()), + None, + BoxPath::from([fourcc("moof"), fourcc("traf"), fourcc("sgpd")]), + ) + .unwrap(); + assert_eq!(sgpd.len(), 1); + assert_eq!(sgpd[0].grouping_type, fourcc("seig")); + assert_eq!(sgpd[0].seig_entries_l.len(), 1); + assert_eq!(sgpd[0].seig_entries_l[0].description_length, 20); + assert_eq!(sgpd[0].seig_entries_l[0].seig_entry.per_sample_iv_size, 8); + assert_eq!(sgpd[0].seig_entries_l[0].seig_entry.crypt_byte_block, 1); + assert_eq!(sgpd[0].seig_entries_l[0].seig_entry.skip_byte_block, 9); + + let sbgp = extract_box_as::<_, Sbgp>( + &mut Cursor::new(file), + None, + BoxPath::from([fourcc("moof"), fourcc("traf"), fourcc("sbgp")]), + ) + .unwrap(); + assert_eq!(sbgp.len(), 1); + assert_eq!(sbgp[0].grouping_type, u32::from_be_bytes(*b"seig")); + assert_eq!(sbgp[0].entries.len(), 1); + assert_eq!(sbgp[0].entries[0].sample_count, 1); + assert_eq!(sbgp[0].entries[0].group_description_index, 65_537); +} + +#[test] +fn extract_box_as_decodes_compact_metadata_boxes() { + let mut elng = Elng::default(); + elng.extended_language = "en-US".into(); + let elng = encode_supported_box(&elng, &[]); + + let mut subs = Subs::default(); + subs.entry_count = 1; + subs.entries = vec![SubsEntry { + sample_delta: 7, + subsample_count: 1, + subsamples: vec![SubsSample { + subsample_size: 11, + subsample_priority: 2, + discardable: 0, + codec_specific_parameters: 0x01020304, + }], + }]; + let subs = encode_supported_box(&subs, &[]); + + let stbl = encode_supported_box(&Stbl, &subs); + let minf = encode_supported_box(&Minf, &stbl); + let mdia = encode_supported_box(&Mdia, &[elng, minf].concat()); + let trak = encode_supported_box(&Trak, &mdia); + + let mut leva = Leva::default(); + leva.level_count = 1; + leva.levels = vec![LevaLevel { + track_id: 9, + assignment_type: 4, + sub_track_id: 11, + ..LevaLevel::default() + }]; + let mut trep = Trep::default(); + trep.track_id = 9; + let trep = encode_supported_box(&trep, &encode_supported_box(&leva, &[])); + let mvex = encode_supported_box(&Mvex, &trep); + + let mut ssix = Ssix::default(); + ssix.subsegment_count = 1; + ssix.subsegments = vec![SsixSubsegment { + range_count: 1, + ranges: vec![SsixRange { + level: 3, + range_size: 0x44, + }], + }]; + let ssix = encode_supported_box(&ssix, &[]); + + let moov = encode_supported_box(&Moov, &[trak, mvex].concat()); + let file = [moov, ssix].concat(); + + let extracted_elng = extract_box_as::<_, Elng>( + &mut Cursor::new(file.clone()), + None, + BoxPath::from([ + fourcc("moov"), + fourcc("trak"), + fourcc("mdia"), + fourcc("elng"), + ]), + ) + .unwrap(); + assert_eq!(extracted_elng.len(), 1); + assert_eq!(extracted_elng[0].extended_language, "en-US"); + + let extracted_subs = extract_box_as::<_, Subs>( + &mut Cursor::new(file.clone()), + None, + BoxPath::from([ + fourcc("moov"), + fourcc("trak"), + fourcc("mdia"), + fourcc("minf"), + fourcc("stbl"), + fourcc("subs"), + ]), + ) + .unwrap(); + assert_eq!(extracted_subs.len(), 1); + assert_eq!(extracted_subs[0].entries[0].sample_delta, 7); + assert_eq!( + extracted_subs[0].entries[0].subsamples[0].codec_specific_parameters, + 0x01020304 + ); + + let extracted_leva = extract_box_as::<_, Leva>( + &mut Cursor::new(file.clone()), + None, + BoxPath::from([ + fourcc("moov"), + fourcc("mvex"), + fourcc("trep"), + fourcc("leva"), + ]), + ) + .unwrap(); + assert_eq!(extracted_leva.len(), 1); + assert_eq!(extracted_leva[0].levels[0].track_id, 9); + assert_eq!(extracted_leva[0].levels[0].sub_track_id, 11); + + let extracted_ssix = extract_box_as::<_, Ssix>( + &mut Cursor::new(file), + None, + BoxPath::from([fourcc("ssix")]), + ) + .unwrap(); + assert_eq!(extracted_ssix.len(), 1); + assert_eq!(extracted_ssix[0].subsegments[0].ranges[0].level, 3); + assert_eq!(extracted_ssix[0].subsegments[0].ranges[0].range_size, 0x44); +} + +#[test] +fn extract_box_as_decodes_event_message_boxes() { + let file = build_event_message_movie_file(); + + let evte = extract_box_as::<_, EventMessageSampleEntry>( + &mut Cursor::new(file.clone()), + None, + BoxPath::from([ + fourcc("moov"), + fourcc("trak"), + fourcc("mdia"), + fourcc("minf"), + fourcc("stbl"), + fourcc("stsd"), + fourcc("evte"), + ]), + ) + .unwrap(); + assert_eq!(evte.len(), 1); + assert_eq!(evte[0].sample_entry.data_reference_index, 1); + + let silb = extract_box_as::<_, Silb>( + &mut Cursor::new(file.clone()), + None, + BoxPath::from([ + fourcc("moov"), + fourcc("trak"), + fourcc("mdia"), + fourcc("minf"), + fourcc("stbl"), + fourcc("stsd"), + fourcc("evte"), + fourcc("silb"), + ]), + ) + .unwrap(); + assert_eq!(silb.len(), 1); + assert_eq!(silb[0].scheme_count, 2); + assert_eq!(silb[0].schemes[0].scheme_id_uri, "urn:mpeg:dash:event:2012"); + assert_eq!(silb[0].schemes[1].value, "splice"); + assert!(silb[0].schemes[1].at_least_one_flag); + assert!(silb[0].other_schemes_flag); + + let emib = extract_box_as::<_, Emib>( + &mut Cursor::new(file.clone()), + None, + BoxPath::from([fourcc("emib")]), + ) + .unwrap(); + assert_eq!(emib.len(), 1); + assert_eq!(emib[0].presentation_time_delta, -1_000); + assert_eq!(emib[0].event_duration, 2_000); + assert_eq!(emib[0].scheme_id_uri, "urn:scte:scte35:2013:bin"); + assert_eq!(emib[0].message_data, vec![0x01, 0x02, 0x03]); + + let emeb = extract_box_as::<_, Emeb>( + &mut Cursor::new(file), + None, + BoxPath::from([fourcc("emeb")]), + ) + .unwrap(); + assert_eq!(emeb.len(), 1); +} + #[test] fn extract_boxes_bytes_match_shared_fixture_box_ranges() { let sample = std::fs::read(fixture_path("sample.mp4")).unwrap(); @@ -424,7 +747,7 @@ fn extract_box_rejects_empty_paths() { } #[test] -fn extract_boxes_match_shared_fixture_reference_paths() { +fn extract_boxes_match_shared_fixture_expected_paths() { let sample = std::fs::read(fixture_path("sample.mp4")).unwrap(); let ftyp = extract_box( &mut Cursor::new(sample.clone()), diff --git a/tests/parity_harness.rs b/tests/parity_harness.rs index fcd91b6..8959e23 100644 --- a/tests/parity_harness.rs +++ b/tests/parity_harness.rs @@ -12,6 +12,9 @@ use mp4forge::probe::{ ProbeError, ProbeOptions, TrackCodec, average_sample_bitrate, average_segment_bitrate, find_idr_frames, max_sample_bitrate, max_segment_bitrate, probe, probe_with_options, }; +use mp4forge::sidx::{ + TopLevelSidxPlanOptions, apply_top_level_sidx_plan_bytes, plan_top_level_sidx_update_bytes, +}; use mp4forge::walk::BoxPath; use support::{fixture_path, read_golden, read_text, temp_output_dir, write_temp_file}; @@ -541,6 +544,84 @@ fn fragmented_and_encrypted_cli_surfaces_match_shared_fixture_expectations() { let _ = fs::remove_dir_all(÷_output_dir); } +#[test] +fn probe_surfaces_stay_stable_after_top_level_sidx_refresh_on_shared_fixture() { + let input = fs::read(fixture_path("sample_fragmented.mp4")).unwrap(); + let original_summary = probe(&mut Cursor::new(&input)).unwrap(); + let original_report = cli_probe::build_report(&mut Cursor::new(&input)).unwrap(); + + let plan = plan_top_level_sidx_update_bytes( + &input, + TopLevelSidxPlanOptions { + add_if_not_exists: true, + non_zero_ept: false, + }, + ) + .unwrap() + .unwrap(); + let output = apply_top_level_sidx_plan_bytes(&input, &plan).unwrap(); + + let refreshed_summary = probe(&mut Cursor::new(&output)).unwrap(); + let refreshed_report = cli_probe::build_report(&mut Cursor::new(&output)).unwrap(); + let sidx = extract_box( + &mut Cursor::new(&output), + None, + BoxPath::from([fourcc("sidx")]), + ) + .unwrap(); + + assert_eq!(sidx.len(), 1); + assert_eq!(original_report, refreshed_report); + assert_eq!(original_summary.major_brand, refreshed_summary.major_brand); + assert_eq!( + original_summary.minor_version, + refreshed_summary.minor_version + ); + assert_eq!( + original_summary.compatible_brands, + refreshed_summary.compatible_brands + ); + assert_eq!(original_summary.fast_start, refreshed_summary.fast_start); + assert_eq!(original_summary.timescale, refreshed_summary.timescale); + assert_eq!(original_summary.duration, refreshed_summary.duration); + assert_eq!(original_summary.tracks, refreshed_summary.tracks); + assert_eq!( + original_summary.segments.len(), + refreshed_summary.segments.len() + ); + + let offset_delta = sidx[0].size(); + for (original_segment, refreshed_segment) in original_summary + .segments + .iter() + .zip(refreshed_summary.segments.iter()) + { + assert_eq!(original_segment.track_id, refreshed_segment.track_id); + assert_eq!( + original_segment.moof_offset + offset_delta, + refreshed_segment.moof_offset + ); + assert_eq!( + original_segment.base_media_decode_time, + refreshed_segment.base_media_decode_time + ); + assert_eq!( + original_segment.default_sample_duration, + refreshed_segment.default_sample_duration + ); + assert_eq!( + original_segment.sample_count, + refreshed_segment.sample_count + ); + assert_eq!(original_segment.duration, refreshed_segment.duration); + assert_eq!( + original_segment.composition_time_offset, + refreshed_segment.composition_time_offset + ); + assert_eq!(original_segment.size, refreshed_segment.size); + } +} + fn expected_bitrate( summary: &mp4forge::probe::ProbeInfo, track: &mp4forge::probe::TrackInfo, diff --git a/tests/probe.rs b/tests/probe.rs index 8f96fda..a90a611 100644 --- a/tests/probe.rs +++ b/tests/probe.rs @@ -4,39 +4,55 @@ use std::io::Cursor; use mp4forge::boxes::AnyTypeBox; use mp4forge::boxes::av1::AV1CodecConfiguration; -use mp4forge::boxes::etsi_ts_102_366::Dac3; +use mp4forge::boxes::avs3::Av3c; +use mp4forge::boxes::etsi_ts_102_366::{Dac3, Dec3, Ec3Substream}; +use mp4forge::boxes::etsi_ts_103_190::Dac4; +use mp4forge::boxes::flac::{DfLa, FlacMetadataBlock}; use mp4forge::boxes::iso14496_12::{ - AVCDecoderConfiguration, AudioSampleEntry, Btrt, Colr, Ctts, CttsEntry, Edts, Elst, ElstEntry, - Fiel, Frma, Ftyp, HEVCDecoderConfiguration, Hdlr, Mdhd, Mdia, Minf, Moof, Moov, Mvhd, Pasp, - SampleEntry, Schm, Sinf, Stbl, Stco, Stsc, StscEntry, Stsd, Stsz, Stts, SttsEntry, - TFHD_DEFAULT_SAMPLE_DURATION_PRESENT, TFHD_DEFAULT_SAMPLE_SIZE_PRESENT, - TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT, TRUN_SAMPLE_DURATION_PRESENT, - TRUN_SAMPLE_SIZE_PRESENT, TextSubtitleSampleEntry, Tfdt, Tfhd, Tkhd, Traf, Trak, Trun, - TrunEntry, VisualSampleEntry, XMLSubtitleSampleEntry, + AVCDecoderConfiguration, AlbumLoudnessInfo, AudioSampleEntry, Btrt, Clap, CoLL, Colr, Ctts, + CttsEntry, Edts, Elng, Elst, ElstEntry, Fiel, Frma, Ftyp, HEVCDecoderConfiguration, Hdlr, + LoudnessEntry, LoudnessMeasurement, Ludt, Mdhd, Mdia, Meta, Minf, Moof, Moov, Mvhd, Nmhd, Pasp, + Prft, SampleEntry, Schm, Sinf, SmDm, SphericalVideoV1Metadata, Stbl, Stco, Sthd, Stsc, + StscEntry, Stsd, Stsz, Stts, SttsEntry, TFHD_DEFAULT_SAMPLE_DURATION_PRESENT, + TFHD_DEFAULT_SAMPLE_SIZE_PRESENT, TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT, + TRUN_SAMPLE_DURATION_PRESENT, TRUN_SAMPLE_SIZE_PRESENT, TextSubtitleSampleEntry, Tfdt, Tfhd, + Tkhd, TrackLoudnessInfo, Traf, Trak, Trun, TrunEntry, UUID_FRAGMENT_ABSOLUTE_TIMING, + UUID_FRAGMENT_RUN_TABLE, UUID_SAMPLE_ENCRYPTION, UUID_SPHERICAL_VIDEO_V1, Udta, Uuid, + UuidFragmentAbsoluteTiming, UuidFragmentRunEntry, UuidFragmentRunTable, UuidPayload, + VisualSampleEntry, XMLSubtitleSampleEntry, }; use mp4forge::boxes::iso14496_14::{ DECODER_CONFIG_DESCRIPTOR_TAG, DECODER_SPECIFIC_INFO_TAG, DecoderConfigDescriptor, Descriptor, Esds, }; +use mp4forge::boxes::iso14496_15::VVCDecoderConfiguration; use mp4forge::boxes::iso14496_30::{WVTTSampleEntry, WebVTTConfigurationBox, WebVTTSourceLabelBox}; use mp4forge::boxes::iso23001_5::PcmC; +use mp4forge::boxes::iso23001_7::{SENC_USE_SUBSAMPLE_ENCRYPTION, Senc, SencSample, SencSubsample}; +use mp4forge::boxes::metadata::Id32; +use mp4forge::boxes::mpeg_h::MhaC; use mp4forge::boxes::opus::DOps; use mp4forge::boxes::vp::VpCodecConfiguration; use mp4forge::codec::{CodecBox, MutableBox, marshal}; use mp4forge::probe::{ AacProfileInfo, EditListEntry, ProbeOptions, TrackCodec, TrackCodecDetails, TrackCodecFamily, average_sample_bitrate, average_segment_bitrate, detect_aac_profile, find_idr_frames, - max_sample_bitrate, max_segment_bitrate, probe, probe_bytes, probe_bytes_with_options, - probe_codec_detailed, probe_codec_detailed_bytes, probe_codec_detailed_bytes_with_options, - probe_codec_detailed_with_options, probe_detailed, probe_detailed_bytes, - probe_detailed_bytes_with_options, probe_detailed_with_options, probe_fra, probe_fra_bytes, - probe_fra_codec_detailed, probe_fra_codec_detailed_bytes, probe_fra_detailed, + max_sample_bitrate, max_segment_bitrate, normalized_codec_family_name, probe, probe_bytes, + probe_bytes_with_options, probe_codec_detailed, probe_codec_detailed_bytes, + probe_codec_detailed_bytes_with_options, probe_codec_detailed_with_options, probe_detailed, + probe_detailed_bytes, probe_detailed_bytes_with_options, probe_detailed_with_options, + probe_extended_media_characteristics, probe_extended_media_characteristics_bytes, probe_fra, + probe_fra_bytes, probe_fra_codec_detailed, probe_fra_codec_detailed_bytes, probe_fra_detailed, probe_fra_detailed_bytes, probe_media_characteristics, probe_media_characteristics_bytes, probe_media_characteristics_bytes_with_options, probe_media_characteristics_with_options, probe_with_options, }; use mp4forge::{BoxInfo, FourCc}; +mod support; + +use support::{build_encrypted_fragmented_video_file, build_event_message_movie_file}; + #[test] fn probe_summarizes_movie_tracks_samples_and_codecs() { let file = build_movie_file(); @@ -217,6 +233,46 @@ fn probe_detailed_exposes_handler_language_sample_entry_and_codec_family() { assert_eq!(audio.sample_rate, Some(48_000)); } +#[test] +fn probe_detailed_prefers_extended_language_box_when_present() { + let file = build_movie_file_with_extended_language(); + let mut reader = Cursor::new(file); + + let info = probe_detailed(&mut reader).unwrap(); + + assert_eq!(info.tracks.len(), 1); + assert_eq!(info.tracks[0].summary.track_id, 1); + assert_eq!(info.tracks[0].language.as_deref(), Some("en-US")); +} + +#[test] +fn probe_detailed_lightweight_is_stable_when_movie_carries_user_metadata_boxes() { + let options = ProbeOptions::lightweight(); + let expected = + probe_detailed_with_options(&mut Cursor::new(build_movie_file()), options).unwrap(); + let actual = probe_detailed_with_options( + &mut Cursor::new(build_movie_file_with_user_metadata()), + options, + ) + .unwrap(); + + assert_eq!(actual, expected); +} + +#[test] +fn probe_detailed_lightweight_is_stable_when_movie_carries_legacy_uuid_boxes() { + let options = ProbeOptions::lightweight(); + let expected = + probe_detailed_with_options(&mut Cursor::new(build_movie_file()), options).unwrap(); + let actual = probe_detailed_with_options( + &mut Cursor::new(build_movie_file_with_legacy_uuid_boxes()), + options, + ) + .unwrap(); + + assert_eq!(actual, expected); +} + #[test] fn probe_detailed_bytes_matches_cursor_based_probe_detailed() { let file = build_movie_file(); @@ -331,6 +387,46 @@ fn probe_and_probe_fra_summarize_fragment_runs() { assert_eq!(second.size, 36); } +#[test] +fn probe_fragment_summary_stays_stable_when_prft_precedes_each_moof() { + let file = build_fragment_file_with_prft(); + + let mut reader = Cursor::new(file.clone()); + let info = probe(&mut reader).unwrap(); + + let mut reader = Cursor::new(file); + let fra_info = probe_fra(&mut reader).unwrap(); + + assert_eq!(fra_info, info); + assert!(info.tracks.is_empty()); + assert_eq!(info.segments.len(), 2); + + let prft_size = build_prft_box_v0(7, 0x0000_0001_0203_0405, 9_000).len() as u64; + + let first = &info.segments[0]; + assert_eq!(first.track_id, 7); + assert_eq!(first.moof_offset, 24 + prft_size); + assert_eq!(first.base_media_decode_time, 9_000); + assert_eq!(first.default_sample_duration, 1_000); + assert_eq!(first.sample_count, 2); + assert_eq!(first.duration, 3_000); + assert_eq!(first.composition_time_offset, 500); + assert_eq!(first.size, 10); + + let second = &info.segments[1]; + assert_eq!(second.track_id, 7); + assert_eq!( + second.moof_offset, + 24 + prft_size + build_fragment_moof_one().len() as u64 + prft_size + ); + assert_eq!(second.base_media_decode_time, 12_000); + assert_eq!(second.default_sample_duration, 1_024); + assert_eq!(second.sample_count, 3); + assert_eq!(second.duration, 3_072); + assert_eq!(second.composition_time_offset, 0); + assert_eq!(second.size, 36); +} + #[test] fn probe_fra_detailed_bytes_matches_cursor_based_probe_fra_detailed() { let file = build_fragment_file(); @@ -375,6 +471,136 @@ fn probe_detailed_recognizes_av01_track_family() { assert_eq!(track.summary.samples.len(), 1); } +#[test] +fn probe_detailed_surfaces_new_sample_entry_types_without_new_family_variants() { + { + let mut reader = Cursor::new(build_ec3_movie_file()); + let info = probe_detailed(&mut reader).unwrap(); + let track = &info.tracks[0]; + assert_eq!(track.summary.codec, TrackCodec::Unknown); + assert_eq!(track.codec_family, TrackCodecFamily::Unknown); + assert_eq!(track.sample_entry_type, Some(fourcc("ec-3"))); + assert_eq!(track.channel_count, Some(6)); + assert_eq!(track.sample_rate, Some(48_000)); + assert_eq!( + normalized_codec_family_name( + track.codec_family, + track.sample_entry_type, + track.original_format, + ), + "unknown" + ); + } + + { + let mut reader = Cursor::new(build_ac4_movie_file()); + let info = probe_detailed(&mut reader).unwrap(); + let track = &info.tracks[0]; + assert_eq!(track.summary.codec, TrackCodec::Unknown); + assert_eq!(track.codec_family, TrackCodecFamily::Unknown); + assert_eq!(track.sample_entry_type, Some(fourcc("ac-4"))); + assert_eq!(track.channel_count, Some(2)); + assert_eq!(track.sample_rate, Some(48_000)); + assert_eq!( + normalized_codec_family_name( + track.codec_family, + track.sample_entry_type, + track.original_format, + ), + "unknown" + ); + } + + { + let mut reader = Cursor::new(build_vvc_movie_file()); + let info = probe_detailed(&mut reader).unwrap(); + let track = &info.tracks[0]; + assert_eq!(track.summary.codec, TrackCodec::Unknown); + assert_eq!(track.codec_family, TrackCodecFamily::Unknown); + assert_eq!(track.sample_entry_type, Some(fourcc("vvc1"))); + assert_eq!(track.display_width, Some(640)); + assert_eq!(track.display_height, Some(360)); + assert_eq!( + normalized_codec_family_name( + track.codec_family, + track.sample_entry_type, + track.original_format, + ), + "unknown" + ); + } + + { + let mut reader = Cursor::new(build_avs3_movie_file()); + let info = probe_detailed(&mut reader).unwrap(); + let track = &info.tracks[0]; + assert_eq!(track.summary.codec, TrackCodec::Unknown); + assert_eq!(track.codec_family, TrackCodecFamily::Unknown); + assert_eq!(track.sample_entry_type, Some(fourcc("avs3"))); + assert_eq!(track.display_width, Some(640)); + assert_eq!(track.display_height, Some(360)); + assert_eq!( + normalized_codec_family_name( + track.codec_family, + track.sample_entry_type, + track.original_format, + ), + "avs3" + ); + } + + { + let mut reader = Cursor::new(build_flac_movie_file()); + let info = probe_detailed(&mut reader).unwrap(); + let track = &info.tracks[0]; + assert_eq!(track.summary.codec, TrackCodec::Unknown); + assert_eq!(track.codec_family, TrackCodecFamily::Unknown); + assert_eq!(track.sample_entry_type, Some(fourcc("fLaC"))); + assert_eq!(track.channel_count, Some(2)); + assert_eq!(track.sample_rate, Some(48_000)); + assert_eq!( + normalized_codec_family_name( + track.codec_family, + track.sample_entry_type, + track.original_format, + ), + "flac" + ); + } + + { + let mut reader = Cursor::new(build_mha1_movie_file()); + let info = probe_detailed(&mut reader).unwrap(); + let track = &info.tracks[0]; + assert_eq!(track.summary.codec, TrackCodec::Unknown); + assert_eq!(track.codec_family, TrackCodecFamily::Unknown); + assert_eq!(track.sample_entry_type, Some(fourcc("mha1"))); + assert_eq!(track.channel_count, Some(2)); + assert_eq!(track.sample_rate, Some(48_000)); + assert_eq!( + normalized_codec_family_name( + track.codec_family, + track.sample_entry_type, + track.original_format, + ), + "mpeg_h" + ); + } +} + +#[test] +fn probe_codec_detailed_keeps_unknown_codec_details_for_new_family_strings() { + for file in [ + build_avs3_movie_file(), + build_flac_movie_file(), + build_mha1_movie_file(), + ] { + let info = probe_codec_detailed(&mut Cursor::new(file)).unwrap(); + let track = &info.tracks[0]; + assert_eq!(track.codec_details, TrackCodecDetails::Unknown); + } +} + #[test] fn probe_codec_detailed_exposes_richer_landed_codec_details() { { @@ -539,6 +765,26 @@ fn probe_codec_detailed_exposes_richer_landed_codec_details() { } } +#[test] +fn probe_media_characteristics_reports_event_message_track_metadata() { + let mut reader = Cursor::new(build_event_message_movie_file()); + let info = probe_media_characteristics(&mut reader).unwrap(); + let track = &info.tracks[0]; + + assert_eq!(track.summary.codec_family, TrackCodecFamily::Unknown); + assert_eq!(track.summary.sample_entry_type, Some(fourcc("evte"))); + assert_eq!(track.summary.handler_type, Some(fourcc("subt"))); + assert_eq!( + track + .media_characteristics + .declared_bitrate + .as_ref() + .map(|value| (value.buffer_size_db, value.max_bitrate, value.avg_bitrate)), + Some((32_768, 4_000_000, 2_500_000)) + ); + assert_eq!(track.codec_details, TrackCodecDetails::Unknown); +} + #[test] fn probe_detailed_reports_protected_sample_entry_metadata() { let file = build_encrypted_video_movie_file(); @@ -589,6 +835,39 @@ fn probe_codec_detailed_reports_protected_hevc_codec_details() { } } +#[test] +fn probe_detailed_handles_fragmented_encrypted_metadata_boxes() { + let file = build_encrypted_fragmented_video_file(); + let mut reader = Cursor::new(file); + + let info = probe_detailed(&mut reader).unwrap(); + + assert_eq!(info.tracks.len(), 1); + assert_eq!(info.segments.len(), 1); + + let track = &info.tracks[0]; + assert_eq!(track.summary.track_id, 1); + assert_eq!(track.summary.codec, TrackCodec::Avc1); + assert!(track.summary.encrypted); + assert_eq!(track.codec_family, TrackCodecFamily::Avc); + assert_eq!(track.sample_entry_type, Some(fourcc("encv"))); + assert_eq!(track.original_format, Some(fourcc("avc1"))); + assert_eq!( + track + .protection_scheme + .as_ref() + .map(|value| (value.scheme_type, value.scheme_version)), + Some((fourcc("cenc"), 0x0001_0000)) + ); + + let segment = &info.segments[0]; + assert_eq!(segment.track_id, 1); + assert_eq!(segment.sample_count, 1); + assert_eq!(segment.default_sample_duration, 1_000); + assert_eq!(segment.duration, 1_000); + assert_eq!(segment.size, 4); +} + #[test] fn probe_media_characteristics_exposes_sample_entry_side_metadata() { let file = build_media_characteristics_movie_file(); @@ -638,6 +917,60 @@ fn probe_media_characteristics_exposes_sample_entry_side_metadata() { ); } +#[test] +fn probe_extended_media_characteristics_exposes_visual_sample_entry_side_metadata() { + let file = build_media_characteristics_movie_file(); + let mut reader = Cursor::new(file); + + let info = probe_extended_media_characteristics(&mut reader).unwrap(); + + assert_eq!(info.tracks.len(), 1); + let track = &info.tracks[0]; + assert_eq!( + track.visual_metadata.clean_aperture, + Some(mp4forge::probe::CleanApertureInfo { + width_numerator: 1_920, + width_denominator: 1, + height_numerator: 1_080, + height_denominator: 1, + horizontal_offset_numerator: 0, + horizontal_offset_denominator: 1, + vertical_offset_numerator: 0, + vertical_offset_denominator: 1, + }) + ); + assert_eq!( + track.visual_metadata.content_light_level, + Some(mp4forge::probe::ContentLightLevelInfo { + max_cll: 1_000, + max_fall: 400, + }) + ); + assert_eq!( + track.visual_metadata.mastering_display, + Some(mp4forge::probe::MasteringDisplayInfo { + primary_r_chromaticity_x: 34_000, + primary_r_chromaticity_y: 16_000, + primary_g_chromaticity_x: 13_250, + primary_g_chromaticity_y: 34_500, + primary_b_chromaticity_x: 7_500, + primary_b_chromaticity_y: 3_000, + white_point_chromaticity_x: 15_635, + white_point_chromaticity_y: 16_450, + luminance_max: 1_000_000, + luminance_min: 50, + }) + ); +} + +#[test] +fn probe_extended_media_characteristics_bytes_matches_cursor_based_probe() { + let file = build_media_characteristics_movie_file(); + let expected = probe_extended_media_characteristics(&mut Cursor::new(file.clone())).unwrap(); + let actual = probe_extended_media_characteristics_bytes(&file).unwrap(); + assert_eq!(actual, expected); +} + #[test] fn probe_media_characteristics_bytes_matches_cursor_based_probe() { let file = build_media_characteristics_movie_file(); @@ -680,7 +1013,7 @@ fn probe_bytes_propagates_decode_errors() { } #[test] -fn detect_aac_profile_matches_reference_cases() { +fn detect_aac_profile_matches_expected_cases() { let cases = [ ( aac_profile_esds(0x40, &[0x10, 0x00]), @@ -721,7 +1054,7 @@ fn detect_aac_profile_matches_reference_cases() { } #[test] -fn bitrate_helpers_match_reference_math() { +fn bitrate_helpers_match_expected_math() { let samples = [ sample_info(100, 10, 0), sample_info(200, 10, 0), @@ -772,6 +1105,90 @@ fn build_movie_file() -> Vec { [ftyp, moov, mdat].concat() } +fn build_movie_file_with_extended_language() -> Vec { + let ftyp = encode_supported_box( + &Ftyp { + major_brand: fourcc("isom"), + minor_version: 0x0200, + compatible_brands: vec![fourcc("isom"), fourcc("iso2"), fourcc("avc1")], + }, + &[], + ); + + let placeholder_moov = build_movie_moov_with_extended_language(&[0, 0]); + let mdat_payload = movie_mdat_payload(); + let mdat_data_offset = ftyp.len() as u64 + placeholder_moov.len() as u64 + 8; + let video_offsets = [mdat_data_offset, mdat_data_offset + 10]; + + let moov = build_movie_moov_with_extended_language(&video_offsets); + let mdat = encode_raw_box(fourcc("mdat"), &mdat_payload); + [ftyp, moov, mdat].concat() +} + +fn build_movie_file_with_user_metadata() -> Vec { + let ftyp = encode_supported_box( + &Ftyp { + major_brand: fourcc("isom"), + minor_version: 0x0200, + compatible_brands: vec![fourcc("isom"), fourcc("iso2"), fourcc("avc1")], + }, + &[], + ); + + let placeholder_moov = build_movie_moov_with_user_metadata(&[0, 0], &[0, 0]); + let spherical_uuid = build_spherical_uuid_box(); + let raw_uuid = build_raw_uuid_box(); + let mdat_payload = movie_mdat_payload(); + let mdat_data_offset = ftyp.len() as u64 + + placeholder_moov.len() as u64 + + spherical_uuid.len() as u64 + + raw_uuid.len() as u64 + + 8; + let video_offsets = [mdat_data_offset, mdat_data_offset + 10]; + let audio_offsets = [mdat_data_offset + 15, mdat_data_offset + 18]; + + let moov = build_movie_moov_with_user_metadata(&video_offsets, &audio_offsets); + let mdat = encode_raw_box(fourcc("mdat"), &mdat_payload); + [ftyp, moov, spherical_uuid, raw_uuid, mdat].concat() +} + +fn build_movie_file_with_legacy_uuid_boxes() -> Vec { + let ftyp = encode_supported_box( + &Ftyp { + major_brand: fourcc("isom"), + minor_version: 0x0200, + compatible_brands: vec![fourcc("isom"), fourcc("iso2"), fourcc("avc1")], + }, + &[], + ); + + let placeholder_moov = build_movie_moov(&[0, 0], &[0, 0]); + let fragment_timing_uuid = build_fragment_timing_uuid_box(); + let fragment_run_uuid = build_fragment_run_uuid_box(); + let sample_encryption_uuid = build_sample_encryption_uuid_box(); + let mdat_payload = movie_mdat_payload(); + let mdat_data_offset = ftyp.len() as u64 + + placeholder_moov.len() as u64 + + fragment_timing_uuid.len() as u64 + + fragment_run_uuid.len() as u64 + + sample_encryption_uuid.len() as u64 + + 8; + let video_offsets = [mdat_data_offset, mdat_data_offset + 10]; + let audio_offsets = [mdat_data_offset + 15, mdat_data_offset + 18]; + + let moov = build_movie_moov(&video_offsets, &audio_offsets); + let mdat = encode_raw_box(fourcc("mdat"), &mdat_payload); + [ + ftyp, + moov, + fragment_timing_uuid, + fragment_run_uuid, + sample_encryption_uuid, + mdat, + ] + .concat() +} + fn build_movie_moov(video_offsets: &[u64; 2], audio_offsets: &[u64; 2]) -> Vec { let mut mvhd = Mvhd::default(); mvhd.timescale = 1_000; @@ -785,6 +1202,35 @@ fn build_movie_moov(video_offsets: &[u64; 2], audio_offsets: &[u64; 2]) -> Vec Vec { + let mut mvhd = Mvhd::default(); + mvhd.timescale = 1_000; + mvhd.duration_v0 = 2_000; + mvhd.rate = 1 << 16; + mvhd.volume = 1 << 8; + mvhd.next_track_id = 3; + let mvhd = encode_supported_box(&mvhd, &[]); + let video = build_video_trak(video_offsets); + let audio = build_audio_trak(audio_offsets); + let udta = build_movie_user_metadata_box(); + encode_supported_box(&Moov, &[mvhd, video, audio, udta].concat()) +} + +fn build_movie_moov_with_extended_language(video_offsets: &[u64; 2]) -> Vec { + let mut mvhd = Mvhd::default(); + mvhd.timescale = 1_000; + mvhd.duration_v0 = 2_000; + mvhd.rate = 1 << 16; + mvhd.volume = 1 << 8; + mvhd.next_track_id = 2; + let mvhd = encode_supported_box(&mvhd, &[]); + let video = build_video_trak_with_extended_language(video_offsets); + encode_supported_box(&Moov, &[mvhd, video].concat()) +} + fn build_video_trak(chunk_offsets: &[u64; 2]) -> Vec { let mut tkhd = Tkhd::default(); tkhd.track_id = 1; @@ -874,6 +1320,90 @@ fn build_video_trak(chunk_offsets: &[u64; 2]) -> Vec { encode_supported_box(&Trak, &[tkhd, edts, mdia].concat()) } +fn build_video_trak_with_extended_language(chunk_offsets: &[u64; 2]) -> Vec { + let mut tkhd = Tkhd::default(); + tkhd.track_id = 1; + tkhd.duration_v0 = 3_072; + tkhd.width = u32::from(320_u16) << 16; + tkhd.height = u32::from(180_u16) << 16; + let tkhd = encode_supported_box(&tkhd, &[]); + + let mut mdhd = Mdhd::default(); + mdhd.timescale = 90_000; + mdhd.duration_v0 = 3_072; + mdhd.language = [5, 14, 7]; + let mdhd = encode_supported_box(&mdhd, &[]); + + let mut elng = Elng::default(); + elng.extended_language = "en-US".into(); + let elng = encode_supported_box(&elng, &[]); + + let hdlr = handler_box("vide", "VideoHandler"); + + let mut stsd = Stsd::default(); + stsd.entry_count = 1; + let avc1 = encode_supported_box( + &video_sample_entry(), + &encode_supported_box(&avc_config(), &[]), + ); + let stsd = encode_supported_box(&stsd, &avc1); + + let mut stco = Stco::default(); + stco.entry_count = 2; + stco.chunk_offset = chunk_offsets.to_vec(); + let stco = encode_supported_box(&stco, &[]); + + let mut stts = Stts::default(); + stts.entry_count = 1; + stts.entries = vec![SttsEntry { + sample_count: 3, + sample_delta: 1_024, + }]; + let stts = encode_supported_box(&stts, &[]); + + let mut ctts = Ctts::default(); + ctts.entry_count = 2; + ctts.entries = vec![ + CttsEntry { + sample_count: 2, + sample_offset_v0: 256, + ..CttsEntry::default() + }, + CttsEntry { + sample_count: 1, + sample_offset_v0: 128, + ..CttsEntry::default() + }, + ]; + let ctts = encode_supported_box(&ctts, &[]); + + let mut stsc = Stsc::default(); + stsc.entry_count = 2; + stsc.entries = vec![ + StscEntry { + first_chunk: 1, + samples_per_chunk: 2, + sample_description_index: 1, + }, + StscEntry { + first_chunk: 2, + samples_per_chunk: 1, + sample_description_index: 1, + }, + ]; + let stsc = encode_supported_box(&stsc, &[]); + + let mut stsz = Stsz::default(); + stsz.sample_count = 3; + stsz.entry_size = vec![5, 5, 5]; + let stsz = encode_supported_box(&stsz, &[]); + + let stbl = encode_supported_box(&Stbl, &[stsd, stco, stts, ctts, stsc, stsz].concat()); + let minf = encode_supported_box(&Minf, &stbl); + let mdia = encode_supported_box(&Mdia, &[mdhd, elng, hdlr, minf].concat()); + encode_supported_box(&Trak, &[tkhd, mdia].concat()) +} + fn build_audio_trak(chunk_offsets: &[u64; 2]) -> Vec { let mut tkhd = Tkhd::default(); tkhd.track_id = 2; @@ -928,6 +1458,135 @@ fn build_audio_trak(chunk_offsets: &[u64; 2]) -> Vec { encode_supported_box(&Trak, &[tkhd, mdia].concat()) } +fn build_movie_user_metadata_box() -> Vec { + let mut id32 = Id32::default(); + id32.language = "eng".into(); + id32.id3v2_data = b"ID3\x04".to_vec(); + let id32 = encode_supported_box(&id32, &[]); + let meta = encode_supported_box(&Meta::default(), &id32); + + let mut track_loudness = TrackLoudnessInfo::default(); + track_loudness.set_version(1); + track_loudness.entries = vec![LoudnessEntry { + eq_set_id: 7, + downmix_id: 12, + drc_set_id: 18, + bs_sample_peak_level: 528, + bs_true_peak_level: 801, + measurement_system_for_tp: 4, + reliability_for_tp: 6, + measurements: vec![LoudnessMeasurement { + method_definition: 7, + method_value: 8, + measurement_system: 9, + reliability: 10, + }], + }]; + let track_loudness = encode_supported_box(&track_loudness, &[]); + + let mut album_loudness = AlbumLoudnessInfo::default(); + album_loudness.set_version(0); + album_loudness.entries = vec![LoudnessEntry { + downmix_id: 9, + drc_set_id: 17, + bs_sample_peak_level: 274, + bs_true_peak_level: 291, + measurement_system_for_tp: 2, + reliability_for_tp: 3, + measurements: vec![LoudnessMeasurement { + method_definition: 1, + method_value: 2, + measurement_system: 4, + reliability: 5, + }], + ..LoudnessEntry::default() + }]; + let album_loudness = encode_supported_box(&album_loudness, &[]); + let loudness = encode_supported_box(&Ludt, &[track_loudness, album_loudness].concat()); + + encode_supported_box(&Udta, &[meta, loudness].concat()) +} + +fn build_spherical_uuid_box() -> Vec { + encode_supported_box( + &Uuid { + user_type: UUID_SPHERICAL_VIDEO_V1, + payload: UuidPayload::SphericalVideoV1(SphericalVideoV1Metadata { + xml_data: b"S".to_vec(), + }), + }, + &[], + ) +} + +fn build_raw_uuid_box() -> Vec { + encode_supported_box( + &Uuid { + user_type: [ + 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, + 0xcd, 0xef, + ], + payload: UuidPayload::Raw(vec![0xde, 0xad, 0xbe]), + }, + &[], + ) +} + +fn build_fragment_timing_uuid_box() -> Vec { + encode_supported_box( + &Uuid { + user_type: UUID_FRAGMENT_ABSOLUTE_TIMING, + payload: UuidPayload::FragmentAbsoluteTiming(UuidFragmentAbsoluteTiming { + version: 1, + flags: 0, + fragment_absolute_time: 0x0001_05c6_49bd_a400, + fragment_absolute_duration: 0x0000_0000_0005_4600, + }), + }, + &[], + ) +} + +fn build_fragment_run_uuid_box() -> Vec { + encode_supported_box( + &Uuid { + user_type: UUID_FRAGMENT_RUN_TABLE, + payload: UuidPayload::FragmentRunTable(UuidFragmentRunTable { + version: 1, + flags: 0, + fragment_count: 1, + entries: vec![UuidFragmentRunEntry { + fragment_absolute_time: 0x0001_05c6_49c2_ea00, + fragment_absolute_duration: 0x0000_0000_0005_4600, + }], + }), + }, + &[], + ) +} + +fn build_sample_encryption_uuid_box() -> Vec { + let mut sample_encryption = Senc::default(); + sample_encryption.set_version(0); + sample_encryption.set_flags(SENC_USE_SUBSAMPLE_ENCRYPTION); + sample_encryption.sample_count = 1; + sample_encryption.samples = vec![SencSample { + initialization_vector: vec![1, 2, 3, 4, 5, 6, 7, 8], + subsamples: vec![SencSubsample { + bytes_of_clear_data: 5, + bytes_of_protected_data: 16, + }], + }]; + + encode_supported_box( + &Uuid { + user_type: UUID_SAMPLE_ENCRYPTION, + payload: UuidPayload::SampleEncryption(sample_encryption), + }, + &[], + ) +} + fn build_fragment_file() -> Vec { let ftyp = encode_supported_box( &Ftyp { @@ -942,6 +1601,30 @@ fn build_fragment_file() -> Vec { [ftyp, moof_one, moof_two].concat() } +fn build_fragment_file_with_prft() -> Vec { + let ftyp = encode_supported_box( + &Ftyp { + major_brand: fourcc("iso6"), + minor_version: 1, + compatible_brands: vec![fourcc("iso6"), fourcc("dash")], + }, + &[], + ); + let prft_one = build_prft_box_v0(7, 0x0000_0001_0203_0405, 9_000); + let prft_two = build_prft_box_v0(7, 0x0000_0006_0708_090a, 12_000); + let moof_one = build_fragment_moof_one(); + let moof_two = build_fragment_moof_two(); + [ftyp, prft_one, moof_one, prft_two, moof_two].concat() +} + +fn build_prft_box_v0(reference_track_id: u32, ntp_timestamp: u64, media_time_v0: u32) -> Vec { + let mut prft = Prft::default(); + prft.reference_track_id = reference_track_id; + prft.ntp_timestamp = ntp_timestamp; + prft.media_time_v0 = media_time_v0; + encode_supported_box(&prft, &[]) +} + fn build_fragment_moof_one() -> Vec { let tfhd = { let mut tfhd = Tfhd::default(); @@ -1293,6 +1976,58 @@ fn build_ac3_trak(chunk_offsets: &[u64; 1]) -> Vec { build_single_sample_audio_trak(1, 48_000, 1_536, sample_entry, chunk_offsets, 4) } +fn build_ec3_movie_file() -> Vec { + build_single_track_movie_file( + vec![fourcc("isom"), fourcc("iso8"), fourcc("ec-3")], + build_ec3_trak, + vec![0x25, 0x26, 0x27, 0x28], + ) +} + +fn build_ec3_trak(chunk_offsets: &[u64; 1]) -> Vec { + let sample_entry = encode_supported_box( + &audio_sample_entry_with_type("ec-3", 6, 48_000), + &encode_supported_box( + &Dec3 { + data_rate: 448, + num_ind_sub: 0, + ec3_substreams: vec![Ec3Substream { + fscod: 2, + bsid: 0x10, + acmod: 4, + ..Ec3Substream::default() + }], + reserved: Vec::new(), + }, + &[], + ), + ); + build_single_sample_audio_trak(1, 48_000, 1_536, sample_entry, chunk_offsets, 4) +} + +fn build_ac4_movie_file() -> Vec { + build_single_track_movie_file( + vec![fourcc("isom"), fourcc("iso8"), fourcc("ac-4")], + build_ac4_trak, + vec![0x29, 0x2a, 0x2b, 0x2c], + ) +} + +fn build_ac4_trak(chunk_offsets: &[u64; 1]) -> Vec { + let sample_entry = encode_supported_box( + &audio_sample_entry_with_type("ac-4", 2, 48_000), + &encode_supported_box( + &Dac4 { + data: vec![ + 0x22, 0x00, 0x80, 0x01, 0xf4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ], + }, + &[], + ), + ); + build_single_sample_audio_trak(1, 48_000, 1_024, sample_entry, chunk_offsets, 4) +} + fn build_pcm_movie_file() -> Vec { build_single_track_movie_file( vec![fourcc("isom"), fourcc("iso8"), fourcc("ipcm")], @@ -1319,7 +2054,15 @@ fn build_stpp_movie_file() -> Vec { fn build_stpp_trak(chunk_offsets: &[u64; 1]) -> Vec { let sample_entry = encode_supported_box(&xml_subtitle_sample_entry(), &[]); - build_single_sample_subtitle_trak(1, 1_000, 1_000, sample_entry, chunk_offsets, 4) + build_single_sample_subtitle_trak( + 1, + 1_000, + 1_000, + subtitle_media_header_box(), + sample_entry, + chunk_offsets, + 4, + ) } fn build_sbtt_movie_file() -> Vec { @@ -1332,7 +2075,15 @@ fn build_sbtt_movie_file() -> Vec { fn build_sbtt_trak(chunk_offsets: &[u64; 1]) -> Vec { let sample_entry = encode_supported_box(&text_subtitle_sample_entry(), &[]); - build_single_sample_subtitle_trak(1, 1_000, 1_000, sample_entry, chunk_offsets, 4) + build_single_sample_subtitle_trak( + 1, + 1_000, + 1_000, + subtitle_media_header_box(), + sample_entry, + chunk_offsets, + 4, + ) } fn build_wvtt_movie_file() -> Vec { @@ -1362,7 +2113,15 @@ fn build_wvtt_trak(chunk_offsets: &[u64; 1]) -> Vec { ] .concat(), ); - build_single_sample_subtitle_trak(1, 1_000, 1_000, sample_entry, chunk_offsets, 4) + build_single_sample_subtitle_trak( + 1, + 1_000, + 1_000, + null_media_header_box(), + sample_entry, + chunk_offsets, + 4, + ) } fn build_encrypted_hevc_movie_file() -> Vec { @@ -1399,6 +2158,78 @@ fn build_encrypted_hevc_trak(chunk_offsets: &[u64; 1]) -> Vec { build_single_sample_video_trak(1, 1_000, 1_000, (640, 360), sample_entry, chunk_offsets, 4) } +fn build_vvc_movie_file() -> Vec { + build_single_track_movie_file( + vec![fourcc("isom"), fourcc("iso8"), fourcc("vvc1")], + build_vvc_trak, + vec![0x75, 0x76, 0x77, 0x78], + ) +} + +fn build_vvc_trak(chunk_offsets: &[u64; 1]) -> Vec { + let sample_entry = encode_supported_box( + &video_sample_entry_with_type("vvc1", 640, 360), + &encode_supported_box( + &{ + let mut vvcc = VVCDecoderConfiguration::default(); + vvcc.set_version(0); + vvcc.decoder_configuration_record = vec![0x01, 0x23, 0x45, 0x67, 0x89]; + vvcc + }, + &[], + ), + ); + build_single_sample_video_trak(1, 1_000, 1_000, (640, 360), sample_entry, chunk_offsets, 4) +} + +fn build_avs3_movie_file() -> Vec { + build_single_track_movie_file( + vec![fourcc("isom"), fourcc("iso8"), fourcc("avs3")], + build_avs3_trak, + vec![0x85, 0x86, 0x87, 0x88], + ) +} + +fn build_avs3_trak(chunk_offsets: &[u64; 1]) -> Vec { + let sample_entry = encode_supported_box( + &video_sample_entry_with_type("avs3", 640, 360), + &encode_supported_box(&avs3_config(), &[]), + ); + build_single_sample_video_trak(1, 1_000, 1_000, (640, 360), sample_entry, chunk_offsets, 4) +} + +fn build_flac_movie_file() -> Vec { + build_single_track_movie_file( + vec![fourcc("isom"), fourcc("iso8"), fourcc("fLaC")], + build_flac_trak, + vec![0x89, 0x8a, 0x8b, 0x8c], + ) +} + +fn build_flac_trak(chunk_offsets: &[u64; 1]) -> Vec { + let sample_entry = encode_supported_box( + &audio_sample_entry_with_type("fLaC", 2, 48_000), + &encode_supported_box(&flac_config(), &[]), + ); + build_single_sample_audio_trak(1, 48_000, 1_024, sample_entry, chunk_offsets, 4) +} + +fn build_mha1_movie_file() -> Vec { + build_single_track_movie_file( + vec![fourcc("isom"), fourcc("iso8"), fourcc("mha1")], + build_mha1_trak, + vec![0x8d, 0x8e, 0x8f, 0x90], + ) +} + +fn build_mha1_trak(chunk_offsets: &[u64; 1]) -> Vec { + let sample_entry = encode_supported_box( + &audio_sample_entry_with_type("mha1", 2, 48_000), + &encode_supported_box(&mha_config(), &[]), + ); + build_single_sample_audio_trak(1, 48_000, 1_024, sample_entry, chunk_offsets, 4) +} + fn build_media_characteristics_movie_file() -> Vec { build_single_track_movie_file( vec![fourcc("isom"), fourcc("iso8"), fourcc("avc1")], @@ -1420,6 +2251,29 @@ fn build_media_characteristics_trak(chunk_offsets: &[u64; 1]) -> Vec { }, &[], ), + encode_supported_box( + &Clap { + clean_aperture_width_n: 1_920, + clean_aperture_width_d: 1, + clean_aperture_height_n: 1_080, + clean_aperture_height_d: 1, + horiz_off_n: 0, + horiz_off_d: 1, + vert_off_n: 0, + vert_off_d: 1, + }, + &[], + ), + encode_supported_box( + &{ + let mut coll = CoLL::default(); + coll.set_version(0); + coll.max_cll = 1_000; + coll.max_fall = 400; + coll + }, + &[], + ), encode_supported_box( &Colr { colour_type: fourcc("nclx"), @@ -1447,6 +2301,24 @@ fn build_media_characteristics_trak(chunk_offsets: &[u64; 1]) -> Vec { }, &[], ), + encode_supported_box( + &{ + let mut smdm = SmDm::default(); + smdm.set_version(0); + smdm.primary_r_chromaticity_x = 34_000; + smdm.primary_r_chromaticity_y = 16_000; + smdm.primary_g_chromaticity_x = 13_250; + smdm.primary_g_chromaticity_y = 34_500; + smdm.primary_b_chromaticity_x = 7_500; + smdm.primary_b_chromaticity_y = 3_000; + smdm.white_point_chromaticity_x = 15_635; + smdm.white_point_chromaticity_y = 16_450; + smdm.luminance_max = 1_000_000; + smdm.luminance_min = 50; + smdm + }, + &[], + ), ] .concat(), ); @@ -1574,6 +2446,7 @@ fn build_single_sample_subtitle_trak( track_id: u32, timescale: u32, duration: u32, + media_header: Vec, sample_entry: Vec, chunk_offsets: &[u64; 1], sample_size: u32, @@ -1622,11 +2495,19 @@ fn build_single_sample_subtitle_trak( let stsz = encode_supported_box(&stsz, &[]); let stbl = encode_supported_box(&Stbl, &[stsd, stco, stts, stsc, stsz].concat()); - let minf = encode_supported_box(&Minf, &stbl); + let minf = encode_supported_box(&Minf, &[media_header, stbl].concat()); let mdia = encode_supported_box(&Mdia, &[mdhd, hdlr, minf].concat()); encode_supported_box(&Trak, &[tkhd, mdia].concat()) } +fn subtitle_media_header_box() -> Vec { + encode_supported_box(&Sthd::default(), &[]) +} + +fn null_media_header_box() -> Vec { + encode_supported_box(&Nmhd::default(), &[]) +} + fn avc_config() -> AVCDecoderConfiguration { AVCDecoderConfiguration { configuration_version: 1, @@ -1729,6 +2610,40 @@ fn pcm_config() -> PcmC { config } +fn avs3_config() -> Av3c { + Av3c { + configuration_version: 1, + sequence_header_length: 4, + sequence_header: vec![0x01, 0x02, 0x03, 0x04], + library_dependency_idc: 2, + } +} + +fn flac_config() -> DfLa { + let mut config = DfLa::default(); + config.metadata_blocks = vec![FlacMetadataBlock { + last_metadata_block_flag: true, + block_type: 0, + length: 34, + block_data: vec![ + 0x11, 0x22, 0x00, 0x10, 0x00, 0x10, 0x00, 0x0c, 0xac, 0x44, 0xf0, 0x00, 0x00, 0x00, + 0x64, 0x20, 0x00, 0x00, 0x0b, 0xb8, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, + ], + }]; + config +} + +fn mha_config() -> MhaC { + MhaC { + config_version: 1, + mpeg_h_3da_profile_level_indication: 12, + reference_channel_layout: 6, + mpeg_h_3da_config_length: 4, + mpeg_h_3da_config: vec![0x01, 0x02, 0x03, 0x04], + } +} + fn handler_box(handler_type: &str, name: &str) -> Vec { let mut hdlr = Hdlr::default(); hdlr.handler_type = fourcc(handler_type); diff --git a/tests/rewrite.rs b/tests/rewrite.rs index 6648a1c..15775cf 100644 --- a/tests/rewrite.rs +++ b/tests/rewrite.rs @@ -5,14 +5,17 @@ mod support; use std::fs; use std::io::Cursor; -use mp4forge::boxes::iso14496_12::{Meta, Moof, Tfdt, Traf}; +use mp4forge::boxes::iso14496_12::{Emib, Meta, Moof, Sgpd, Silb, Tfdt, Traf}; use mp4forge::extract::extract_box_as; use mp4forge::rewrite::{ RewriteError, rewrite_box_as, rewrite_box_as_bytes, rewrite_boxes_as_bytes, }; use mp4forge::walk::BoxPath; -use support::{encode_raw_box, encode_supported_box, fixture_path, fourcc}; +use support::{ + build_encrypted_fragmented_video_file, build_event_message_movie_file, encode_raw_box, + encode_supported_box, fixture_path, fourcc, +}; #[test] fn rewrite_box_as_updates_matching_typed_payloads() { @@ -65,6 +68,34 @@ fn rewrite_box_as_bytes_updates_matching_typed_payloads() { assert_eq!(tfdt[0].base_media_decode_time_v0, 12_345); } +#[test] +fn rewrite_box_as_bytes_updates_fragmented_encrypted_sample_group_descriptions() { + let input = build_encrypted_fragmented_video_file(); + let output = rewrite_box_as_bytes::( + &input, + BoxPath::from([fourcc("moof"), fourcc("traf"), fourcc("sgpd")]), + |sgpd| { + sgpd.seig_entries_l[0].seig_entry.crypt_byte_block = 5; + sgpd.seig_entries_l[0].seig_entry.skip_byte_block = 6; + }, + ) + .unwrap(); + + let sgpd = extract_box_as::<_, Sgpd>( + &mut Cursor::new(output), + None, + BoxPath::from([fourcc("moof"), fourcc("traf"), fourcc("sgpd")]), + ) + .unwrap(); + + assert_eq!(sgpd.len(), 1); + assert_eq!(sgpd[0].grouping_type, fourcc("seig")); + assert_eq!(sgpd[0].seig_entries_l.len(), 1); + assert_eq!(sgpd[0].seig_entries_l[0].seig_entry.crypt_byte_block, 5); + assert_eq!(sgpd[0].seig_entries_l[0].seig_entry.skip_byte_block, 6); + assert_eq!(sgpd[0].seig_entries_l[0].description_length, 20); +} + #[test] fn rewrite_box_as_returns_zero_and_preserves_bytes_when_nothing_matches() { let input = fs::read(fixture_path("sample_fragmented.mp4")).unwrap(); @@ -83,6 +114,64 @@ fn rewrite_box_as_returns_zero_and_preserves_bytes_when_nothing_matches() { assert_eq!(output.into_inner(), input); } +#[test] +fn rewrite_box_as_bytes_updates_event_message_boxes() { + let input = build_event_message_movie_file(); + let output = rewrite_box_as_bytes::( + &input, + BoxPath::from([ + fourcc("moov"), + fourcc("trak"), + fourcc("mdia"), + fourcc("minf"), + fourcc("stbl"), + fourcc("stsd"), + fourcc("evte"), + fourcc("silb"), + ]), + |silb| { + silb.schemes[0].value = "event-1b".to_string(); + silb.other_schemes_flag = false; + }, + ) + .unwrap(); + let output = + rewrite_box_as_bytes::(&output, BoxPath::from([fourcc("emib")]), |emib| { + emib.event_duration = 3_000; + emib.value = "3".to_string(); + }) + .unwrap(); + + let silb = extract_box_as::<_, Silb>( + &mut Cursor::new(output.clone()), + None, + BoxPath::from([ + fourcc("moov"), + fourcc("trak"), + fourcc("mdia"), + fourcc("minf"), + fourcc("stbl"), + fourcc("stsd"), + fourcc("evte"), + fourcc("silb"), + ]), + ) + .unwrap(); + assert_eq!(silb.len(), 1); + assert_eq!(silb[0].schemes[0].value, "event-1b"); + assert!(!silb[0].other_schemes_flag); + + let emib = extract_box_as::<_, Emib>( + &mut Cursor::new(output), + None, + BoxPath::from([fourcc("emib")]), + ) + .unwrap(); + assert_eq!(emib.len(), 1); + assert_eq!(emib[0].event_duration, 3_000); + assert_eq!(emib[0].value, "3"); +} + #[test] fn rewrite_boxes_as_bytes_preserves_bytes_when_nothing_matches() { let input = fs::read(fixture_path("sample_fragmented.mp4")).unwrap(); diff --git a/tests/sidx.rs b/tests/sidx.rs new file mode 100644 index 0000000..216d0cd --- /dev/null +++ b/tests/sidx.rs @@ -0,0 +1,917 @@ +#![allow(clippy::field_reassign_with_default)] + +mod support; + +use std::fs; +use std::io::Cursor; + +use mp4forge::boxes::iso14496_12::{ + Ftyp, Hdlr, Mdhd, Mdia, Mfhd, Moof, Moov, Mvex, Sidx, SidxReference, Styp, + TFHD_DEFAULT_SAMPLE_DURATION_PRESENT, TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT, + TRUN_SAMPLE_DURATION_PRESENT, TRUN_SAMPLE_SIZE_PRESENT, Tfdt, Tfhd, Tkhd, Traf, Trak, Trex, + Trun, TrunEntry, +}; +use mp4forge::codec::{ImmutableBox, MutableBox}; +use mp4forge::extract::{extract_box, extract_box_as}; +use mp4forge::sidx::{ + SidxAnalysisError, SidxPlanError, SidxRewriteError, TopLevelSidxPlanAction, + TopLevelSidxPlanOptions, analyze_top_level_sidx_update, analyze_top_level_sidx_update_bytes, + apply_top_level_sidx_plan, apply_top_level_sidx_plan_bytes, build_top_level_sidx_plan, + plan_top_level_sidx_update, plan_top_level_sidx_update_bytes, +}; +use mp4forge::walk::BoxPath; + +use support::{encode_raw_box, encode_supported_box, fixture_path, fourcc}; + +#[test] +fn analyze_top_level_sidx_update_prefers_video_over_first_track() { + let input = build_audio_first_fragmented_file(); + + let analysis = analyze_top_level_sidx_update_bytes(&input).unwrap(); + + assert_eq!(analysis.timing_track.track_id, 2); + assert_eq!(analysis.timing_track.handler_type, Some(fourcc("vide"))); + assert_eq!(analysis.timing_track.timescale, 1_000); + assert!(analysis.placement.existing_top_level_sidxs.is_empty()); + assert_eq!(analysis.segments.len(), 1); + + let segment = &analysis.segments[0]; + assert_eq!(segment.moof_count, 1); + assert_eq!(segment.timing_fragment_count, 1); + assert_eq!(segment.base_decode_time, 900); + assert_eq!(segment.presentation_time, 900); + assert_eq!(segment.duration, 100); +} + +#[test] +fn analyze_top_level_sidx_update_groups_styp_runs_separately() { + let input = build_styp_fragmented_single_track_file(); + let analysis = analyze_top_level_sidx_update_bytes(&input).unwrap(); + + let styps = extract_box( + &mut Cursor::new(input.clone()), + None, + BoxPath::from([fourcc("styp")]), + ) + .unwrap(); + + assert!(analysis.placement.existing_top_level_sidxs.is_empty()); + assert_eq!(analysis.placement.insertion_box, styps[0]); + assert_eq!(analysis.segments.len(), 2); + + let first = &analysis.segments[0]; + assert_eq!(first.first_box, styps[0]); + assert_eq!(first.presentation_time, 105); + assert_eq!(first.base_decode_time, 100); + assert_eq!(first.duration, 60); + assert_eq!( + first.size, + total_box_size( + &input, + &[fourcc("styp"), fourcc("moof"), fourcc("mdat")], + 0, + 1 + ) + ); + + let second = &analysis.segments[1]; + assert_eq!(second.first_box, styps[1]); + assert_eq!(second.presentation_time, 160); + assert_eq!(second.base_decode_time, 160); + assert_eq!(second.duration, 40); + assert_eq!( + second.size, + total_box_size( + &input, + &[fourcc("styp"), fourcc("moof"), fourcc("mdat")], + 1, + 1 + ) + ); +} + +#[test] +fn analyze_top_level_sidx_update_uses_existing_top_level_sidx_boundaries() { + let input = build_top_level_sidx_fragmented_single_track_file(false); + let analysis = analyze_top_level_sidx_update_bytes(&input).unwrap(); + + let sidx = extract_box( + &mut Cursor::new(input.clone()), + None, + BoxPath::from([fourcc("sidx")]), + ) + .unwrap(); + let moofs = extract_box( + &mut Cursor::new(input.clone()), + None, + BoxPath::from([fourcc("moof")]), + ) + .unwrap(); + let mdats = extract_box( + &mut Cursor::new(input), + None, + BoxPath::from([fourcc("mdat")]), + ) + .unwrap(); + + assert_eq!(analysis.placement.insertion_box, moofs[0]); + assert_eq!(analysis.placement.existing_top_level_sidxs.len(), 1); + assert_eq!(analysis.placement.existing_top_level_sidxs[0].info, sidx[0]); + assert_eq!( + analysis.placement.existing_top_level_sidxs[0].segment_starts, + moofs.iter().map(|info| info.offset()).collect::>() + ); + + assert_eq!(analysis.segments.len(), 2); + assert_eq!(analysis.segments[0].first_box, moofs[0]); + assert_eq!(analysis.segments[0].duration, 60); + assert_eq!(analysis.segments[0].size, moofs[0].size() + mdats[0].size()); + assert_eq!(analysis.segments[1].first_box, moofs[1]); + assert_eq!(analysis.segments[1].duration, 40); + assert_eq!(analysis.segments[1].size, moofs[1].size() + mdats[1].size()); +} + +#[test] +fn analyze_top_level_sidx_update_matches_interleaved_fixture_grouping() { + let input = fs::read(fixture_path("sample_fragmented.mp4")).unwrap(); + let analysis = analyze_top_level_sidx_update(&mut Cursor::new(&input)).unwrap(); + + let moofs = extract_box( + &mut Cursor::new(&input), + None, + BoxPath::from([fourcc("moof")]), + ) + .unwrap(); + let mdats = extract_box( + &mut Cursor::new(&input), + None, + BoxPath::from([fourcc("mdat")]), + ) + .unwrap(); + let expected_size = moofs.iter().map(|info| info.size()).sum::() + + mdats.iter().map(|info| info.size()).sum::(); + + assert_eq!(analysis.timing_track.track_id, 1); + assert_eq!(analysis.timing_track.handler_type, Some(fourcc("vide"))); + assert!(analysis.placement.existing_top_level_sidxs.is_empty()); + assert_eq!(analysis.placement.insertion_box, moofs[0]); + assert_eq!(analysis.segments.len(), 1); + + let segment = &analysis.segments[0]; + assert_eq!(segment.first_box, moofs[0]); + assert_eq!(segment.first_moof_offset, moofs[0].offset()); + assert_eq!(segment.moof_count, moofs.len()); + assert_eq!(segment.timing_fragment_count, 4); + assert_eq!(segment.size, expected_size); +} + +#[test] +fn analyze_top_level_sidx_update_rejects_chained_top_level_entries() { + let input = build_top_level_sidx_fragmented_single_track_file(true); + let error = analyze_top_level_sidx_update_bytes(&input).unwrap_err(); + + assert!(matches!( + error, + SidxAnalysisError::UnsupportedTopLevelSidxIndirectEntry { entry_index: 1, .. } + )); +} + +#[test] +fn plan_top_level_sidx_update_returns_none_when_add_if_not_exists_is_disabled() { + let input = build_styp_fragmented_single_track_file(); + + let plan = plan_top_level_sidx_update_bytes( + &input, + TopLevelSidxPlanOptions { + add_if_not_exists: false, + non_zero_ept: false, + }, + ) + .unwrap(); + + assert!(plan.is_none()); +} + +#[test] +fn plan_top_level_sidx_update_builds_insert_plan_with_default_values() { + let input = build_styp_fragmented_single_track_file(); + + let plan = plan_top_level_sidx_update_bytes( + &input, + TopLevelSidxPlanOptions { + add_if_not_exists: true, + non_zero_ept: false, + }, + ) + .unwrap() + .unwrap(); + + let styps = extract_box( + &mut Cursor::new(&input), + None, + BoxPath::from([fourcc("styp")]), + ) + .unwrap(); + + assert_eq!(plan.timing_track.track_id, 1); + assert_eq!(plan.action, TopLevelSidxPlanAction::Insert); + assert_eq!(plan.insertion_box, styps[0]); + assert_eq!(plan.sidx.reference_id, 1); + assert_eq!(plan.sidx.timescale, 1_000); + assert_eq!(plan.sidx.earliest_presentation_time(), 0); + assert_eq!(plan.sidx.first_offset(), 0); + assert_eq!(plan.sidx.reference_count, 2); + assert_eq!(plan.sidx.references.len(), 2); + assert_eq!( + plan.sidx.references, + vec![ + SidxReference { + reference_type: false, + referenced_size: u32::try_from(plan.entries[0].segment.size).unwrap(), + subsegment_duration: 60, + starts_with_sap: true, + sap_type: 1, + sap_delta_time: 0, + }, + SidxReference { + reference_type: false, + referenced_size: u32::try_from(plan.entries[1].segment.size).unwrap(), + subsegment_duration: 40, + starts_with_sap: true, + sap_type: 1, + sap_delta_time: 0, + }, + ] + ); + assert_eq!(plan.entries[0].start_offset, styps[0].offset()); + assert_eq!( + plan.entries[0].end_offset, + styps[0].offset() + plan.entries[0].segment.size + ); + assert_eq!(plan.entries[1].start_offset, styps[1].offset()); + assert!(plan.encoded_box_size >= 44); +} + +#[test] +fn plan_top_level_sidx_update_builds_replace_plan_with_non_zero_ept() { + let input = build_top_level_sidx_fragmented_single_track_file(false); + + let plan = plan_top_level_sidx_update( + &mut Cursor::new(&input), + TopLevelSidxPlanOptions { + add_if_not_exists: false, + non_zero_ept: true, + }, + ) + .unwrap() + .unwrap(); + + let sidx = extract_box( + &mut Cursor::new(&input), + None, + BoxPath::from([fourcc("sidx")]), + ) + .unwrap(); + + match &plan.action { + TopLevelSidxPlanAction::Replace { existing } => { + assert_eq!(existing.info, sidx[0]); + } + TopLevelSidxPlanAction::Insert => panic!("expected replace plan"), + } + assert_eq!(plan.insertion_box.offset(), plan.entries[0].start_offset); + assert_eq!(plan.sidx.earliest_presentation_time(), 105); + assert_eq!(plan.sidx.first_offset(), 0); + assert_eq!(plan.sidx.reference_count, 2); + assert_eq!(plan.sidx.references[0].subsegment_duration, 60); + assert_eq!(plan.sidx.references[1].subsegment_duration, 40); +} + +#[test] +fn plan_top_level_sidx_update_matches_interleaved_fixture_payload() { + let input = fs::read(fixture_path("sample_fragmented.mp4")).unwrap(); + let probe = mp4forge::probe::probe(&mut Cursor::new(&input)).unwrap(); + + let plan = plan_top_level_sidx_update_bytes( + &input, + TopLevelSidxPlanOptions { + add_if_not_exists: true, + non_zero_ept: false, + }, + ) + .unwrap() + .unwrap(); + + let moofs = extract_box( + &mut Cursor::new(&input), + None, + BoxPath::from([fourcc("moof")]), + ) + .unwrap(); + let mdats = extract_box( + &mut Cursor::new(&input), + None, + BoxPath::from([fourcc("mdat")]), + ) + .unwrap(); + let expected_size = moofs.iter().map(|info| info.size()).sum::() + + mdats.iter().map(|info| info.size()).sum::(); + let expected_duration = probe + .segments + .iter() + .filter(|segment| segment.track_id == 1) + .map(|segment| u64::from(segment.duration)) + .sum::(); + + assert_eq!(plan.action, TopLevelSidxPlanAction::Insert); + assert_eq!(plan.sidx.reference_count, 1); + assert_eq!(plan.sidx.earliest_presentation_time(), 0); + assert_eq!(plan.entries.len(), 1); + assert_eq!(plan.entries[0].segment.size, expected_size); + assert_eq!( + plan.entries[0].subsegment_duration, + expected_duration as u32 + ); + assert_eq!( + plan.sidx.references[0].referenced_size, + expected_size as u32 + ); + assert_eq!( + plan.sidx.references[0].subsegment_duration, + expected_duration as u32 + ); +} + +#[test] +fn apply_top_level_sidx_plan_bytes_inserts_top_level_sidx_and_preserves_other_bytes() { + let input = build_styp_fragmented_single_track_file(); + let plan = plan_top_level_sidx_update_bytes( + &input, + TopLevelSidxPlanOptions { + add_if_not_exists: true, + non_zero_ept: false, + }, + ) + .unwrap() + .unwrap(); + + let output = apply_top_level_sidx_plan_bytes(&input, &plan).unwrap(); + let sidx = extract_box_as::<_, Sidx>( + &mut Cursor::new(&output), + None, + BoxPath::from([fourcc("sidx")]), + ) + .unwrap(); + let sidx_info = extract_box( + &mut Cursor::new(&output), + None, + BoxPath::from([fourcc("sidx")]), + ) + .unwrap(); + + assert_eq!(sidx.len(), 1); + assert_eq!(sidx[0], plan.sidx); + let insertion_offset = plan.insertion_box.offset() as usize; + let encoded_size = sidx_info[0].size() as usize; + assert_eq!(&output[..insertion_offset], &input[..insertion_offset]); + assert_eq!( + &output[insertion_offset + encoded_size..], + &input[insertion_offset..] + ); +} + +#[test] +fn apply_top_level_sidx_plan_replaces_existing_box_and_preserves_following_bytes() { + let input = build_gapped_top_level_sidx_fragmented_single_track_file_v0(); + let plan = plan_top_level_sidx_update_bytes( + &input, + TopLevelSidxPlanOptions { + add_if_not_exists: false, + non_zero_ept: true, + }, + ) + .unwrap() + .unwrap(); + + let existing = match &plan.action { + TopLevelSidxPlanAction::Replace { existing } => existing.clone(), + TopLevelSidxPlanAction::Insert => panic!("expected replace plan"), + }; + + let mut output = Vec::new(); + let applied = apply_top_level_sidx_plan(&mut Cursor::new(&input), &mut output, &plan).unwrap(); + + assert_eq!(applied.info.offset(), existing.info.offset()); + assert_eq!(applied.sidx.version(), 1); + assert_eq!(applied.sidx.earliest_presentation_time(), 105); + assert_eq!(applied.sidx.first_offset(), segment_gap_box().len() as u64); + assert_eq!( + &output[..existing.info.offset() as usize], + &input[..existing.info.offset() as usize] + ); + let new_end = (applied.info.offset() + applied.info.size()) as usize; + let old_end = (existing.info.offset() + existing.info.size()) as usize; + assert_eq!(&output[new_end..], &input[old_end..]); +} + +#[test] +fn apply_top_level_sidx_plan_bytes_is_stable_after_replanning() { + let input = build_styp_fragmented_single_track_file(); + let options = TopLevelSidxPlanOptions { + add_if_not_exists: true, + non_zero_ept: false, + }; + + let first_plan = plan_top_level_sidx_update_bytes(&input, options) + .unwrap() + .unwrap(); + let first_output = apply_top_level_sidx_plan_bytes(&input, &first_plan).unwrap(); + let second_plan = plan_top_level_sidx_update_bytes(&first_output, options) + .unwrap() + .unwrap(); + let second_output = apply_top_level_sidx_plan_bytes(&first_output, &second_plan).unwrap(); + + assert!(matches!( + second_plan.action, + TopLevelSidxPlanAction::Replace { .. } + )); + assert_eq!(second_output, first_output); +} + +#[test] +fn apply_top_level_sidx_plan_bytes_rejects_stale_input() { + let input = build_styp_fragmented_single_track_file(); + let stale_input = build_audio_first_fragmented_file(); + let plan = plan_top_level_sidx_update_bytes( + &input, + TopLevelSidxPlanOptions { + add_if_not_exists: true, + non_zero_ept: false, + }, + ) + .unwrap() + .unwrap(); + + let error = apply_top_level_sidx_plan_bytes(&stale_input, &plan).unwrap_err(); + + assert!(matches!( + error, + SidxRewriteError::PlannedBoxMismatch { + expected_type, + .. + } if expected_type == fourcc("styp") + )); +} + +#[test] +fn build_top_level_sidx_plan_rejects_multiple_file_level_top_level_sidx_boxes() { + let input = build_multiple_top_level_sidx_fragmented_single_track_file(); + let analysis = analyze_top_level_sidx_update_bytes(&input).unwrap(); + + let error = build_top_level_sidx_plan( + &analysis, + TopLevelSidxPlanOptions { + add_if_not_exists: true, + non_zero_ept: false, + }, + ) + .unwrap_err(); + + assert!(matches!( + error, + SidxPlanError::UnsupportedTopLevelSidxCount { count: 2 } + )); +} + +struct TrackSpec { + track_id: u32, + handler_type: &'static str, + timescale: u32, +} + +struct TrafSpec<'a> { + track_id: u32, + base_decode_time: u64, + default_sample_duration: Option, + sample_durations: &'a [u32], + sample_sizes: &'a [u32], + composition_offsets: &'a [u32], +} + +fn build_audio_first_fragmented_file() -> Vec { + let ftyp = fragmented_ftyp(); + let moov = build_fragmented_moov(&[ + TrackSpec { + track_id: 1, + handler_type: "soun", + timescale: 48_000, + }, + TrackSpec { + track_id: 2, + handler_type: "vide", + timescale: 1_000, + }, + ]); + let moof = build_moof(&[ + TrafSpec { + track_id: 1, + base_decode_time: 0, + default_sample_duration: Some(20), + sample_durations: &[], + sample_sizes: &[], + composition_offsets: &[], + }, + TrafSpec { + track_id: 2, + base_decode_time: 900, + default_sample_duration: Some(50), + sample_durations: &[], + sample_sizes: &[], + composition_offsets: &[], + }, + ]); + let mdat = encode_raw_box(fourcc("mdat"), &[0; 20]); + [ftyp, moov, moof, mdat].concat() +} + +fn build_styp_fragmented_single_track_file() -> Vec { + let ftyp = fragmented_ftyp(); + let moov = build_fragmented_moov(&[TrackSpec { + track_id: 1, + handler_type: "vide", + timescale: 1_000, + }]); + let styp1 = segment_styp(); + let moof1 = build_moof(&[TrafSpec { + track_id: 1, + base_decode_time: 100, + default_sample_duration: None, + sample_durations: &[30, 30], + sample_sizes: &[4, 4], + composition_offsets: &[5, 0], + }]); + let mdat1 = encode_raw_box(fourcc("mdat"), &[0; 8]); + let styp2 = segment_styp(); + let moof2 = build_moof(&[TrafSpec { + track_id: 1, + base_decode_time: 160, + default_sample_duration: None, + sample_durations: &[40], + sample_sizes: &[6], + composition_offsets: &[0], + }]); + let mdat2 = encode_raw_box(fourcc("mdat"), &[0; 6]); + + [ftyp, moov, styp1, moof1, mdat1, styp2, moof2, mdat2].concat() +} + +fn build_top_level_sidx_fragmented_single_track_file(indirect_first_entry: bool) -> Vec { + let ftyp = fragmented_ftyp(); + let moov = build_fragmented_moov(&[TrackSpec { + track_id: 1, + handler_type: "vide", + timescale: 1_000, + }]); + let moof1 = build_moof(&[TrafSpec { + track_id: 1, + base_decode_time: 100, + default_sample_duration: None, + sample_durations: &[30, 30], + sample_sizes: &[4, 4], + composition_offsets: &[5, 0], + }]); + let mdat1 = encode_raw_box(fourcc("mdat"), &[0; 8]); + let moof2 = build_moof(&[TrafSpec { + track_id: 1, + base_decode_time: 160, + default_sample_duration: None, + sample_durations: &[40], + sample_sizes: &[6], + composition_offsets: &[0], + }]); + let mdat2 = encode_raw_box(fourcc("mdat"), &[0; 6]); + + let first_segment_size = u32::try_from(moof1.len() + mdat1.len()).unwrap(); + let second_segment_size = u32::try_from(moof2.len() + mdat2.len()).unwrap(); + + let mut sidx = Sidx::default(); + sidx.set_version(1); + sidx.reference_id = 1; + sidx.timescale = 1_000; + sidx.reference_count = 2; + sidx.references = vec![ + SidxReference { + reference_type: indirect_first_entry, + referenced_size: first_segment_size, + subsegment_duration: 60, + starts_with_sap: true, + sap_type: 1, + sap_delta_time: 0, + }, + SidxReference { + reference_type: false, + referenced_size: second_segment_size, + subsegment_duration: 40, + starts_with_sap: true, + sap_type: 1, + sap_delta_time: 0, + }, + ]; + let sidx = encode_supported_box(&sidx, &[]); + + [ftyp, moov, sidx, moof1, mdat1, moof2, mdat2].concat() +} + +fn build_gapped_top_level_sidx_fragmented_single_track_file_v0() -> Vec { + let ftyp = fragmented_ftyp(); + let moov = build_fragmented_moov(&[TrackSpec { + track_id: 1, + handler_type: "vide", + timescale: 1_000, + }]); + let gap = segment_gap_box(); + let moof1 = build_moof(&[TrafSpec { + track_id: 1, + base_decode_time: 100, + default_sample_duration: None, + sample_durations: &[30, 30], + sample_sizes: &[4, 4], + composition_offsets: &[5, 0], + }]); + let mdat1 = encode_raw_box(fourcc("mdat"), &[0; 8]); + let moof2 = build_moof(&[TrafSpec { + track_id: 1, + base_decode_time: 160, + default_sample_duration: None, + sample_durations: &[40], + sample_sizes: &[6], + composition_offsets: &[0], + }]); + let mdat2 = encode_raw_box(fourcc("mdat"), &[0; 6]); + + let first_segment_size = u32::try_from(moof1.len() + mdat1.len()).unwrap(); + let second_segment_size = u32::try_from(moof2.len() + mdat2.len()).unwrap(); + + let mut sidx = Sidx::default(); + sidx.set_version(0); + sidx.reference_id = 1; + sidx.timescale = 1_000; + sidx.earliest_presentation_time_v0 = 105; + sidx.first_offset_v0 = gap.len() as u32; + sidx.reference_count = 2; + sidx.references = vec![ + SidxReference { + reference_type: false, + referenced_size: first_segment_size, + subsegment_duration: 60, + starts_with_sap: true, + sap_type: 1, + sap_delta_time: 0, + }, + SidxReference { + reference_type: false, + referenced_size: second_segment_size, + subsegment_duration: 40, + starts_with_sap: true, + sap_type: 1, + sap_delta_time: 0, + }, + ]; + let sidx = encode_supported_box(&sidx, &[]); + + [ftyp, moov, sidx, gap, moof1, mdat1, moof2, mdat2].concat() +} + +fn build_multiple_top_level_sidx_fragmented_single_track_file() -> Vec { + let ftyp = fragmented_ftyp(); + let moov = build_fragmented_moov(&[TrackSpec { + track_id: 1, + handler_type: "vide", + timescale: 1_000, + }]); + let moof1 = build_moof(&[TrafSpec { + track_id: 1, + base_decode_time: 100, + default_sample_duration: None, + sample_durations: &[30, 30], + sample_sizes: &[4, 4], + composition_offsets: &[5, 0], + }]); + let mdat1 = encode_raw_box(fourcc("mdat"), &[0; 8]); + let moof2 = build_moof(&[TrafSpec { + track_id: 1, + base_decode_time: 160, + default_sample_duration: None, + sample_durations: &[40], + sample_sizes: &[6], + composition_offsets: &[0], + }]); + let mdat2 = encode_raw_box(fourcc("mdat"), &[0; 6]); + + let first_segment_size = u32::try_from(moof1.len() + mdat1.len()).unwrap(); + let second_segment_size = u32::try_from(moof2.len() + mdat2.len()).unwrap(); + + let mut second_sidx = Sidx::default(); + second_sidx.set_version(1); + second_sidx.reference_id = 1; + second_sidx.timescale = 1_000; + second_sidx.reference_count = 2; + second_sidx.references = vec![ + SidxReference { + reference_type: false, + referenced_size: first_segment_size, + subsegment_duration: 60, + starts_with_sap: true, + sap_type: 1, + sap_delta_time: 0, + }, + SidxReference { + reference_type: false, + referenced_size: second_segment_size, + subsegment_duration: 40, + starts_with_sap: true, + sap_type: 1, + sap_delta_time: 0, + }, + ]; + let second_sidx_bytes = encode_supported_box(&second_sidx, &[]); + + let mut first_sidx = second_sidx.clone(); + first_sidx.first_offset_v1 = second_sidx_bytes.len() as u64; + let first_sidx_bytes = encode_supported_box(&first_sidx, &[]); + + [ + ftyp, + moov, + first_sidx_bytes, + second_sidx_bytes, + moof1, + mdat1, + moof2, + mdat2, + ] + .concat() +} + +fn segment_gap_box() -> Vec { + encode_raw_box(fourcc("free"), &[0x41, 0x42, 0x43, 0x44, 0x45]) +} + +fn fragmented_ftyp() -> Vec { + encode_supported_box( + &Ftyp { + major_brand: fourcc("iso6"), + minor_version: 1, + compatible_brands: vec![fourcc("iso6"), fourcc("dash")], + }, + &[], + ) +} + +fn segment_styp() -> Vec { + encode_supported_box( + &Styp { + major_brand: fourcc("msdh"), + minor_version: 0, + compatible_brands: vec![fourcc("msdh"), fourcc("msix")], + }, + &[], + ) +} + +fn build_fragmented_moov(track_specs: &[TrackSpec]) -> Vec { + let tracks = track_specs + .iter() + .map(build_fragmented_trak) + .collect::>(); + + let mut mvex_children = Vec::new(); + for track in track_specs { + let mut trex = Trex::default(); + trex.track_id = track.track_id; + mvex_children.extend_from_slice(&encode_supported_box(&trex, &[])); + } + + let mut moov_children = Vec::new(); + for track in tracks { + moov_children.extend_from_slice(&track); + } + moov_children.extend_from_slice(&encode_supported_box(&Mvex, &mvex_children)); + encode_supported_box(&Moov, &moov_children) +} + +fn build_fragmented_trak(track: &TrackSpec) -> Vec { + let mut tkhd = Tkhd::default(); + tkhd.track_id = track.track_id; + let tkhd = encode_supported_box(&tkhd, &[]); + + let mut mdhd = Mdhd::default(); + mdhd.timescale = track.timescale; + mdhd.language = [5, 14, 7]; + let mdhd = encode_supported_box(&mdhd, &[]); + + let mut hdlr = Hdlr::default(); + hdlr.handler_type = fourcc(track.handler_type); + hdlr.name = track.handler_type.to_string(); + let hdlr = encode_supported_box(&hdlr, &[]); + + let mdia = encode_supported_box(&Mdia, &[mdhd, hdlr].concat()); + encode_supported_box(&Trak, &[tkhd, mdia].concat()) +} + +fn build_moof(trafs: &[TrafSpec<'_>]) -> Vec { + let mut mfhd = Mfhd::default(); + mfhd.sequence_number = 1; + let mfhd = encode_supported_box(&mfhd, &[]); + + let mut moof_children = mfhd; + for traf in trafs { + moof_children.extend_from_slice(&build_traf(traf)); + } + encode_supported_box(&Moof, &moof_children) +} + +fn build_traf(spec: &TrafSpec<'_>) -> Vec { + let mut tfhd = Tfhd::default(); + tfhd.track_id = spec.track_id; + if let Some(default_sample_duration) = spec.default_sample_duration { + tfhd.set_flags(TFHD_DEFAULT_SAMPLE_DURATION_PRESENT); + tfhd.default_sample_duration = default_sample_duration; + } + let tfhd = encode_supported_box(&tfhd, &[]); + + let mut tfdt = Tfdt::default(); + tfdt.set_version(1); + tfdt.base_media_decode_time_v1 = spec.base_decode_time; + let tfdt = encode_supported_box(&tfdt, &[]); + + let mut trun = Trun::default(); + trun.sample_count = spec + .sample_durations + .len() + .max(spec.sample_sizes.len()) + .max(if spec.default_sample_duration.is_some() { + 2 + } else { + 0 + }) as u32; + if spec.sample_durations.is_empty() { + trun.sample_count = if spec.default_sample_duration.is_some() { + 2 + } else { + 0 + }; + } + + let mut flags = 0_u32; + if !spec.sample_durations.is_empty() { + flags |= TRUN_SAMPLE_DURATION_PRESENT; + } + if !spec.sample_sizes.is_empty() { + flags |= TRUN_SAMPLE_SIZE_PRESENT; + } + if !spec.composition_offsets.is_empty() { + flags |= TRUN_SAMPLE_COMPOSITION_TIME_OFFSET_PRESENT; + } + trun.set_flags(flags); + + let entry_count = if flags == 0 { + 0 + } else { + spec.sample_durations.len() + }; + trun.sample_count = if flags == 0 { 2 } else { entry_count as u32 }; + trun.entries = (0..entry_count) + .map(|index| TrunEntry { + sample_duration: spec.sample_durations[index], + sample_size: spec.sample_sizes[index], + sample_composition_time_offset_v0: spec + .composition_offsets + .get(index) + .copied() + .unwrap_or(0), + ..TrunEntry::default() + }) + .collect(); + let trun = encode_supported_box(&trun, &[]); + + encode_supported_box(&Traf, &[tfhd, tfdt, trun].concat()) +} + +fn total_box_size( + input: &[u8], + types: &[mp4forge::FourCc], + start_index: usize, + count: usize, +) -> u64 { + types + .iter() + .map(|box_type| { + extract_box(&mut Cursor::new(input), None, BoxPath::from([*box_type])).unwrap() + [start_index..start_index + count] + .iter() + .map(|info| info.size()) + .sum::() + }) + .sum() +} diff --git a/tests/support/mod.rs b/tests/support/mod.rs index e66487a..8aa5da6 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -5,6 +5,18 @@ use std::fs; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; +use mp4forge::boxes::AnyTypeBox; +use mp4forge::boxes::iso14496_12::{ + AVCDecoderConfiguration, Btrt, Emeb, Emib, EventMessageSampleEntry, Frma, Ftyp, Hdlr, Mdhd, + Mdia, Mfhd, Minf, Moof, Moov, Mvex, Mvhd, Pasp, Saio, Saiz, SampleEntry, Sbgp, SbgpEntry, Schi, + Schm, SeigEntry, SeigEntryL, Sgpd, Silb, SilbEntry, Sinf, Stbl, Stco, Stsc, Stsd, Stsz, Stts, + TFHD_DEFAULT_SAMPLE_DURATION_PRESENT, TFHD_DEFAULT_SAMPLE_SIZE_PRESENT, Tfdt, Tfhd, Traf, Trak, + Trex, Trun, VisualSampleEntry, +}; +use mp4forge::boxes::iso23001_7::{ + SENC_USE_SUBSAMPLE_ENCRYPTION, Senc, SencSample, SencSubsample, Tenc, +}; +use mp4forge::codec::MutableBox; use mp4forge::codec::{CodecBox, marshal}; use mp4forge::{BoxInfo, FourCc}; @@ -73,3 +85,390 @@ pub fn read_golden(relative_path: &str) -> String { pub fn normalize_text(value: &str) -> String { value.replace("\r\n", "\n") } + +pub fn build_encrypted_fragmented_video_file() -> Vec { + let ftyp = encode_supported_box( + &Ftyp { + major_brand: fourcc("iso6"), + minor_version: 1, + compatible_brands: vec![fourcc("iso6"), fourcc("dash"), fourcc("cenc")], + }, + &[], + ); + let moov = build_encrypted_fragmented_video_moov(); + let moof = build_encrypted_fragmented_video_moof(); + let mdat = encode_raw_box(fourcc("mdat"), &[0xde, 0xad, 0xbe, 0xef]); + [ftyp, moov, moof, mdat].concat() +} + +pub fn build_visual_sample_entry_box_with_trailing_bytes() -> Vec { + let pasp = encode_supported_box( + &Pasp { + h_spacing: 1, + v_spacing: 1, + }, + &[], + ); + let mut extensions = pasp; + extensions.extend_from_slice(&visual_sample_entry_trailing_bytes()); + encode_supported_box(&video_sample_entry_with_type("avc1", 640, 360), &extensions) +} + +pub fn visual_sample_entry_trailing_bytes() -> Vec { + vec![0xde, 0xad, 0xbe] +} + +pub fn build_event_message_movie_file() -> Vec { + let ftyp = encode_supported_box( + &Ftyp { + major_brand: fourcc("isom"), + minor_version: 1, + compatible_brands: vec![fourcc("isom"), fourcc("iso8")], + }, + &[], + ); + let moov = build_event_message_moov(); + let emib = encode_supported_box(&event_message_instance_box(), &[]); + let emeb = encode_supported_box(&Emeb, &[]); + let mdat = encode_raw_box(fourcc("mdat"), &[0x01, 0x02, 0x03, 0x04]); + [ftyp, moov, emib, emeb, mdat].concat() +} + +fn build_encrypted_fragmented_video_moov() -> Vec { + let mut mvhd = Mvhd::default(); + mvhd.timescale = 1_000; + mvhd.duration_v0 = 1_000; + mvhd.rate = 1 << 16; + mvhd.volume = 1 << 8; + mvhd.next_track_id = 2; + let mvhd = encode_supported_box(&mvhd, &[]); + + let mut trex = Trex::default(); + trex.track_id = 1; + trex.default_sample_description_index = 1; + let trex = encode_supported_box(&trex, &[]); + let mvex = encode_supported_box(&Mvex, &trex); + + encode_supported_box( + &Moov, + &[mvhd, build_encrypted_fragmented_video_trak(), mvex].concat(), + ) +} + +fn build_encrypted_fragmented_video_trak() -> Vec { + let mut tkhd = mp4forge::boxes::iso14496_12::Tkhd::default(); + tkhd.track_id = 1; + tkhd.width = u32::from(320_u16) << 16; + tkhd.height = u32::from(180_u16) << 16; + let tkhd = encode_supported_box(&tkhd, &[]); + + let mut mdhd = Mdhd::default(); + mdhd.timescale = 1_000; + mdhd.language = [5, 14, 7]; + let mdhd = encode_supported_box(&mdhd, &[]); + + let mut stsd = Stsd::default(); + stsd.entry_count = 1; + let stsd = encode_supported_box( + &stsd, + &encode_supported_box( + &video_sample_entry_with_type("encv", 320, 180), + &[ + encode_supported_box(&avc_config(), &[]), + build_encrypted_fragmented_video_sinf(), + ] + .concat(), + ), + ); + + let mut stco = Stco::default(); + stco.entry_count = 0; + let stco = encode_supported_box(&stco, &[]); + + let mut stts = Stts::default(); + stts.entry_count = 0; + let stts = encode_supported_box(&stts, &[]); + + let mut stsc = Stsc::default(); + stsc.entry_count = 0; + let stsc = encode_supported_box(&stsc, &[]); + + let mut stsz = Stsz::default(); + stsz.sample_count = 0; + let stsz = encode_supported_box(&stsz, &[]); + + let stbl = encode_supported_box(&Stbl, &[stsd, stco, stts, stsc, stsz].concat()); + let minf = encode_supported_box(&Minf, &stbl); + let mdia = encode_supported_box( + &Mdia, + &[mdhd, handler_box("vide", "VideoHandler"), minf].concat(), + ); + encode_supported_box(&Trak, &[tkhd, mdia].concat()) +} + +fn build_event_message_moov() -> Vec { + let mut mvhd = Mvhd::default(); + mvhd.timescale = 1_000; + mvhd.duration_v0 = 1_000; + mvhd.rate = 1 << 16; + mvhd.volume = 1 << 8; + mvhd.next_track_id = 2; + let mvhd = encode_supported_box(&mvhd, &[]); + + encode_supported_box(&Moov, &[mvhd, build_event_message_trak()].concat()) +} + +fn build_event_message_trak() -> Vec { + let mut tkhd = mp4forge::boxes::iso14496_12::Tkhd::default(); + tkhd.track_id = 1; + tkhd.duration_v0 = 1_000; + let tkhd = encode_supported_box(&tkhd, &[]); + + let mut mdhd = Mdhd::default(); + mdhd.timescale = 1_000; + mdhd.duration_v0 = 1_000; + mdhd.language = [5, 14, 7]; + let mdhd = encode_supported_box(&mdhd, &[]); + + let mut stsd = Stsd::default(); + stsd.entry_count = 1; + let stsd = encode_supported_box(&stsd, &event_message_sample_entry_box()); + + let mut stco = Stco::default(); + stco.entry_count = 1; + stco.chunk_offset = vec![0x40]; + let stco = encode_supported_box(&stco, &[]); + + let mut stts = Stts::default(); + stts.entry_count = 1; + stts.entries = vec![mp4forge::boxes::iso14496_12::SttsEntry { + sample_count: 1, + sample_delta: 1_000, + }]; + let stts = encode_supported_box(&stts, &[]); + + let mut stsc = Stsc::default(); + stsc.entry_count = 1; + stsc.entries = vec![mp4forge::boxes::iso14496_12::StscEntry { + first_chunk: 1, + samples_per_chunk: 1, + sample_description_index: 1, + }]; + let stsc = encode_supported_box(&stsc, &[]); + + let mut stsz = Stsz::default(); + stsz.sample_count = 1; + stsz.entry_size = vec![4]; + let stsz = encode_supported_box(&stsz, &[]); + + let stbl = encode_supported_box(&Stbl, &[stsd, stco, stts, stsc, stsz].concat()); + let minf = encode_supported_box(&Minf, &stbl); + let mdia = encode_supported_box( + &Mdia, + &[mdhd, handler_box("subt", "SubtitleHandler"), minf].concat(), + ); + encode_supported_box(&Trak, &[tkhd, mdia].concat()) +} + +fn event_message_sample_entry_box() -> Vec { + let entry = EventMessageSampleEntry { + sample_entry: SampleEntry { + box_type: fourcc("evte"), + data_reference_index: 1, + }, + }; + let children = [ + encode_supported_box( + &Btrt { + buffer_size_db: 32_768, + max_bitrate: 4_000_000, + avg_bitrate: 2_500_000, + }, + &[], + ), + encode_supported_box(&event_message_scheme_box(), &[]), + ] + .concat(); + encode_supported_box(&entry, &children) +} + +pub fn event_message_scheme_box() -> Silb { + let mut silb = Silb::default(); + silb.set_version(0); + silb.scheme_count = 2; + silb.schemes = vec![ + SilbEntry { + scheme_id_uri: "urn:mpeg:dash:event:2012".to_string(), + value: "event-1".to_string(), + at_least_one_flag: false, + }, + SilbEntry { + scheme_id_uri: "urn:scte:scte35:2013:bin".to_string(), + value: "splice".to_string(), + at_least_one_flag: true, + }, + ]; + silb.other_schemes_flag = true; + silb +} + +pub fn event_message_instance_box() -> Emib { + let mut emib = Emib::default(); + emib.set_version(0); + emib.presentation_time_delta = -1_000; + emib.event_duration = 2_000; + emib.id = 1_234; + emib.scheme_id_uri = "urn:scte:scte35:2013:bin".to_string(); + emib.value = "2".to_string(); + emib.message_data = vec![0x01, 0x02, 0x03]; + emib +} + +fn build_encrypted_fragmented_video_sinf() -> Vec { + let mut schm = Schm::default(); + schm.set_version(0); + schm.scheme_type = fourcc("cenc"); + schm.scheme_version = 0x0001_0000; + + let mut tenc = Tenc::default(); + tenc.set_version(1); + tenc.default_crypt_byte_block = 1; + tenc.default_skip_byte_block = 9; + tenc.default_is_protected = 1; + tenc.default_per_sample_iv_size = 8; + tenc.default_kid = encrypted_fragment_default_kid(); + + let schi = encode_supported_box(&Schi, &encode_supported_box(&tenc, &[])); + encode_supported_box( + &Sinf, + &[ + encode_supported_box( + &Frma { + data_format: fourcc("avc1"), + }, + &[], + ), + encode_supported_box(&schm, &[]), + schi, + ] + .concat(), + ) +} + +fn build_encrypted_fragmented_video_moof() -> Vec { + let mut mfhd = Mfhd::default(); + mfhd.sequence_number = 1; + let mfhd = encode_supported_box(&mfhd, &[]); + + let mut tfhd = Tfhd::default(); + tfhd.set_flags(TFHD_DEFAULT_SAMPLE_DURATION_PRESENT | TFHD_DEFAULT_SAMPLE_SIZE_PRESENT); + tfhd.track_id = 1; + tfhd.default_sample_duration = 1_000; + tfhd.default_sample_size = 4; + let tfhd = encode_supported_box(&tfhd, &[]); + + let mut tfdt = Tfdt::default(); + tfdt.set_version(1); + tfdt.base_media_decode_time_v1 = 0; + let tfdt = encode_supported_box(&tfdt, &[]); + + let mut trun = Trun::default(); + trun.sample_count = 1; + let trun = encode_supported_box(&trun, &[]); + + let mut saiz = Saiz::default(); + saiz.sample_count = 1; + saiz.sample_info_size = vec![16]; + let saiz = encode_supported_box(&saiz, &[]); + + let mut saio = Saio::default(); + saio.entry_count = 1; + saio.offset_v0 = vec![0]; + let saio = encode_supported_box(&saio, &[]); + + let mut senc = Senc::default(); + senc.set_version(0); + senc.set_flags(SENC_USE_SUBSAMPLE_ENCRYPTION); + senc.sample_count = 1; + senc.samples = vec![SencSample { + initialization_vector: vec![1, 2, 3, 4, 5, 6, 7, 8], + subsamples: vec![SencSubsample { + bytes_of_clear_data: 32, + bytes_of_protected_data: 480, + }], + }]; + let senc = encode_supported_box(&senc, &[]); + + let mut sgpd = Sgpd::default(); + sgpd.set_version(1); + sgpd.grouping_type = fourcc("seig"); + sgpd.default_length = 0; + sgpd.entry_count = 1; + sgpd.seig_entries_l = vec![SeigEntryL { + description_length: 20, + seig_entry: SeigEntry { + crypt_byte_block: 1, + skip_byte_block: 9, + is_protected: 1, + per_sample_iv_size: 8, + kid: encrypted_fragment_default_kid(), + ..SeigEntry::default() + }, + }]; + let sgpd = encode_supported_box(&sgpd, &[]); + + let mut sbgp = Sbgp::default(); + sbgp.grouping_type = u32::from_be_bytes(*b"seig"); + sbgp.entry_count = 1; + sbgp.entries = vec![SbgpEntry { + sample_count: 1, + group_description_index: 65_537, + }]; + let sbgp = encode_supported_box(&sbgp, &[]); + + let traf = encode_supported_box( + &Traf, + &[tfhd, tfdt, trun, saiz, saio, senc, sgpd, sbgp].concat(), + ); + encode_supported_box(&Moof, &[mfhd, traf].concat()) +} + +fn encrypted_fragment_default_kid() -> [u8; 16] { + [ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, + 0xfe, + ] +} + +fn avc_config() -> AVCDecoderConfiguration { + AVCDecoderConfiguration { + configuration_version: 1, + profile: 0x64, + profile_compatibility: 0, + level: 0x1f, + length_size_minus_one: 3, + ..AVCDecoderConfiguration::default() + } +} + +fn handler_box(handler_type: &str, name: &str) -> Vec { + let mut hdlr = Hdlr::default(); + hdlr.handler_type = fourcc(handler_type); + hdlr.name = name.to_string(); + encode_supported_box(&hdlr, &[]) +} + +fn video_sample_entry_with_type(box_type: &str, width: u16, height: u16) -> VisualSampleEntry { + let mut entry = VisualSampleEntry { + sample_entry: SampleEntry { + box_type: fourcc(box_type), + data_reference_index: 1, + }, + width, + height, + frame_count: 1, + ..VisualSampleEntry::default() + }; + entry.set_box_type(fourcc(box_type)); + entry +} diff --git a/tests/visual_sample_entry_trailing_bytes.rs b/tests/visual_sample_entry_trailing_bytes.rs new file mode 100644 index 0000000..4615125 --- /dev/null +++ b/tests/visual_sample_entry_trailing_bytes.rs @@ -0,0 +1,53 @@ +use std::io::Cursor; + +use mp4forge::boxes::iso14496_12::VisualSampleEntry; +use mp4forge::cli::dump::{DumpOptions, dump_reader}; +use mp4forge::extract::extract_box_as_bytes; +use mp4forge::rewrite::rewrite_box_as_bytes; +use mp4forge::walk::BoxPath; + +mod support; + +use support::{ + build_visual_sample_entry_box_with_trailing_bytes, fourcc, visual_sample_entry_trailing_bytes, +}; + +#[test] +fn visual_sample_entry_extract_decodes_trailing_byte_layout() { + let file = build_visual_sample_entry_box_with_trailing_bytes(); + + let entries = + extract_box_as_bytes::(&file, BoxPath::from([fourcc("avc1")])).unwrap(); + + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].width, 640); + assert_eq!(entries[0].height, 360); +} + +#[test] +fn visual_sample_entry_dump_descends_through_children_without_eof() { + let file = build_visual_sample_entry_box_with_trailing_bytes(); + let mut output = Vec::new(); + + dump_reader(&mut Cursor::new(file), &DumpOptions::default(), &mut output).unwrap(); + + let rendered = String::from_utf8(output).unwrap(); + assert!(rendered.contains("[avc1]")); + assert!(rendered.contains("[pasp]")); + assert!(!rendered.contains("unexpected EOF")); +} + +#[test] +fn visual_sample_entry_rewrite_roundtrip_preserves_children_and_trailing_bytes() { + let file = build_visual_sample_entry_box_with_trailing_bytes(); + + let rewritten = rewrite_box_as_bytes::( + &file, + BoxPath::from([fourcc("avc1")]), + |_| {}, + ) + .unwrap(); + + assert_eq!(rewritten, file); + assert!(rewritten.ends_with(&visual_sample_entry_trailing_bytes())); +}