--- /dev/null
+//! Module for parsing ISO Base Media Format aka video/mp4 streams.
+
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+// `clippy::upper_case_acronyms` is a nightly-only lint as of 2021-03-15, so we
+// allow `clippy::unknown_clippy_lints` to ignore it on stable - but
+// `clippy::unknown_clippy_lints` has been renamed in nightly, so we need to
+// allow `renamed_and_removed_lints` to ignore a warning for that.
+#![allow(renamed_and_removed_lints)]
+#![allow(clippy::unknown_clippy_lints)]
+#![allow(clippy::upper_case_acronyms)]
+
+#[macro_use]
+extern crate log;
+
+extern crate bitreader;
+extern crate byteorder;
+extern crate fallible_collections;
+extern crate num_traits;
+use bitreader::{BitReader, ReadInto};
+use byteorder::{ReadBytesExt, WriteBytesExt};
+
+use fallible_collections::TryRead;
+use fallible_collections::TryReserveError;
+
+use num_traits::Num;
+use std::convert::{TryFrom, TryInto as _};
+use std::fmt;
+use std::io::Cursor;
+use std::io::{Read, Take};
+
+#[macro_use]
+mod macros;
+
+mod boxes;
+use boxes::{BoxType, FourCC};
+
+// Unit tests.
+#[cfg(test)]
+mod tests;
+
+#[cfg(feature = "unstable-api")]
+pub mod unstable;
+
+/// The 'mif1' brand indicates structural requirements on files
+/// See HEIF (ISO 23008-12:2017) § 10.2.1
+const MIF1_BRAND: FourCC = FourCC { value: *b"mif1" };
+
+/// A trait to indicate a type can be infallibly converted to `u64`.
+/// This should only be implemented for infallible conversions, so only unsigned types are valid.
+trait ToU64 {
+ // Remove when https://github.com/rust-lang/rust-clippy/issues/6727 is resolved
+ #[allow(clippy::wrong_self_convention)]
+ fn to_u64(self) -> u64;
+}
+
+/// Statically verify that the platform `usize` can fit within a `u64`.
+/// If the size won't fit on the given platform, this will fail at compile time, but if a type
+/// which can fail TryInto<usize> is used, it may panic.
+impl ToU64 for usize {
+ fn to_u64(self) -> u64 {
+ static_assertions::const_assert!(
+ std::mem::size_of::<usize>() <= std::mem::size_of::<u64>()
+ );
+ self.try_into().expect("usize -> u64 conversion failed")
+ }
+}
+
+/// A trait to indicate a type can be infallibly converted to `usize`.
+/// This should only be implemented for infallible conversions, so only unsigned types are valid.
+pub trait ToUsize {
+ fn to_usize(self) -> usize;
+}
+
+/// Statically verify that the given type can fit within a `usize`.
+/// If the size won't fit on the given platform, this will fail at compile time, but if a type
+/// which can fail TryInto<usize> is used, it may panic.
+macro_rules! impl_to_usize_from {
+ ( $from_type:ty ) => {
+ impl ToUsize for $from_type {
+ fn to_usize(self) -> usize {
+ static_assertions::const_assert!(
+ std::mem::size_of::<$from_type>() <= std::mem::size_of::<usize>()
+ );
+ self.try_into().expect(concat!(
+ stringify!($from_type),
+ " -> usize conversion failed"
+ ))
+ }
+ }
+ };
+}
+
+impl_to_usize_from!(u8);
+impl_to_usize_from!(u16);
+impl_to_usize_from!(u32);
+
+/// Indicate the current offset (i.e., bytes already read) in a reader
+trait Offset {
+ fn offset(&self) -> u64;
+}
+
+/// Wraps a reader to track the current offset
+struct OffsetReader<'a, T: 'a> {
+ reader: &'a mut T,
+ offset: u64,
+}
+
+impl<'a, T> OffsetReader<'a, T> {
+ fn new(reader: &'a mut T) -> Self {
+ Self { reader, offset: 0 }
+ }
+}
+
+impl<'a, T> Offset for OffsetReader<'a, T> {
+ fn offset(&self) -> u64 {
+ self.offset
+ }
+}
+
+impl<'a, T: Read> Read for OffsetReader<'a, T> {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ let bytes_read = self.reader.read(buf)?;
+ self.offset = self
+ .offset
+ .checked_add(bytes_read.to_u64())
+ .expect("total bytes read too large for offset type");
+ Ok(bytes_read)
+ }
+}
+
+pub type TryVec<T> = fallible_collections::TryVec<T>;
+pub type TryString = fallible_collections::TryVec<u8>;
+pub type TryHashMap<K, V> = fallible_collections::TryHashMap<K, V>;
+pub type TryBox<T> = fallible_collections::TryBox<T>;
+
+// To ensure we don't use stdlib allocating types by accident
+#[allow(dead_code)]
+struct Vec;
+#[allow(dead_code)]
+struct Box;
+#[allow(dead_code)]
+struct HashMap;
+#[allow(dead_code)]
+struct String;
+
+/// The return value to the C API
+/// Any detail that needs to be communicated to the caller must be encoded here
+/// since the [`Error`] type's associated data is part of the FFI.
+#[repr(C)]
+#[derive(PartialEq, Debug)]
+pub enum Status {
+ Ok = 0,
+ BadArg = 1,
+ Invalid = 2,
+ Unsupported = 3,
+ Eof = 4,
+ Io = 5,
+ Oom = 6,
+ UnsupportedA1lx,
+ UnsupportedA1op,
+ UnsupportedClap,
+ UnsupportedGrid,
+ UnsupportedIpro,
+ UnsupportedLsel,
+}
+
+/// For convenience of creating an error for an unsupported feature which we
+/// want to communicate the specific feature back to the C API caller
+impl From<Status> for Error {
+ fn from(parse_status: Status) -> Self {
+ let msg = match parse_status {
+ Status::Ok
+ | Status::BadArg
+ | Status::Invalid
+ | Status::Unsupported
+ | Status::Eof
+ | Status::Io
+ | Status::Oom => {
+ panic!("Status -> Error is only for Status:UnsupportedXXX errors")
+ }
+
+ Status::UnsupportedA1lx => "AV1 layered image indexing (a1lx) is unsupported",
+ Status::UnsupportedA1op => "Operating point selection (a1op) is unsupported",
+ Status::UnsupportedClap => "Clean aperture (clap) transform is unsupported",
+ Status::UnsupportedGrid => "Grid-based images are unsupported",
+ Status::UnsupportedIpro => "Item protection (ipro) is unsupported",
+ Status::UnsupportedLsel => "Layer selection (lsel) is unsupported",
+ };
+ Self::UnsupportedDetail(parse_status, msg)
+ }
+}
+
+impl From<Error> for Status {
+ fn from(error: Error) -> Self {
+ match error {
+ Error::NoMoov | Error::InvalidData(_) => Self::Invalid,
+ Error::Unsupported(_) => Self::Unsupported,
+ Error::UnsupportedDetail(parse_status, _msg) => parse_status,
+ Error::UnexpectedEOF => Self::Eof,
+ Error::Io(_) => {
+ // Getting std::io::ErrorKind::UnexpectedEof is normal
+ // but our From trait implementation should have converted
+ // those to our Error::UnexpectedEOF variant.
+ Self::Io
+ }
+ Error::OutOfMemory => Self::Oom,
+ }
+ }
+}
+
+impl From<Result<(), Status>> for Status {
+ fn from(result: Result<(), Status>) -> Self {
+ match result {
+ Ok(()) => Status::Ok,
+ Err(Status::Ok) => unreachable!(),
+ Err(e) => e,
+ }
+ }
+}
+
+impl From<fallible_collections::TryReserveError> for Status {
+ fn from(_: fallible_collections::TryReserveError) -> Self {
+ Status::Oom
+ }
+}
+
+/// Describes parser failures.
+///
+/// This enum wraps the standard `io::Error` type, unified with
+/// our own parser error states and those of crates we use.
+#[derive(Debug)]
+pub enum Error {
+ /// Parse error caused by corrupt or malformed data.
+ InvalidData(&'static str),
+ /// Parse error caused by limited parser support rather than invalid data.
+ Unsupported(&'static str),
+ /// Similar to [`Self::Unsupported`], but for errors that have a specific
+ /// [`Status`] variant for communicating the detail across FFI.
+ /// See the helper [`From<Status> for Error`](enum.Error.html#impl-From<Status>)
+ UnsupportedDetail(Status, &'static str),
+ /// Reflect `std::io::ErrorKind::UnexpectedEof` for short data.
+ UnexpectedEOF,
+ /// Propagate underlying errors from `std::io`.
+ Io(std::io::Error),
+ /// read_mp4 terminated without detecting a moov box.
+ NoMoov,
+ /// Out of memory
+ OutOfMemory,
+}
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{:?}", self)
+ }
+}
+
+impl std::error::Error for Error {}
+
+impl From<bitreader::BitReaderError> for Error {
+ fn from(_: bitreader::BitReaderError) -> Error {
+ Error::InvalidData("invalid data")
+ }
+}
+
+impl From<std::io::Error> for Error {
+ fn from(err: std::io::Error) -> Error {
+ match err.kind() {
+ std::io::ErrorKind::UnexpectedEof => Error::UnexpectedEOF,
+ _ => Error::Io(err),
+ }
+ }
+}
+
+impl From<std::string::FromUtf8Error> for Error {
+ fn from(_: std::string::FromUtf8Error) -> Error {
+ Error::InvalidData("invalid utf8")
+ }
+}
+
+impl From<std::str::Utf8Error> for Error {
+ fn from(_: std::str::Utf8Error) -> Error {
+ Error::InvalidData("invalid utf8")
+ }
+}
+
+impl From<std::num::TryFromIntError> for Error {
+ fn from(_: std::num::TryFromIntError) -> Error {
+ Error::Unsupported("integer conversion failed")
+ }
+}
+
+impl From<Error> for std::io::Error {
+ fn from(err: Error) -> Self {
+ let kind = match err {
+ Error::InvalidData(_) => std::io::ErrorKind::InvalidData,
+ Error::UnexpectedEOF => std::io::ErrorKind::UnexpectedEof,
+ Error::Io(io_err) => return io_err,
+ _ => std::io::ErrorKind::Other,
+ };
+ Self::new(kind, err)
+ }
+}
+
+impl From<TryReserveError> for Error {
+ fn from(_: TryReserveError) -> Error {
+ Error::OutOfMemory
+ }
+}
+
+/// Result shorthand using our Error enum.
+pub type Result<T, E = Error> = std::result::Result<T, E>;
+
+/// Basic ISO box structure.
+///
+/// mp4 files are a sequence of possibly-nested 'box' structures. Each box
+/// begins with a header describing the length of the box's data and a
+/// four-byte box type which identifies the type of the box. Together these
+/// are enough to interpret the contents of that section of the file.
+///
+/// See ISOBMFF (ISO 14496-12:2020) § 4.2
+#[derive(Debug, Clone, Copy)]
+struct BoxHeader {
+ /// Box type.
+ name: BoxType,
+ /// Size of the box in bytes.
+ size: u64,
+ /// Offset to the start of the contained data (or header size).
+ offset: u64,
+ /// Uuid for extended type.
+ #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340
+ uuid: Option<[u8; 16]>,
+}
+
+impl BoxHeader {
+ const MIN_SIZE: u64 = 8; // 4-byte size + 4-byte type
+ const MIN_LARGE_SIZE: u64 = 16; // 4-byte size + 4-byte type + 16-byte size
+}
+
+/// File type box 'ftyp'.
+#[derive(Debug)]
+struct FileTypeBox {
+ #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340
+ major_brand: FourCC,
+ #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340
+ minor_version: u32,
+ compatible_brands: TryVec<FourCC>,
+}
+
+/// Movie header box 'mvhd'.
+#[derive(Debug)]
+struct MovieHeaderBox {
+ pub timescale: u32,
+ #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340
+ duration: u64,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct Matrix {
+ pub a: i32, // 16.16 fix point
+ pub b: i32, // 16.16 fix point
+ pub u: i32, // 2.30 fix point
+ pub c: i32, // 16.16 fix point
+ pub d: i32, // 16.16 fix point
+ pub v: i32, // 2.30 fix point
+ pub x: i32, // 16.16 fix point
+ pub y: i32, // 16.16 fix point
+ pub w: i32, // 2.30 fix point
+}
+
+/// Track header box 'tkhd'
+#[derive(Debug, Clone)]
+pub struct TrackHeaderBox {
+ track_id: u32,
+ pub disabled: bool,
+ pub duration: u64,
+ pub width: u32,
+ pub height: u32,
+ pub matrix: Matrix,
+}
+
+/// Edit list box 'elst'
+#[derive(Debug)]
+struct EditListBox {
+ edits: TryVec<Edit>,
+}
+
+#[derive(Debug)]
+struct Edit {
+ segment_duration: u64,
+ media_time: i64,
+ #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340
+ media_rate_integer: i16,
+ #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340
+ media_rate_fraction: i16,
+}
+
+/// Media header box 'mdhd'
+#[derive(Debug)]
+struct MediaHeaderBox {
+ timescale: u32,
+ duration: u64,
+}
+
+// Chunk offset box 'stco' or 'co64'
+#[derive(Debug)]
+pub struct ChunkOffsetBox {
+ pub offsets: TryVec<u64>,
+}
+
+// Sync sample box 'stss'
+#[derive(Debug)]
+pub struct SyncSampleBox {
+ pub samples: TryVec<u32>,
+}
+
+// Sample to chunk box 'stsc'
+#[derive(Debug)]
+pub struct SampleToChunkBox {
+ pub samples: TryVec<SampleToChunk>,
+}
+
+#[derive(Debug)]
+pub struct SampleToChunk {
+ pub first_chunk: u32,
+ pub samples_per_chunk: u32,
+ pub sample_description_index: u32,
+}
+
+// Sample size box 'stsz'
+#[derive(Debug)]
+pub struct SampleSizeBox {
+ pub sample_size: u32,
+ pub sample_sizes: TryVec<u32>,
+}
+
+// Time to sample box 'stts'
+#[derive(Debug)]
+pub struct TimeToSampleBox {
+ pub samples: TryVec<Sample>,
+}
+
+#[repr(C)]
+#[derive(Debug)]
+pub struct Sample {
+ pub sample_count: u32,
+ pub sample_delta: u32,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum TimeOffsetVersion {
+ Version0(u32),
+ Version1(i32),
+}
+
+#[derive(Debug, Clone)]
+pub struct TimeOffset {
+ pub sample_count: u32,
+ pub time_offset: TimeOffsetVersion,
+}
+
+#[derive(Debug)]
+pub struct CompositionOffsetBox {
+ pub samples: TryVec<TimeOffset>,
+}
+
+// Handler reference box 'hdlr'
+#[derive(Debug)]
+struct HandlerBox {
+ handler_type: FourCC,
+}
+
+// Sample description box 'stsd'
+#[derive(Debug)]
+pub struct SampleDescriptionBox {
+ pub descriptions: TryVec<SampleEntry>,
+}
+
+#[derive(Debug)]
+pub enum SampleEntry {
+ Audio(AudioSampleEntry),
+ Video(VideoSampleEntry),
+ Unknown,
+}
+
+/// An Elementary Stream Descriptor
+/// See MPEG-4 Systems (ISO 14496-1:2010) § 7.2.6.5
+#[allow(non_camel_case_types)]
+#[derive(Debug, Default)]
+pub struct ES_Descriptor {
+ pub audio_codec: CodecType,
+ pub audio_object_type: Option<u16>,
+ pub extended_audio_object_type: Option<u16>,
+ pub audio_sample_rate: Option<u32>,
+ pub audio_channel_count: Option<u16>,
+ #[cfg(feature = "mp4v")]
+ pub video_codec: CodecType,
+ pub codec_esds: TryVec<u8>,
+ pub decoder_specific_data: TryVec<u8>, // Data in DECODER_SPECIFIC_TAG
+}
+
+#[allow(non_camel_case_types)]
+#[derive(Debug)]
+pub enum AudioCodecSpecific {
+ ES_Descriptor(ES_Descriptor),
+ FLACSpecificBox(FLACSpecificBox),
+ OpusSpecificBox(OpusSpecificBox),
+ ALACSpecificBox(ALACSpecificBox),
+ MP3,
+ LPCM,
+ #[cfg(feature = "3gpp")]
+ AMRSpecificBox(TryVec<u8>),
+}
+
+#[derive(Debug)]
+pub struct AudioSampleEntry {
+ pub codec_type: CodecType,
+ #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340
+ data_reference_index: u16,
+ pub channelcount: u32,
+ pub samplesize: u16,
+ pub samplerate: f64,
+ pub codec_specific: AudioCodecSpecific,
+ pub protection_info: TryVec<ProtectionSchemeInfoBox>,
+}
+
+#[derive(Debug)]
+pub enum VideoCodecSpecific {
+ AVCConfig(TryVec<u8>),
+ VPxConfig(VPxConfigBox),
+ AV1Config(AV1ConfigBox),
+ ESDSConfig(TryVec<u8>),
+ H263Config(TryVec<u8>),
+}
+
+#[derive(Debug)]
+pub struct VideoSampleEntry {
+ pub codec_type: CodecType,
+ #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340
+ data_reference_index: u16,
+ pub width: u16,
+ pub height: u16,
+ pub codec_specific: VideoCodecSpecific,
+ pub protection_info: TryVec<ProtectionSchemeInfoBox>,
+}
+
+/// Represent a Video Partition Codec Configuration 'vpcC' box (aka vp9). The meaning of each
+/// field is covered in detail in "VP Codec ISO Media File Format Binding".
+#[derive(Debug)]
+pub struct VPxConfigBox {
+ /// An integer that specifies the VP codec profile.
+ #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340
+ profile: u8,
+ /// An integer that specifies a VP codec level all samples conform to the following table.
+ /// For a description of the various levels, please refer to the VP9 Bitstream Specification.
+ #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340
+ level: u8,
+ /// An integer that specifies the bit depth of the luma and color components. Valid values
+ /// are 8, 10, and 12.
+ pub bit_depth: u8,
+ /// Really an enum defined by the "Colour primaries" section of ISO 23091-2:2019 § 8.1.
+ pub colour_primaries: u8,
+ /// Really an enum defined by "VP Codec ISO Media File Format Binding".
+ pub chroma_subsampling: u8,
+ /// Really an enum defined by the "Transfer characteristics" section of ISO 23091-2:2019 § 8.2.
+ #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340
+ transfer_characteristics: u8,
+ /// Really an enum defined by the "Matrix coefficients" section of ISO 23091-2:2019 § 8.3.
+ /// Available in 'VP Codec ISO Media File Format' version 1 only.
+ #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340
+ matrix_coefficients: Option<u8>,
+ /// Indicates the black level and range of the luma and chroma signals. 0 = legal range
+ /// (e.g. 16-235 for 8 bit sample depth); 1 = full range (e.g. 0-255 for 8-bit sample depth).
+ #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340
+ video_full_range_flag: bool,
+ /// This is not used for VP8 and VP9 . Intended for binary codec initialization data.
+ pub codec_init: TryVec<u8>,
+}
+
+/// See [AV1-ISOBMFF § 2.3.3](https://aomediacodec.github.io/av1-isobmff/#av1codecconfigurationbox-syntax)
+#[derive(Debug)]
+pub struct AV1ConfigBox {
+ pub profile: u8,
+ pub level: u8,
+ pub tier: u8,
+ pub bit_depth: u8,
+ pub monochrome: bool,
+ pub chroma_subsampling_x: u8,
+ pub chroma_subsampling_y: u8,
+ pub chroma_sample_position: u8,
+ pub initial_presentation_delay_present: bool,
+ pub initial_presentation_delay_minus_one: u8,
+ // The raw config contained in the av1c box. Because some decoders accept this data as a binary
+ // blob, rather than as structured data, we store the blob here for convenience.
+ pub raw_config: TryVec<u8>,
+}
+
+impl AV1ConfigBox {
+ const CONFIG_OBUS_OFFSET: usize = 4;
+
+ pub fn config_obus(&self) -> &[u8] {
+ &self.raw_config[Self::CONFIG_OBUS_OFFSET..]
+ }
+}
+
+#[derive(Debug)]
+pub struct FLACMetadataBlock {
+ pub block_type: u8,
+ pub data: TryVec<u8>,
+}
+
+/// Represents a FLACSpecificBox 'dfLa'
+#[derive(Debug)]
+pub struct FLACSpecificBox {
+ #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340
+ version: u8,
+ pub blocks: TryVec<FLACMetadataBlock>,
+}
+
+#[derive(Debug)]
+struct ChannelMappingTable {
+ stream_count: u8,
+ coupled_count: u8,
+ channel_mapping: TryVec<u8>,
+}
+
+/// Represent an OpusSpecificBox 'dOps'
+#[derive(Debug)]
+pub struct OpusSpecificBox {
+ pub version: u8,
+ output_channel_count: u8,
+ pre_skip: u16,
+ input_sample_rate: u32,
+ output_gain: i16,
+ channel_mapping_family: u8,
+ channel_mapping_table: Option<ChannelMappingTable>,
+}
+
+/// Represent an ALACSpecificBox 'alac'
+#[derive(Debug)]
+pub struct ALACSpecificBox {
+ #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340
+ version: u8,
+ pub data: TryVec<u8>,
+}
+
+#[derive(Debug)]
+pub struct MovieExtendsBox {
+ pub fragment_duration: Option<MediaScaledTime>,
+}
+
+pub type ByteData = TryVec<u8>;
+
+#[derive(Debug, Default)]
+pub struct ProtectionSystemSpecificHeaderBox {
+ pub system_id: ByteData,
+ pub kid: TryVec<ByteData>,
+ pub data: ByteData,
+
+ // The entire pssh box (include header) required by Gecko.
+ pub box_content: ByteData,
+}
+
+#[derive(Debug, Default, Clone)]
+pub struct SchemeTypeBox {
+ pub scheme_type: FourCC,
+ pub scheme_version: u32,
+}
+
+#[derive(Debug, Default)]
+pub struct TrackEncryptionBox {
+ pub is_encrypted: u8,
+ pub iv_size: u8,
+ pub kid: TryVec<u8>,
+ // Members for pattern encryption schemes
+ pub crypt_byte_block_count: Option<u8>,
+ pub skip_byte_block_count: Option<u8>,
+ pub constant_iv: Option<TryVec<u8>>,
+ // End pattern encryption scheme members
+}
+
+#[derive(Debug, Default)]
+pub struct ProtectionSchemeInfoBox {
+ pub original_format: FourCC,
+ pub scheme_type: Option<SchemeTypeBox>,
+ pub tenc: Option<TrackEncryptionBox>,
+}
+
+/// Represents a userdata box 'udta'.
+/// Currently, only the metadata atom 'meta'
+/// is parsed.
+#[derive(Debug, Default)]
+pub struct UserdataBox {
+ pub meta: Option<MetadataBox>,
+}
+
+/// Represents possible contents of the
+/// ©gen or gnre atoms within a metadata box.
+/// 'udta.meta.ilst' may only have either a
+/// standard genre box 'gnre' or a custom
+/// genre box '©gen', but never both at once.
+#[derive(Debug, PartialEq)]
+pub enum Genre {
+ /// A standard ID3v1 numbered genre.
+ StandardGenre(u8),
+ /// Any custom genre string.
+ CustomGenre(TryString),
+}
+
+/// Represents the contents of a 'stik'
+/// atom that indicates content types within
+/// iTunes.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum MediaType {
+ /// Movie is stored as 0 in a 'stik' atom.
+ Movie, // 0
+ /// Normal is stored as 1 in a 'stik' atom.
+ Normal, // 1
+ /// AudioBook is stored as 2 in a 'stik' atom.
+ AudioBook, // 2
+ /// WhackedBookmark is stored as 5 in a 'stik' atom.
+ WhackedBookmark, // 5
+ /// MusicVideo is stored as 6 in a 'stik' atom.
+ MusicVideo, // 6
+ /// ShortFilm is stored as 9 in a 'stik' atom.
+ ShortFilm, // 9
+ /// TVShow is stored as 10 in a 'stik' atom.
+ TVShow, // 10
+ /// Booklet is stored as 11 in a 'stik' atom.
+ Booklet, // 11
+ /// An unknown 'stik' value.
+ Unknown(u8),
+}
+
+/// Represents the parental advisory rating on the track,
+/// stored within the 'rtng' atom.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum AdvisoryRating {
+ /// Clean is always stored as 2 in an 'rtng' atom.
+ Clean, // 2
+ /// A value of 0 in an 'rtng' atom indicates 'Inoffensive'
+ Inoffensive, // 0
+ /// Any non 2 or 0 value in 'rtng' indicates the track is explicit.
+ Explicit(u8),
+}
+
+/// Represents the contents of 'ilst' atoms within
+/// a metadata box 'meta', parsed as iTunes metadata using
+/// the conventional tags.
+#[derive(Debug, Default)]
+pub struct MetadataBox {
+ /// The album name, '©alb'
+ pub album: Option<TryString>,
+ /// The artist name '©art' or '©ART'
+ pub artist: Option<TryString>,
+ /// The album artist 'aART'
+ pub album_artist: Option<TryString>,
+ /// Track comments '©cmt'
+ pub comment: Option<TryString>,
+ /// The date or year field '©day'
+ ///
+ /// This is stored as an arbitrary string,
+ /// and may not necessarily be in a valid date
+ /// format.
+ pub year: Option<TryString>,
+ /// The track title '©nam'
+ pub title: Option<TryString>,
+ /// The track genre '©gen' or 'gnre'.
+ pub genre: Option<Genre>,
+ /// The track number 'trkn'.
+ pub track_number: Option<u8>,
+ /// The disc number 'disk'
+ pub disc_number: Option<u8>,
+ /// The total number of tracks on the disc,
+ /// stored in 'trkn'
+ pub total_tracks: Option<u8>,
+ /// The total number of discs in the album,
+ /// stored in 'disk'
+ pub total_discs: Option<u8>,
+ /// The composer of the track '©wrt'
+ pub composer: Option<TryString>,
+ /// The encoder used to create this track '©too'
+ pub encoder: Option<TryString>,
+ /// The encoded-by settingo this track '©enc'
+ pub encoded_by: Option<TryString>,
+ /// The tempo or BPM of the track 'tmpo'
+ pub beats_per_minute: Option<u8>,
+ /// Copyright information of the track 'cprt'
+ pub copyright: Option<TryString>,
+ /// Whether or not this track is part of a compilation 'cpil'
+ pub compilation: Option<bool>,
+ /// The advisory rating of this track 'rtng'
+ pub advisory: Option<AdvisoryRating>,
+ /// The personal rating of this track, 'rate'.
+ ///
+ /// This is stored in the box as string data, but
+ /// the format is an integer percentage from 0 - 100,
+ /// where 100 is displayed as 5 stars out of 5.
+ pub rating: Option<TryString>,
+ /// The grouping this track belongs to '©grp'
+ pub grouping: Option<TryString>,
+ /// The media type of this track 'stik'
+ pub media_type: Option<MediaType>, // stik
+ /// Whether or not this track is a podcast 'pcst'
+ pub podcast: Option<bool>,
+ /// The category of ths track 'catg'
+ pub category: Option<TryString>,
+ /// The podcast keyword 'keyw'
+ pub keyword: Option<TryString>,
+ /// The podcast url 'purl'
+ pub podcast_url: Option<TryString>,
+ /// The podcast episode GUID 'egid'
+ pub podcast_guid: Option<TryString>,
+ /// The description of the track 'desc'
+ pub description: Option<TryString>,
+ /// The long description of the track 'ldes'.
+ ///
+ /// Unlike other string fields, the long description field
+ /// can be longer than 256 characters.
+ pub long_description: Option<TryString>,
+ /// The lyrics of the track '©lyr'.
+ ///
+ /// Unlike other string fields, the lyrics field
+ /// can be longer than 256 characters.
+ pub lyrics: Option<TryString>,
+ /// The name of the TV network this track aired on 'tvnn'.
+ pub tv_network_name: Option<TryString>,
+ /// The name of the TV Show for this track 'tvsh'.
+ pub tv_show_name: Option<TryString>,
+ /// The name of the TV Episode for this track 'tven'.
+ pub tv_episode_name: Option<TryString>,
+ /// The number of the TV Episode for this track 'tves'.
+ pub tv_episode_number: Option<u8>,
+ /// The season of the TV Episode of this track 'tvsn'.
+ pub tv_season: Option<u8>,
+ /// The date this track was purchased 'purd'.
+ pub purchase_date: Option<TryString>,
+ /// Whether or not this track supports gapless playback 'pgap'
+ pub gapless_playback: Option<bool>,
+ /// Any cover artwork attached to this track 'covr'
+ ///
+ /// 'covr' is unique in that it may contain multiple 'data' sub-entries,
+ /// each an image file. Here, each subentry's raw binary data is exposed,
+ /// which may contain image data in JPEG or PNG format.
+ pub cover_art: Option<TryVec<TryVec<u8>>>,
+ /// The owner of the track 'ownr'
+ pub owner: Option<TryString>,
+ /// Whether or not this track is HD Video 'hdvd'
+ pub hd_video: Option<bool>,
+ /// The name of the track to sort by 'sonm'
+ pub sort_name: Option<TryString>,
+ /// The name of the album to sort by 'soal'
+ pub sort_album: Option<TryString>,
+ /// The name of the artist to sort by 'soar'
+ pub sort_artist: Option<TryString>,
+ /// The name of the album artist to sort by 'soaa'
+ pub sort_album_artist: Option<TryString>,
+ /// The name of the composer to sort by 'soco'
+ pub sort_composer: Option<TryString>,
+ /// Metadata
+ #[cfg(feature = "meta-xml")]
+ pub xml: Option<XmlBox>,
+}
+
+/// See ISOBMFF (ISO 14496-12:2020) § 8.11.2.1
+#[cfg(feature = "meta-xml")]
+#[derive(Debug)]
+pub enum XmlBox {
+ /// XML metadata
+ StringXmlBox(TryString),
+ /// Binary XML metadata
+ BinaryXmlBox(TryVec<u8>),
+}
+
+/// Internal data structures.
+#[derive(Debug, Default)]
+pub struct MediaContext {
+ pub timescale: Option<MediaTimeScale>,
+ /// Tracks found in the file.
+ pub tracks: TryVec<Track>,
+ pub mvex: Option<MovieExtendsBox>,
+ pub psshs: TryVec<ProtectionSystemSpecificHeaderBox>,
+ pub userdata: Option<Result<UserdataBox>>,
+ #[cfg(feature = "meta-xml")]
+ pub metadata: Option<Result<MetadataBox>>,
+}
+
+/// An ISOBMFF item as described by an iloc box. For the sake of avoiding copies,
+/// this can either be represented by the `Location` variant, which indicates
+/// where the data exists within a `MediaDataBox` stored separately, or the
+/// `Data` variant which owns the data. Unfortunately, it's not simple to
+/// represent this as a [`std::borrow::Cow`], or other reference-based type, because
+/// multiple instances may references different parts of the same [`MediaDataBox`]
+/// and we want to avoid the copy that splitting the storage would entail.
+#[derive(Debug)]
+enum IsobmffItem {
+ Location(Extent),
+ Data(TryVec<u8>),
+}
+
+#[derive(Debug)]
+struct AvifItem {
+ /// The `item_ID` from ISOBMFF (ISO 14496-12:2020) § 8.11.3
+ ///
+ /// See [`read_iloc`]
+ id: ItemId,
+
+ /// AV1 Image Item per <https://aomediacodec.github.io/av1-avif/#image-item>
+ image_data: IsobmffItem,
+}
+
+impl AvifItem {
+ fn with_data_location(id: ItemId, extent: Extent) -> Self {
+ Self {
+ id,
+ image_data: IsobmffItem::Location(extent),
+ }
+ }
+
+ fn with_inline_data(id: ItemId) -> Self {
+ Self {
+ id,
+ image_data: IsobmffItem::Data(TryVec::new()),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct AvifContext {
+ /// Level of deviation from the specification before failing the parse
+ strictness: ParseStrictness,
+ /// Referred to by the `Location` variants of the `AvifItem`s in this struct
+ item_storage: TryVec<MediaDataBox>,
+ /// The item indicated by the `pitm` box, See ISOBMFF (ISO 14496-12:2020) § 8.11.4
+ primary_item: AvifItem,
+ /// Associated alpha channel for the primary item, if any
+ alpha_item: Option<AvifItem>,
+ /// If true, divide RGB values by the alpha value.
+ /// See `prem` in MIAF (ISO 23000-22:2019) § 7.3.5.2
+ pub premultiplied_alpha: bool,
+ /// All properties associated with `primary_item` or `alpha_item`
+ item_properties: ItemPropertiesBox,
+}
+
+impl AvifContext {
+ pub fn primary_item_coded_data(&self) -> &[u8] {
+ self.item_as_slice(&self.primary_item)
+ }
+
+ pub fn primary_item_bits_per_channel(&self) -> Result<&[u8]> {
+ self.image_bits_per_channel(self.primary_item.id)
+ }
+
+ pub fn alpha_item_coded_data(&self) -> &[u8] {
+ self.alpha_item
+ .as_ref()
+ .map_or(&[], |item| self.item_as_slice(item))
+ }
+
+ pub fn alpha_item_bits_per_channel(&self) -> Result<&[u8]> {
+ self.alpha_item
+ .as_ref()
+ .map_or(Ok(&[]), |item| self.image_bits_per_channel(item.id))
+ }
+
+ fn image_bits_per_channel(&self, item_id: ItemId) -> Result<&[u8]> {
+ match self
+ .item_properties
+ .get(item_id, BoxType::PixelInformationBox)?
+ {
+ Some(ItemProperty::Channels(pixi)) => Ok(pixi.bits_per_channel.as_slice()),
+ Some(other_property) => panic!("property key mismatch: {:?}", other_property),
+ None => Ok(&[]),
+ }
+ }
+
+ pub fn spatial_extents_ptr(&self) -> Result<*const ImageSpatialExtentsProperty> {
+ match self
+ .item_properties
+ .get(self.primary_item.id, BoxType::ImageSpatialExtentsProperty)?
+ {
+ Some(ItemProperty::ImageSpatialExtents(ispe)) => Ok(ispe),
+ Some(other_property) => panic!("property key mismatch: {:?}", other_property),
+ None => {
+ fail_if(
+ self.strictness == ParseStrictness::Permissive,
+ "ispe is a mandatory property",
+ )?;
+ Ok(std::ptr::null())
+ }
+ }
+ }
+
+ pub fn nclx_colour_information_ptr(&self) -> Result<*const NclxColourInformation> {
+ let nclx_colr_boxes = self
+ .item_properties
+ .get_multiple(self.primary_item.id, |prop| {
+ matches!(prop, ItemProperty::Colour(ColourInformation::Nclx(_)))
+ })?;
+
+ match *nclx_colr_boxes.as_slice() {
+ [] => Ok(std::ptr::null()),
+ [ItemProperty::Colour(ColourInformation::Nclx(nclx)), ..] => {
+ if nclx_colr_boxes.len() > 1 {
+ warn!("Multiple nclx colr boxes, using first");
+ }
+ Ok(nclx)
+ }
+ _ => unreachable!("Expect only ColourInformation::Nclx(_) matches"),
+ }
+ }
+
+ pub fn icc_colour_information(&self) -> Result<&[u8]> {
+ let icc_colr_boxes = self
+ .item_properties
+ .get_multiple(self.primary_item.id, |prop| {
+ matches!(prop, ItemProperty::Colour(ColourInformation::Icc(_, _)))
+ })?;
+
+ match *icc_colr_boxes.as_slice() {
+ [] => Ok(&[]),
+ [ItemProperty::Colour(ColourInformation::Icc(icc, _)), ..] => {
+ if icc_colr_boxes.len() > 1 {
+ warn!("Multiple ICC profiles in colr boxes, using first");
+ }
+ Ok(icc.bytes.as_slice())
+ }
+ _ => unreachable!("Expect only ColourInformation::Icc(_) matches"),
+ }
+ }
+
+ pub fn image_rotation(&self) -> Result<ImageRotation> {
+ match self
+ .item_properties
+ .get(self.primary_item.id, BoxType::ImageRotation)?
+ {
+ Some(ItemProperty::Rotation(irot)) => Ok(*irot),
+ Some(other_property) => panic!("property key mismatch: {:?}", other_property),
+ None => Ok(ImageRotation::D0),
+ }
+ }
+
+ pub fn image_mirror_ptr(&self) -> Result<*const ImageMirror> {
+ match self
+ .item_properties
+ .get(self.primary_item.id, BoxType::ImageMirror)?
+ {
+ Some(ItemProperty::Mirroring(imir)) => Ok(imir),
+ Some(other_property) => panic!("property key mismatch: {:?}", other_property),
+ None => Ok(std::ptr::null()),
+ }
+ }
+
+ /// A helper for the various `AvifItem`s to expose a reference to the
+ /// underlying data while avoiding copies.
+ fn item_as_slice<'a>(&'a self, item: &'a AvifItem) -> &'a [u8] {
+ match &item.image_data {
+ IsobmffItem::Location(extent) => {
+ for mdat in &self.item_storage {
+ if let Some(slice) = mdat.get(extent) {
+ return slice;
+ }
+ }
+ unreachable!(
+ "IsobmffItem::Location requires the location exists in AvifContext::item_storage"
+ );
+ }
+ IsobmffItem::Data(data) => data.as_slice(),
+ }
+ }
+}
+
+struct AvifMeta {
+ item_references: TryVec<SingleItemTypeReferenceBox>,
+ item_properties: ItemPropertiesBox,
+ primary_item_id: ItemId,
+ iloc_items: TryHashMap<ItemId, ItemLocationBoxItem>,
+}
+
+/// A Media Data Box
+/// See ISOBMFF (ISO 14496-12:2020) § 8.1.1
+struct MediaDataBox {
+ /// Offset of `data` from the beginning of the "file". See ConstructionMethod::File.
+ /// Note: the file may not be an actual file, read_avif supports any `&mut impl Read`
+ /// source for input. However we try to match the terminology used in the spec.
+ file_offset: u64,
+ data: TryVec<u8>,
+}
+
+impl fmt::Debug for MediaDataBox {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct("MediaDataBox")
+ .field("file_offset", &self.file_offset)
+ .field("data", &format_args!("{} bytes", self.data.len()))
+ .finish()
+ }
+}
+
+impl MediaDataBox {
+ /// Convert an absolute offset to an offset relative to the beginning of the
+ /// `self.data` field. Returns None if the offset would be negative.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the offset would overflow a `usize`.
+ fn file_offset_to_data_offset(&self, offset: u64) -> Option<usize> {
+ let start = offset
+ .checked_sub(self.file_offset)?
+ .try_into()
+ .expect("usize overflow");
+ Some(start)
+ }
+
+ /// Return a slice from the MediaDataBox specified by the provided `extent`.
+ /// Returns `None` if the extent isn't fully contained by the MediaDataBox.
+ ///
+ /// # Panics
+ ///
+ /// Panics if either the offset or length (if the extent is bounded) of the
+ /// slice would overflow a `usize`.
+ pub fn get<'a>(&'a self, extent: &'a Extent) -> Option<&'a [u8]> {
+ match extent {
+ Extent::WithLength { offset, len } => {
+ let start = self.file_offset_to_data_offset(*offset)?;
+ let end = start.checked_add(*len).expect("usize overflow");
+ self.data.get(start..end)
+ }
+ Extent::ToEnd { offset } => {
+ let start = self.file_offset_to_data_offset(*offset)?;
+ self.data.get(start..)
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod media_data_box_tests {
+ use super::*;
+
+ impl MediaDataBox {
+ fn at_offset(file_offset: u64, data: std::vec::Vec<u8>) -> Self {
+ MediaDataBox {
+ file_offset,
+ data: data.into(),
+ }
+ }
+ }
+
+ #[test]
+ fn extent_with_length_before_mdat_returns_none() {
+ let mdat = MediaDataBox::at_offset(100, vec![1; 5]);
+ let extent = Extent::WithLength { offset: 0, len: 2 };
+
+ assert!(mdat.get(&extent).is_none());
+ }
+
+ #[test]
+ fn extent_to_end_before_mdat_returns_none() {
+ let mdat = MediaDataBox::at_offset(100, vec![1; 5]);
+ let extent = Extent::ToEnd { offset: 0 };
+
+ assert!(mdat.get(&extent).is_none());
+ }
+
+ #[test]
+ fn extent_with_length_crossing_front_mdat_boundary_returns_none() {
+ let mdat = MediaDataBox::at_offset(100, vec![1; 5]);
+ let extent = Extent::WithLength { offset: 99, len: 3 };
+
+ assert!(mdat.get(&extent).is_none());
+ }
+
+ #[test]
+ fn extent_with_length_which_is_subset_of_mdat() {
+ let mdat = MediaDataBox::at_offset(100, vec![1; 5]);
+ let extent = Extent::WithLength {
+ offset: 101,
+ len: 2,
+ };
+
+ assert_eq!(mdat.get(&extent), Some(&[1, 1][..]));
+ }
+
+ #[test]
+ fn extent_to_end_which_is_subset_of_mdat() {
+ let mdat = MediaDataBox::at_offset(100, vec![1; 5]);
+ let extent = Extent::ToEnd { offset: 101 };
+
+ assert_eq!(mdat.get(&extent), Some(&[1, 1, 1, 1][..]));
+ }
+
+ #[test]
+ fn extent_with_length_which_is_all_of_mdat() {
+ let mdat = MediaDataBox::at_offset(100, vec![1; 5]);
+ let extent = Extent::WithLength {
+ offset: 100,
+ len: 5,
+ };
+
+ assert_eq!(mdat.get(&extent), Some(mdat.data.as_slice()));
+ }
+
+ #[test]
+ fn extent_to_end_which_is_all_of_mdat() {
+ let mdat = MediaDataBox::at_offset(100, vec![1; 5]);
+ let extent = Extent::ToEnd { offset: 100 };
+
+ assert_eq!(mdat.get(&extent), Some(mdat.data.as_slice()));
+ }
+
+ #[test]
+ fn extent_with_length_crossing_back_mdat_boundary_returns_none() {
+ let mdat = MediaDataBox::at_offset(100, vec![1; 5]);
+ let extent = Extent::WithLength {
+ offset: 103,
+ len: 3,
+ };
+
+ assert!(mdat.get(&extent).is_none());
+ }
+
+ #[test]
+ fn extent_with_length_after_mdat_returns_none() {
+ let mdat = MediaDataBox::at_offset(100, vec![1; 5]);
+ let extent = Extent::WithLength {
+ offset: 200,
+ len: 2,
+ };
+
+ assert!(mdat.get(&extent).is_none());
+ }
+
+ #[test]
+ fn extent_to_end_after_mdat_returns_none() {
+ let mdat = MediaDataBox::at_offset(100, vec![1; 5]);
+ let extent = Extent::ToEnd { offset: 200 };
+
+ assert!(mdat.get(&extent).is_none());
+ }
+
+ #[test]
+ #[should_panic(expected = "usize overflow")]
+ fn extent_with_length_which_overflows_usize_panics() {
+ let mdat = MediaDataBox::at_offset(std::u64::MAX - 1, vec![1; 5]);
+ let extent = Extent::WithLength {
+ offset: std::u64::MAX,
+ len: std::usize::MAX,
+ };
+
+ mdat.get(&extent);
+ }
+
+ // The end of the range would overflow `usize` if it were calculated, but
+ // because the range end is unbounded, we don't calculate it.
+ #[test]
+ fn extent_to_end_which_overflows_usize() {
+ let mdat = MediaDataBox::at_offset(std::u64::MAX - 1, vec![1; 5]);
+ let extent = Extent::ToEnd {
+ offset: std::u64::MAX,
+ };
+
+ assert_eq!(mdat.get(&extent), Some(&[1, 1, 1, 1][..]));
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+struct PropertyIndex(u16);
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)]
+struct ItemId(u32);
+
+impl ItemId {
+ fn read(src: &mut impl ReadBytesExt, version: u8) -> Result<ItemId> {
+ Ok(ItemId(if version == 0 {
+ be_u16(src)?.into()
+ } else {
+ be_u32(src)?
+ }))
+ }
+}
+
+/// Used for 'infe' boxes within 'iinf' boxes
+/// See ISOBMFF (ISO 14496-12:2020) § 8.11.6
+/// Only versions {2, 3} are supported
+#[derive(Debug)]
+struct ItemInfoEntry {
+ item_id: ItemId,
+ item_type: u32,
+}
+
+/// See ISOBMFF (ISO 14496-12:2020) § 8.11.12
+#[derive(Debug)]
+struct SingleItemTypeReferenceBox {
+ item_type: FourCC,
+ from_item_id: ItemId,
+ to_item_id: ItemId,
+}
+
+/// Potential sizes (in bytes) of variable-sized fields of the 'iloc' box
+/// See ISOBMFF (ISO 14496-12:2020) § 8.11.3
+#[derive(Debug, Clone, Copy, PartialEq)]
+enum IlocFieldSize {
+ Zero,
+ Four,
+ Eight,
+}
+
+impl IlocFieldSize {
+ fn as_bits(&self) -> u8 {
+ match self {
+ IlocFieldSize::Zero => 0,
+ IlocFieldSize::Four => 32,
+ IlocFieldSize::Eight => 64,
+ }
+ }
+}
+
+impl TryFrom<u8> for IlocFieldSize {
+ type Error = Error;
+
+ fn try_from(value: u8) -> Result<Self> {
+ match value {
+ 0 => Ok(Self::Zero),
+ 4 => Ok(Self::Four),
+ 8 => Ok(Self::Eight),
+ _ => Err(Error::InvalidData("value must be in the set {0, 4, 8}")),
+ }
+ }
+}
+
+#[derive(PartialEq)]
+enum IlocVersion {
+ Zero,
+ One,
+ Two,
+}
+
+impl TryFrom<u8> for IlocVersion {
+ type Error = Error;
+
+ fn try_from(value: u8) -> Result<Self> {
+ match value {
+ 0 => Ok(Self::Zero),
+ 1 => Ok(Self::One),
+ 2 => Ok(Self::Two),
+ _ => Err(Error::Unsupported("unsupported version in 'iloc' box")),
+ }
+ }
+}
+
+/// Used for 'iloc' boxes
+/// See ISOBMFF (ISO 14496-12:2020) § 8.11.3
+/// `base_offset` is omitted since it is integrated into the ranges in `extents`
+/// `data_reference_index` is omitted, since only 0 (i.e., this file) is supported
+#[derive(Debug)]
+struct ItemLocationBoxItem {
+ construction_method: ConstructionMethod,
+ /// Unused for ConstructionMethod::Idat
+ extents: TryVec<Extent>,
+}
+
+/// See ISOBMFF (ISO 14496-12:2020) § 8.11.3
+///
+/// Note: per MIAF (ISO 23000-22:2019) § 7.2.1.7:<br />
+/// > MIAF image items are constrained as follows:<br />
+/// > — `construction_method` shall be equal to 0 for MIAF image items that are coded image items.<br />
+/// > — `construction_method` shall be equal to 0 or 1 for MIAF image items that are derived image items.
+#[derive(Clone, Copy, Debug, PartialEq)]
+enum ConstructionMethod {
+ File = 0,
+ Idat = 1,
+ #[allow(dead_code)] // TODO: see https://github.com/mozilla/mp4parse-rust/issues/196
+ Item = 2,
+}
+
+/// Describes a region where a item specified by an `ItemLocationBoxItem` is stored.
+/// The offset is `u64` since that's the maximum possible size and since the relative
+/// nature of `MediaDataBox` means this can still possibly succeed even in the case
+/// that the raw value exceeds std::usize::MAX on platforms where that type is smaller
+/// than u64. However, `len` is stored as a `usize` since no value larger than
+/// `std::usize::MAX` can be used in a successful indexing operation in rust.
+/// `extent_index` is omitted since it's only used for ConstructionMethod::Item which
+/// is currently not implemented.
+#[derive(Clone, Debug)]
+enum Extent {
+ WithLength { offset: u64, len: usize },
+ ToEnd { offset: u64 },
+}
+
+#[derive(Debug, PartialEq)]
+pub enum TrackType {
+ Audio,
+ Video,
+ Metadata,
+ Unknown,
+}
+
+impl Default for TrackType {
+ fn default() -> Self {
+ TrackType::Unknown
+ }
+}
+
+// This type is used by mp4parse_capi since it needs to be passed from FFI consumers
+// The C-visible struct is renamed via mp4parse_capi/cbindgen.toml to match naming conventions
+#[repr(C)]
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum ParseStrictness {
+ Permissive, // Error only on ambiguous inputs
+ Normal, // Error on "shall" directives, log warnings for "should"
+ Strict, // Error on "should" directives
+}
+
+fn fail_if(violation: bool, message: &'static str) -> Result<()> {
+ if violation {
+ Err(Error::InvalidData(message))
+ } else {
+ warn!("{}", message);
+ Ok(())
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum CodecType {
+ Unknown,
+ MP3,
+ AAC,
+ FLAC,
+ Opus,
+ H264, // 14496-10
+ MP4V, // 14496-2
+ AV1,
+ VP9,
+ VP8,
+ EncryptedVideo,
+ EncryptedAudio,
+ LPCM, // QT
+ ALAC,
+ H263,
+ #[cfg(feature = "3gpp")]
+ AMRNB,
+ #[cfg(feature = "3gpp")]
+ AMRWB,
+}
+
+impl Default for CodecType {
+ fn default() -> Self {
+ CodecType::Unknown
+ }
+}
+
+/// The media's global (mvhd) timescale in units per second.
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct MediaTimeScale(pub u64);
+
+/// A time to be scaled by the media's global (mvhd) timescale.
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct MediaScaledTime(pub u64);
+
+/// The track's local (mdhd) timescale.
+/// Members are timescale units per second and the track id.
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct TrackTimeScale<T: Num>(pub T, pub usize);
+
+/// A time to be scaled by the track's local (mdhd) timescale.
+/// Members are time in scale units and the track id.
+#[derive(Debug, Copy, Clone, PartialEq)]
+pub struct TrackScaledTime<T>(pub T, pub usize);
+
+impl<T> std::ops::Add for TrackScaledTime<T>
+where
+ T: num_traits::CheckedAdd,
+{
+ type Output = Option<Self>;
+
+ fn add(self, other: TrackScaledTime<T>) -> Self::Output {
+ self.0.checked_add(&other.0).map(|sum| Self(sum, self.1))
+ }
+}
+
+#[derive(Debug, Default)]
+pub struct Track {
+ pub id: usize,
+ pub track_type: TrackType,
+ pub empty_duration: Option<MediaScaledTime>,
+ pub media_time: Option<TrackScaledTime<u64>>,
+ pub timescale: Option<TrackTimeScale<u64>>,
+ pub duration: Option<TrackScaledTime<u64>>,
+ pub track_id: Option<u32>,
+ pub tkhd: Option<TrackHeaderBox>, // TODO(kinetik): find a nicer way to export this.
+ pub stsd: Option<SampleDescriptionBox>,
+ pub stts: Option<TimeToSampleBox>,
+ pub stsc: Option<SampleToChunkBox>,
+ pub stsz: Option<SampleSizeBox>,
+ pub stco: Option<ChunkOffsetBox>, // It is for stco or co64.
+ pub stss: Option<SyncSampleBox>,
+ pub ctts: Option<CompositionOffsetBox>,
+}
+
+impl Track {
+ fn new(id: usize) -> Track {
+ Track {
+ id,
+ ..Default::default()
+ }
+ }
+}
+
+/// See ISOBMFF (ISO 14496-12:2020) § 4.2
+struct BMFFBox<'a, T: 'a> {
+ head: BoxHeader,
+ content: Take<&'a mut T>,
+}
+
+struct BoxIter<'a, T: 'a> {
+ src: &'a mut T,
+}
+
+impl<'a, T: Read> BoxIter<'a, T> {
+ fn new(src: &mut T) -> BoxIter<T> {
+ BoxIter { src }
+ }
+
+ fn next_box(&mut self) -> Result<Option<BMFFBox<T>>> {
+ let r = read_box_header(self.src);
+ match r {
+ Ok(h) => Ok(Some(BMFFBox {
+ head: h,
+ content: self.src.take(h.size - h.offset),
+ })),
+ Err(Error::UnexpectedEOF) => Ok(None),
+ Err(e) => Err(e),
+ }
+ }
+}
+
+impl<'a, T: Read> Read for BMFFBox<'a, T> {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ self.content.read(buf)
+ }
+}
+
+impl<'a, T: Read> TryRead for BMFFBox<'a, T> {
+ fn try_read_to_end(&mut self, buf: &mut TryVec<u8>) -> std::io::Result<usize> {
+ fallible_collections::try_read_up_to(self, self.bytes_left(), buf)
+ }
+}
+
+impl<'a, T: Offset> Offset for BMFFBox<'a, T> {
+ fn offset(&self) -> u64 {
+ self.content.get_ref().offset()
+ }
+}
+
+impl<'a, T: Read> BMFFBox<'a, T> {
+ fn bytes_left(&self) -> u64 {
+ self.content.limit()
+ }
+
+ fn get_header(&self) -> &BoxHeader {
+ &self.head
+ }
+
+ fn box_iter<'b>(&'b mut self) -> BoxIter<BMFFBox<'a, T>> {
+ BoxIter::new(self)
+ }
+}
+
+impl<'a, T> Drop for BMFFBox<'a, T> {
+ fn drop(&mut self) {
+ if self.content.limit() > 0 {
+ let name: FourCC = From::from(self.head.name);
+ debug!("Dropping {} bytes in '{}'", self.content.limit(), name);
+ }
+ }
+}
+
+/// Read and parse a box header.
+///
+/// Call this first to determine the type of a particular mp4 box
+/// and its length. Used internally for dispatching to specific
+/// parsers for the internal content, or to get the length to
+/// skip unknown or uninteresting boxes.
+///
+/// See ISOBMFF (ISO 14496-12:2020) § 4.2
+fn read_box_header<T: ReadBytesExt>(src: &mut T) -> Result<BoxHeader> {
+ let size32 = be_u32(src)?;
+ let name = BoxType::from(be_u32(src)?);
+ let size = match size32 {
+ // valid only for top-level box and indicates it's the last box in the file. usually mdat.
+ 0 => return Err(Error::Unsupported("unknown sized box")),
+ 1 => {
+ let size64 = be_u64(src)?;
+ if size64 < BoxHeader::MIN_LARGE_SIZE {
+ return Err(Error::InvalidData("malformed wide size"));
+ }
+ size64
+ }
+ _ => {
+ if u64::from(size32) < BoxHeader::MIN_SIZE {
+ return Err(Error::InvalidData("malformed size"));
+ }
+ u64::from(size32)
+ }
+ };
+ let mut offset = match size32 {
+ 1 => BoxHeader::MIN_LARGE_SIZE,
+ _ => BoxHeader::MIN_SIZE,
+ };
+ let uuid = if name == BoxType::UuidBox {
+ if size >= offset + 16 {
+ let mut buffer = [0u8; 16];
+ let count = src.read(&mut buffer)?;
+ offset += count.to_u64();
+ if count == 16 {
+ Some(buffer)
+ } else {
+ debug!("malformed uuid (short read), skipping");
+ None
+ }
+ } else {
+ debug!("malformed uuid, skipping");
+ None
+ }
+ } else {
+ None
+ };
+ assert!(offset <= size);
+ Ok(BoxHeader {
+ name,
+ size,
+ offset,
+ uuid,
+ })
+}
+
+/// Parse the extra header fields for a full box.
+fn read_fullbox_extra<T: ReadBytesExt>(src: &mut T) -> Result<(u8, u32)> {
+ let version = src.read_u8()?;
+ let flags_a = src.read_u8()?;
+ let flags_b = src.read_u8()?;
+ let flags_c = src.read_u8()?;
+ Ok((
+ version,
+ u32::from(flags_a) << 16 | u32::from(flags_b) << 8 | u32::from(flags_c),
+ ))
+}
+
+// Parse the extra fields for a full box whose flag fields must be zero.
+fn read_fullbox_version_no_flags<T: ReadBytesExt>(src: &mut T) -> Result<u8> {
+ let (version, flags) = read_fullbox_extra(src)?;
+
+ if flags != 0 {
+ return Err(Error::Unsupported("expected flags to be 0"));
+ }
+
+ Ok(version)
+}
+
+/// Skip over the entire contents of a box.
+fn skip_box_content<T: Read>(src: &mut BMFFBox<T>) -> Result<()> {
+ // Skip the contents of unknown chunks.
+ let to_skip = {
+ let header = src.get_header();
+ debug!("{:?} (skipped)", header);
+ header
+ .size
+ .checked_sub(header.offset)
+ .expect("header offset > size")
+ };
+ assert_eq!(to_skip, src.bytes_left());
+ skip(src, to_skip)
+}
+
+/// Skip over the remain data of a box.
+fn skip_box_remain<T: Read>(src: &mut BMFFBox<T>) -> Result<()> {
+ let remain = {
+ let header = src.get_header();
+ let len = src.bytes_left();
+ debug!("remain {} (skipped) in {:?}", len, header);
+ len
+ };
+ skip(src, remain)
+}
+
+/// Read the contents of an AVIF file
+pub fn read_avif<T: Read>(f: &mut T, strictness: ParseStrictness) -> Result<AvifContext> {
+ let _ = env_logger::try_init();
+
+ debug!("read_avif(strictness: {:?})", strictness);
+
+ let mut f = OffsetReader::new(f);
+
+ let mut iter = BoxIter::new(&mut f);
+
+ // 'ftyp' box must occur first; see ISOBMFF (ISO 14496-12:2020) § 4.3.1
+ if let Some(mut b) = iter.next_box()? {
+ if b.head.name == BoxType::FileTypeBox {
+ let ftyp = read_ftyp(&mut b)?;
+ if !ftyp.compatible_brands.contains(&MIF1_BRAND) {
+ // This mandatory inclusion of this brand is in the process of being changed
+ // to optional. In anticipation of that, only give an error in strict mode
+ // See https://github.com/MPEGGroup/MIAF/issues/5
+ // and https://github.com/MPEGGroup/FileFormat/issues/23
+ fail_if(
+ strictness == ParseStrictness::Strict,
+ "The FileTypeBox should contain 'mif1' in the compatible_brands list \
+ per MIAF (ISO 23000-22:2019) § 7.2.1.2",
+ )?;
+ }
+ } else {
+ return Err(Error::InvalidData("'ftyp' box must occur first"));
+ }
+ }
+
+ let mut meta = None;
+ let mut item_storage = TryVec::new();
+
+ while let Some(mut b) = iter.next_box()? {
+ trace!("read_avif parsing {:?} box", b.head.name);
+ match b.head.name {
+ BoxType::MetadataBox => {
+ if meta.is_some() {
+ return Err(Error::InvalidData(
+ "There should be zero or one meta boxes per ISOBMFF (ISO 14496-12:2020) § 8.11.1.1",
+ ));
+ }
+ meta = Some(read_avif_meta(&mut b, strictness)?);
+ }
+ BoxType::MediaDataBox => {
+ if b.bytes_left() > 0 {
+ let file_offset = b.offset();
+ let data = b.read_into_try_vec()?;
+ item_storage.push(MediaDataBox { file_offset, data })?;
+ }
+ }
+ _ => skip_box_content(&mut b)?,
+ }
+
+ check_parser_state!(b.content);
+ }
+
+ let AvifMeta {
+ item_references,
+ item_properties,
+ primary_item_id,
+ iloc_items,
+ } = meta.ok_or(Error::InvalidData("missing meta"))?;
+
+ let mut alpha_item_ids = item_references
+ .iter()
+ // Auxiliary image for the primary image
+ .filter(|iref| {
+ iref.to_item_id == primary_item_id
+ && iref.from_item_id != primary_item_id
+ && iref.item_type == b"auxl"
+ })
+ .map(|iref| iref.from_item_id)
+ // which has the alpha property
+ .filter(|&item_id| item_properties.is_alpha(item_id));
+ let alpha_item_id = alpha_item_ids.next();
+ if alpha_item_ids.next().is_some() {
+ return Err(Error::InvalidData("multiple alpha planes"));
+ }
+
+ let premultiplied_alpha = alpha_item_id.map_or(false, |alpha_item_id| {
+ item_references.iter().any(|iref| {
+ iref.from_item_id == primary_item_id
+ && iref.to_item_id == alpha_item_id
+ && iref.item_type == b"prem"
+ })
+ });
+
+ let mut primary_item = None;
+ let mut alpha_item = None;
+
+ // store data or record location of relevant items
+ for (item_id, loc) in iloc_items {
+ let item = if item_id == primary_item_id {
+ &mut primary_item
+ } else if Some(item_id) == alpha_item_id {
+ &mut alpha_item
+ } else {
+ continue;
+ };
+
+ if loc.construction_method != ConstructionMethod::File {
+ return Err(Error::Unsupported("unsupported construction_method"));
+ }
+
+ assert!(item.is_none());
+
+ // If our item is spread over multiple extents, we'll need to copy it
+ // into a contiguous buffer. Otherwise, we can just store the extent
+ // and return a pointer into the mdat later to avoid the copy.
+ if loc.extents.len() > 1 {
+ *item = Some(AvifItem::with_inline_data(item_id))
+ }
+
+ for extent in loc.extents {
+ let mut found = false;
+ // try to find an mdat which contains the extent
+ for mdat in item_storage.iter_mut() {
+ if let Some(extent_slice) = mdat.get(&extent) {
+ match item {
+ None => {
+ trace!("Using IsobmffItem::Location");
+ *item = Some(AvifItem::with_data_location(item_id, extent));
+ }
+ Some(AvifItem {
+ image_data: IsobmffItem::Data(item_data),
+ ..
+ }) => {
+ trace!("Using IsobmffItem::Data");
+ // We could potentially optimize memory usage by trying to avoid reading
+ // or storing mdat boxes which aren't used by our API, but for now it seems
+ // like unnecessary complexity
+ item_data.extend_from_slice(extent_slice)?;
+ }
+ _ => unreachable!(),
+ }
+ found = true;
+ break;
+ }
+ }
+
+ if !found {
+ return Err(Error::InvalidData(
+ "iloc contains an extent that is not in any mdat",
+ ));
+ }
+ }
+
+ assert!(item.is_some());
+ }
+
+ let primary_item = primary_item.ok_or(Error::InvalidData(
+ "Missing 'pitm' box, required per HEIF (ISO/IEC 23008-12:2017) § 10.2.1",
+ ))?;
+
+ let has_pixi = |item_id| {
+ item_properties
+ .get(item_id, BoxType::PixelInformationBox)
+ .map_or(false, |opt| opt.is_some())
+ };
+ if !has_pixi(primary_item_id) || !alpha_item_id.map_or(true, has_pixi) {
+ // The requirement to include pixi is in the process of being changed
+ // to allowing its omission to imply a default value. In anticipation
+ // of that, only give an error in strict mode
+ // See https://github.com/MPEGGroup/MIAF/issues/9
+ fail_if(
+ if cfg!(feature = "missing-pixi-permitted") {
+ strictness == ParseStrictness::Strict
+ } else {
+ strictness != ParseStrictness::Permissive
+ },
+ "The pixel information property shall be associated with every image \
+ that is displayable (not hidden) \
+ per MIAF (ISO/IEC 23000-22:2019) specification § 7.3.6.6",
+ )?;
+ }
+
+ let has_av1c = |item_id| {
+ item_properties
+ .get(item_id, BoxType::AV1CodecConfigurationBox)
+ .map_or(false, |opt| opt.is_some())
+ };
+ if !has_av1c(primary_item_id) || !alpha_item_id.map_or(true, has_av1c) {
+ fail_if(
+ strictness != ParseStrictness::Permissive,
+ "One AV1 Item Configuration Property (av1C) is mandatory for an \
+ image item of type 'av01' \
+ per AVIF specification § 2.2.1",
+ )?;
+ }
+
+ if item_properties.get_ispe(primary_item_id)?.is_none() {
+ fail_if(
+ strictness != ParseStrictness::Permissive,
+ "Missing 'ispe' property for primary item, required \
+ per HEIF (ISO/IEC 23008-12:2017) § 6.5.3.1",
+ )?;
+ }
+
+ Ok(AvifContext {
+ strictness,
+ item_storage,
+ primary_item,
+ alpha_item,
+ premultiplied_alpha,
+ item_properties,
+ })
+}
+
+/// Parse a metadata box in the context of an AVIF
+/// Currently requires the primary item to be an av01 item type and generates
+/// an error otherwise.
+/// See ISOBMFF (ISO 14496-12:2020) § 8.11.1
+fn read_avif_meta<T: Read + Offset>(
+ src: &mut BMFFBox<T>,
+ strictness: ParseStrictness,
+) -> Result<AvifMeta> {
+ let version = read_fullbox_version_no_flags(src)?;
+
+ if version != 0 {
+ return Err(Error::Unsupported("unsupported meta version"));
+ }
+
+ let mut read_handler_box = false;
+ let mut primary_item_id = None;
+ let mut item_infos = None;
+ let mut iloc_items = None;
+ let mut item_references = None;
+ let mut item_properties = None;
+
+ let mut iter = src.box_iter();
+ while let Some(mut b) = iter.next_box()? {
+ trace!("read_avif_meta parsing {:?} box", b.head.name);
+
+ if !read_handler_box && b.head.name != BoxType::HandlerBox {
+ fail_if(
+ strictness != ParseStrictness::Permissive,
+ "The HandlerBox shall be the first contained box within the MetaBox \
+ per MIAF (ISO 23000-22:2019) § 7.2.1.5",
+ )?;
+ }
+
+ match b.head.name {
+ BoxType::HandlerBox => {
+ if read_handler_box {
+ return Err(Error::InvalidData(
+ "There shall be exactly one hdlr box per ISOBMFF (ISO 14496-12:2020) § 8.4.3.1",
+ ));
+ }
+ let HandlerBox { handler_type } = read_hdlr(&mut b, strictness)?;
+ if handler_type != b"pict" {
+ fail_if(
+ strictness != ParseStrictness::Permissive,
+ "The HandlerBox handler_type must be 'pict' \
+ per MIAF (ISO 23000-22:2019) § 7.2.1.5",
+ )?;
+ }
+ read_handler_box = true;
+ }
+ BoxType::ItemInfoBox => {
+ if item_infos.is_some() {
+ return Err(Error::InvalidData(
+ "There shall be zero or one iinf boxes per ISOBMFF (ISO 14496-12:2020) § 8.11.6.1",
+ ));
+ }
+ item_infos = Some(read_iinf(&mut b, strictness)?);
+ }
+ BoxType::ItemLocationBox => {
+ if iloc_items.is_some() {
+ return Err(Error::InvalidData(
+ "There shall be zero or one iloc boxes per ISOBMFF (ISO 14496-12:2020) § 8.11.3.1",
+ ));
+ }
+ iloc_items = Some(read_iloc(&mut b)?);
+ }
+ BoxType::PrimaryItemBox => {
+ if primary_item_id.is_some() {
+ return Err(Error::InvalidData(
+ "There shall be zero or one pitm boxes per ISOBMFF (ISO 14496-12:2020) § 8.11.4.1",
+ ));
+ }
+ primary_item_id = Some(read_pitm(&mut b)?);
+ }
+ BoxType::ItemReferenceBox => {
+ if item_references.is_some() {
+ return Err(Error::InvalidData("There shall be zero or one iref boxes per ISOBMFF (ISO 14496-12:2020) § 8.11.12.1"));
+ }
+ item_references = Some(read_iref(&mut b)?);
+ }
+ BoxType::ItemPropertiesBox => {
+ if item_properties.is_some() {
+ return Err(Error::InvalidData("There shall be zero or one iprp boxes per ISOBMFF (ISO 14496-12:2020) § 8.11.14.1"));
+ }
+ item_properties = Some(read_iprp(&mut b, MIF1_BRAND, strictness)?);
+ }
+ _ => skip_box_content(&mut b)?,
+ }
+
+ check_parser_state!(b.content);
+ }
+
+ let primary_item_id = primary_item_id.ok_or(Error::InvalidData(
+ "Required pitm box not present in meta box",
+ ))?;
+
+ let item_infos = item_infos.ok_or(Error::InvalidData("iinf missing"))?;
+
+ if let Some(item_info) = item_infos.iter().find(|x| x.item_id == primary_item_id) {
+ debug!("primary_item_id type: {}", U32BE(item_info.item_type));
+ match &item_info.item_type.to_be_bytes() {
+ b"av01" => {}
+ b"grid" => return Err(Error::from(Status::UnsupportedGrid)),
+ _ => {
+ return Err(Error::InvalidData(
+ "primary_item_id type is neither 'av01' nor 'grid'",
+ ))
+ }
+ }
+ } else {
+ return Err(Error::InvalidData(
+ "primary_item_id not present in iinf box",
+ ));
+ }
+
+ Ok(AvifMeta {
+ item_properties: item_properties.unwrap_or_default(),
+ item_references: item_references.unwrap_or_default(),
+ primary_item_id,
+ iloc_items: iloc_items.ok_or(Error::InvalidData("iloc missing"))?,
+ })
+}
+
+/// Parse a Primary Item Box
+/// See ISOBMFF (ISO 14496-12:2020) § 8.11.4
+fn read_pitm<T: Read>(src: &mut BMFFBox<T>) -> Result<ItemId> {
+ let version = read_fullbox_version_no_flags(src)?;
+
+ let item_id = ItemId(match version {
+ 0 => be_u16(src)?.into(),
+ 1 => be_u32(src)?,
+ _ => return Err(Error::Unsupported("unsupported pitm version")),
+ });
+
+ Ok(item_id)
+}
+
+/// Parse an Item Information Box
+/// See ISOBMFF (ISO 14496-12:2020) § 8.11.6
+fn read_iinf<T: Read>(
+ src: &mut BMFFBox<T>,
+ strictness: ParseStrictness,
+) -> Result<TryVec<ItemInfoEntry>> {
+ let version = read_fullbox_version_no_flags(src)?;
+
+ match version {
+ 0 | 1 => (),
+ _ => return Err(Error::Unsupported("unsupported iinf version")),
+ }
+
+ let entry_count = if version == 0 {
+ be_u16(src)?.to_usize()
+ } else {
+ be_u32(src)?.to_usize()
+ };
+ let mut item_infos = TryVec::with_capacity(entry_count)?;
+
+ let mut iter = src.box_iter();
+ while let Some(mut b) = iter.next_box()? {
+ if b.head.name != BoxType::ItemInfoEntry {
+ return Err(Error::InvalidData(
+ "iinf box shall contain only infe boxes per ISOBMFF (ISO 14496-12:2020) § 8.11.6.2",
+ ));
+ }
+
+ item_infos.push(read_infe(&mut b, strictness)?)?;
+
+ check_parser_state!(b.content);
+ }
+
+ Ok(item_infos)
+}
+
+/// A simple wrapper to interpret a u32 as a 4-byte string in big-endian
+/// order without requiring any allocation.
+struct U32BE(u32);
+
+impl std::fmt::Display for U32BE {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match std::str::from_utf8(&self.0.to_be_bytes()) {
+ Ok(s) => f.write_str(s),
+ Err(_) => write!(f, "{:x?}", self.0),
+ }
+ }
+}
+
+/// Parse an Item Info Entry
+/// See ISOBMFF (ISO 14496-12:2020) § 8.11.6.2
+fn read_infe<T: Read>(src: &mut BMFFBox<T>, strictness: ParseStrictness) -> Result<ItemInfoEntry> {
+ let (version, flags) = read_fullbox_extra(src)?;
+
+ // According to the standard, it seems the flags field shall be 0, but at
+ // least one sample AVIF image has a nonzero value.
+ // See https://github.com/AOMediaCodec/av1-avif/issues/146
+ if flags != 0 {
+ fail_if(
+ strictness == ParseStrictness::Strict,
+ "'infe' flags field shall be 0 \
+ per ISOBMFF (ISO 14496-12:2020) § 8.11.6.2",
+ )?;
+ }
+
+ // mif1 brand (see HEIF (ISO 23008-12:2017) § 10.2.1) only requires v2 and 3
+ let item_id = ItemId(match version {
+ 2 => be_u16(src)?.into(),
+ 3 => be_u32(src)?,
+ _ => return Err(Error::Unsupported("unsupported version in 'infe' box")),
+ });
+
+ let item_protection_index = be_u16(src)?;
+
+ if item_protection_index != 0 {
+ return Err(Error::from(Status::UnsupportedIpro));
+ }
+
+ let item_type = be_u32(src)?;
+ debug!("infe {:?} item_type: {}", item_id, U32BE(item_type));
+
+ // There are some additional fields here, but they're not of interest to us
+ skip_box_remain(src)?;
+
+ Ok(ItemInfoEntry { item_id, item_type })
+}
+
+/// Parse an Item Reference Box
+/// See ISOBMFF (ISO 14496-12:2020) § 8.11.12
+fn read_iref<T: Read>(src: &mut BMFFBox<T>) -> Result<TryVec<SingleItemTypeReferenceBox>> {
+ let mut item_references = TryVec::new();
+ let version = read_fullbox_version_no_flags(src)?;
+ if version > 1 {
+ return Err(Error::Unsupported("iref version"));
+ }
+
+ let mut iter = src.box_iter();
+ while let Some(mut b) = iter.next_box()? {
+ trace!("read_iref parsing {:?} referenceType", b.head.name);
+ let from_item_id = ItemId::read(&mut b, version)?;
+ let reference_count = be_u16(&mut b)?;
+ item_references.reserve(reference_count.to_usize())?;
+ for _ in 0..reference_count {
+ let to_item_id = ItemId::read(&mut b, version)?;
+ if from_item_id == to_item_id {
+ return Err(Error::InvalidData(
+ "from_item_id and to_item_id must be different",
+ ));
+ }
+ item_references.push(SingleItemTypeReferenceBox {
+ item_type: b.head.name.into(),
+ from_item_id,
+ to_item_id,
+ })?;
+ }
+ check_parser_state!(b.content);
+ }
+ Ok(item_references)
+}
+
+/// Parse an Item Properties Box
+///
+/// See ISOBMFF (ISO 14496-12:2020 § 8.11.14)
+///
+/// Note: HEIF (ISO 23008-12:2017) § 9.3.1 also defines the `iprp` box and
+/// related types, but lacks additional requirements specified in 14496-12:2020.
+///
+/// Note: Currently HEIF (ISO 23008-12:2017) § 6.5.5.1 specifies "At most one"
+/// `colr` box per item, but this is being amended in [DIS 23008-12](https://www.iso.org/standard/83650.html).
+/// The new text is likely to be "At most one for a given value of `colour_type`",
+/// so this implementation adheres to that language for forward compatibility.
+fn read_iprp<T: Read>(
+ src: &mut BMFFBox<T>,
+ brand: FourCC,
+ strictness: ParseStrictness,
+) -> Result<ItemPropertiesBox> {
+ let mut iter = src.box_iter();
+
+ let properties = match iter.next_box()? {
+ Some(mut b) if b.head.name == BoxType::ItemPropertyContainerBox => {
+ read_ipco(&mut b, strictness)
+ }
+ Some(_) => Err(Error::InvalidData("unexpected iprp child")),
+ None => Err(Error::UnexpectedEOF),
+ }?;
+
+ let mut ipma_version_and_flag_values_seen = TryVec::with_capacity(1)?;
+ let mut association_entries = TryVec::<ItemPropertyAssociationEntry>::new();
+
+ while let Some(mut b) = iter.next_box()? {
+ if b.head.name != BoxType::ItemPropertyAssociationBox {
+ return Err(Error::InvalidData("unexpected iprp child"));
+ }
+
+ let (version, flags) = read_fullbox_extra(&mut b)?;
+ if ipma_version_and_flag_values_seen.contains(&(version, flags)) {
+ fail_if(
+ strictness != ParseStrictness::Permissive,
+ "There shall be at most one ItemPropertyAssociationbox with a given pair of \
+ values of version and flags \
+ per ISOBMFF (ISO 14496-12:2020 § 8.11.14.1",
+ )?;
+ }
+ if flags != 0 && properties.len() <= 127 {
+ fail_if(
+ strictness == ParseStrictness::Strict,
+ "Unless there are more than 127 properties in the ItemPropertyContainerBox, \
+ flags should be equal to 0 \
+ per ISOBMFF (ISO 14496-12:2020 § 8.11.14.1",
+ )?;
+ }
+ ipma_version_and_flag_values_seen.push((version, flags))?;
+ for association_entry in read_ipma(&mut b, strictness, version, flags)? {
+ if let Some(previous_entry) = association_entries
+ .iter()
+ .find(|e| association_entry.item_id == e.item_id)
+ {
+ error!(
+ "Duplicate ipma entries for item_id\n1: {:?}\n2: {:?}",
+ previous_entry, association_entry
+ );
+ // It's technically possible to make sense of this situation by merging ipma
+ // boxes, but this is a "shall" requirement, so we'd only do it in
+ // ParseStrictness::Permissive mode, and this hasn't shown up in the wild
+ return Err(Error::InvalidData(
+ "There shall be at most one occurrence of a given item_ID, \
+ in the set of ItemPropertyAssociationBox boxes \
+ per ISOBMFF (ISO 14496-12:2020) § 8.11.14.1",
+ ));
+ }
+
+ const TRANSFORM_ORDER_ERROR: &str =
+ "These properties, if used, shall be indicated to be applied \
+ in the following order: clean aperture first, then rotation, \
+ then mirror. \
+ per MIAF (ISO/IEC 23000-22:2019) § 7.3.6.7";
+ const TRANSFORM_ORDER: &[BoxType] = &[
+ BoxType::CleanApertureBox,
+ BoxType::ImageRotation,
+ BoxType::ImageMirror,
+ ];
+ let mut prev_transform_index = None;
+ // Realistically, there should only ever be 1 nclx and 1 icc
+ let mut colour_type_indexes: TryHashMap<FourCC, PropertyIndex> =
+ TryHashMap::with_capacity(2)?;
+
+ for a in &association_entry.associations {
+ if a.property_index == PropertyIndex(0) {
+ if a.essential {
+ fail_if(
+ strictness != ParseStrictness::Permissive,
+ "the essential indicator shall be 0 for property index 0 \
+ per ISOBMFF (ISO 14496-12:2020 § 8.11.14.3",
+ )?;
+ }
+ continue;
+ }
+
+ if let Some(property) = properties.get(&a.property_index) {
+ assert!(brand == MIF1_BRAND);
+ match property {
+ ItemProperty::AV1Config(_)
+ | ItemProperty::Mirroring(_)
+ | ItemProperty::Rotation(_) => {
+ if !a.essential {
+ warn!("{:?} is invalid", property);
+ // This is a "shall", but it is likely to change, so only
+ // fail if using strict parsing.
+ // See https://github.com/mozilla/mp4parse-rust/issues/284
+ fail_if(
+ strictness == ParseStrictness::Strict,
+ "All transformative properties associated with coded and \
+ derived images required or conditionally required by this \
+ document shall be marked as essential \
+ per MIAF (ISO 23000-22:2019) § 7.3.9",
+ )?;
+ }
+ }
+ // XXX this is contrary to the published specification; see doc comment
+ // at the beginning of this function for more details
+ ItemProperty::Colour(colr) => {
+ let colour_type = colr.colour_type();
+ if let Some(prev_colr_index) = colour_type_indexes.get(&colour_type) {
+ warn!(
+ "Multiple '{}' type colr associations with {:?}: {:?} and {:?}",
+ colour_type,
+ association_entry.item_id,
+ a.property_index,
+ prev_colr_index
+ );
+ fail_if(
+ strictness != ParseStrictness::Permissive,
+ "Each item shall have at most one property association with a
+ ColourInformationBox (colr) for a given value of colour_type \
+ per HEIF (ISO/IEC DIS 23008-12) § 6.5.5.1",
+ )?;
+ } else {
+ colour_type_indexes.insert(colour_type, a.property_index)?;
+ }
+ }
+ _ => {}
+ }
+
+ if let Some(transform_index) = TRANSFORM_ORDER
+ .iter()
+ .position(|t| *t == property.box_type())
+ {
+ if let Some(prev) = prev_transform_index {
+ if prev >= transform_index {
+ error!(
+ "{:?} after {:?}",
+ TRANSFORM_ORDER[transform_index], TRANSFORM_ORDER[prev]
+ );
+ return Err(Error::InvalidData(TRANSFORM_ORDER_ERROR));
+ }
+ }
+ prev_transform_index = Some(transform_index);
+ }
+ } else {
+ error!(
+ "Missing property at {:?} for {:?}",
+ a.property_index, association_entry.item_id
+ );
+ fail_if(
+ strictness != ParseStrictness::Permissive,
+ "Invalid property index in ipma",
+ )?;
+ }
+ }
+ association_entries.push(association_entry)?
+ }
+
+ check_parser_state!(b.content);
+ }
+
+ let iprp = ItemPropertiesBox {
+ properties,
+ association_entries,
+ };
+ trace!("read_iprp -> {:#?}", iprp);
+ Ok(iprp)
+}
+
+/// See ISOBMFF (ISO 14496-12:2020 § 8.11.14.1
+#[derive(Debug)]
+pub enum ItemProperty {
+ AuxiliaryType(AuxiliaryTypeProperty),
+ AV1Config(AV1ConfigBox),
+ Channels(PixelInformation),
+ Colour(ColourInformation),
+ ImageSpatialExtents(ImageSpatialExtentsProperty),
+ Mirroring(ImageMirror),
+ Rotation(ImageRotation),
+ /// Necessary to validate property indices in read_iprp
+ Unsupported(BoxType),
+}
+
+impl ItemProperty {
+ fn box_type(&self) -> BoxType {
+ match self {
+ ItemProperty::AuxiliaryType(_) => BoxType::AuxiliaryTypeProperty,
+ ItemProperty::AV1Config(_) => BoxType::AV1CodecConfigurationBox,
+ ItemProperty::Colour(_) => BoxType::ColourInformationBox,
+ ItemProperty::Mirroring(_) => BoxType::ImageMirror,
+ ItemProperty::Rotation(_) => BoxType::ImageRotation,
+ ItemProperty::ImageSpatialExtents(_) => BoxType::ImageSpatialExtentsProperty,
+ ItemProperty::Channels(_) => BoxType::PixelInformationBox,
+ ItemProperty::Unsupported(box_type) => *box_type,
+ }
+ }
+}
+
+#[derive(Debug)]
+struct ItemPropertyAssociationEntry {
+ item_id: ItemId,
+ associations: TryVec<Association>,
+}
+
+/// For storing ItemPropertyAssociation data
+/// See ISOBMFF (ISO 14496-12:2020 § 8.11.14.1
+#[derive(Debug)]
+struct Association {
+ essential: bool,
+ property_index: PropertyIndex,
+}
+
+/// See ISOBMFF (ISO 14496-12:2020 § 8.11.14.1
+///
+/// The properties themselves are stored in `properties`, but the items they're
+/// associated with are stored in `association_entries`. It's necessary to
+/// maintain this indirection because multiple items can reference the same
+/// property. For example, both the primary item and alpha item can share the
+/// same [`ImageSpatialExtentsProperty`].
+#[derive(Debug, Default)]
+pub struct ItemPropertiesBox {
+ /// `ItemPropertyContainerBox property_container` in the spec
+ properties: TryHashMap<PropertyIndex, ItemProperty>,
+ /// `ItemPropertyAssociationBox association[]` in the spec
+ association_entries: TryVec<ItemPropertyAssociationEntry>,
+}
+
+impl ItemPropertiesBox {
+ /// For displayable images `av1C`, `pixi` and `ispe` are mandatory, `colr`
+ /// is typically included too, so we might as well use an even power of 2.
+ const MIN_PROPERTIES: usize = 4;
+
+ fn is_alpha(&self, item_id: ItemId) -> bool {
+ match self.get(item_id, BoxType::AuxiliaryTypeProperty) {
+ Ok(Some(ItemProperty::AuxiliaryType(urn))) => {
+ urn.aux_type.as_slice() == "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha".as_bytes()
+ }
+ Ok(Some(other_property)) => panic!("property key mismatch: {:?}", other_property),
+ Ok(None) => false,
+ Err(e) => {
+ error!(
+ "is_alpha: Error checking AuxiliaryTypeProperty ({}), returning false",
+ e
+ );
+ false
+ }
+ }
+ }
+
+ fn get_ispe(&self, item_id: ItemId) -> Result<Option<&ImageSpatialExtentsProperty>> {
+ if let Some(ItemProperty::ImageSpatialExtents(ispe)) =
+ self.get(item_id, BoxType::ImageSpatialExtentsProperty)?
+ {
+ Ok(Some(ispe))
+ } else {
+ Ok(None)
+ }
+ }
+
+ fn get(&self, item_id: ItemId, property_type: BoxType) -> Result<Option<&ItemProperty>> {
+ match self
+ .get_multiple(item_id, |prop| prop.box_type() == property_type)?
+ .as_slice()
+ {
+ &[] => Ok(None),
+ &[single_value] => Ok(Some(single_value)),
+ multiple_values => {
+ error!(
+ "Multiple values for {:?}: {:?}",
+ property_type, multiple_values
+ );
+ // TODO: add test
+ Err(Error::InvalidData("conflicting item property values"))
+ }
+ }
+ }
+
+ fn get_multiple(
+ &self,
+ item_id: ItemId,
+ filter: impl Fn(&ItemProperty) -> bool,
+ ) -> Result<TryVec<&ItemProperty>> {
+ let mut values = TryVec::new();
+ for entry in &self.association_entries {
+ for a in &entry.associations {
+ if entry.item_id == item_id {
+ match self.properties.get(&a.property_index) {
+ Some(ItemProperty::Unsupported(_)) => {}
+ Some(property) if filter(property) => values.push(property)?,
+ _ => {}
+ }
+ }
+ }
+ }
+
+ Ok(values)
+ }
+}
+
+/// An upper bound which can be used to check overflow at compile time
+trait UpperBounded {
+ const MAX: u64;
+}
+
+/// Implement type $name as a newtype wrapper around an unsigned int which
+/// implements the UpperBounded trait.
+macro_rules! impl_bounded {
+ ( $name:ident, $inner:ty ) => {
+ #[derive(Clone, Copy)]
+ pub struct $name($inner);
+
+ impl $name {
+ pub const fn new(n: $inner) -> Self {
+ Self(n)
+ }
+
+ #[allow(dead_code)]
+ pub fn get(self) -> $inner {
+ self.0
+ }
+ }
+
+ impl UpperBounded for $name {
+ const MAX: u64 = <$inner>::MAX as u64;
+ }
+ };
+}
+
+/// Implement type $name as a type representing the product of two unsigned ints
+/// which implements the UpperBounded trait.
+macro_rules! impl_bounded_product {
+ ( $name:ident, $multiplier:ty, $multiplicand:ty, $inner:ty) => {
+ #[derive(Clone, Copy)]
+ pub struct $name($inner);
+
+ impl $name {
+ pub fn new(value: $inner) -> Self {
+ assert!(value <= Self::MAX);
+ Self(value)
+ }
+
+ pub fn get(self) -> $inner {
+ self.0
+ }
+ }
+
+ impl UpperBounded for $name {
+ const MAX: u64 = <$multiplier>::MAX * <$multiplicand>::MAX;
+ }
+ };
+}
+
+mod bounded_uints {
+ use UpperBounded;
+
+ impl_bounded!(U8, u8);
+ impl_bounded!(U16, u16);
+ impl_bounded!(U32, u32);
+ impl_bounded!(U64, u64);
+
+ impl_bounded_product!(U32MulU8, U32, U8, u64);
+ impl_bounded_product!(U32MulU16, U32, U16, u64);
+
+ impl UpperBounded for std::num::NonZeroU8 {
+ const MAX: u64 = u8::MAX as u64;
+ }
+}
+
+use bounded_uints::*;
+
+/// Implement the multiplication operator for $lhs * $rhs giving $output, which
+/// is internally represented as $inner. The operation is statically checked
+/// to ensure the product won't overflow $inner, nor exceed <$output>::MAX.
+macro_rules! impl_mul {
+ ( ($lhs:ty , $rhs:ty) => ($output:ty, $inner:ty) ) => {
+ impl std::ops::Mul<$rhs> for $lhs {
+ type Output = $output;
+
+ fn mul(self, rhs: $rhs) -> Self::Output {
+ static_assertions::const_assert!(<$output>::MAX <= <$inner>::MAX as u64);
+ static_assertions::const_assert!(<$lhs>::MAX * <$rhs>::MAX <= <$output>::MAX);
+
+ let lhs: $inner = self.get().into();
+ let rhs: $inner = rhs.get().into();
+ Self::Output::new(lhs.checked_mul(rhs).expect("infallible"))
+ }
+ }
+ };
+}
+
+impl_mul!((U8, std::num::NonZeroU8) => (U16, u16));
+impl_mul!((U32, std::num::NonZeroU8) => (U32MulU8, u64));
+impl_mul!((U32, U16) => (U32MulU16, u64));
+
+impl std::ops::Add<U32MulU16> for U32MulU8 {
+ type Output = U64;
+
+ fn add(self, rhs: U32MulU16) -> Self::Output {
+ static_assertions::const_assert!(U32MulU8::MAX + U32MulU16::MAX < U64::MAX);
+ let lhs: u64 = self.get();
+ let rhs: u64 = rhs.get();
+ Self::Output::new(lhs.checked_add(rhs).expect("infallible"))
+ }
+}
+
+const MAX_IPMA_ASSOCIATION_COUNT: U8 = U8::new(u8::MAX);
+
+/// After reading only the `entry_count` field of an ipma box, we can check its
+/// basic validity and calculate (assuming validity) the number of associations
+/// which will be contained (allowing preallocation of the storage).
+/// All the arithmetic is compile-time verified to not overflow via supporting
+/// types implementing the UpperBounded trait. Types are declared explicitly to
+/// show there isn't any accidental inference to primitive types.
+///
+/// See ISOBMFF (ISO 14496-12:2020 § 8.11.14.1
+fn calculate_ipma_total_associations(
+ version: u8,
+ bytes_left: u64,
+ entry_count: U32,
+ num_association_bytes: std::num::NonZeroU8,
+) -> Result<usize> {
+ let min_entry_bytes =
+ std::num::NonZeroU8::new(1 /* association_count */ + if version == 0 { 2 } else { 4 })
+ .unwrap();
+
+ let total_non_association_bytes: U32MulU8 = entry_count * min_entry_bytes;
+ let total_association_bytes: u64;
+
+ if let Some(difference) = bytes_left.checked_sub(total_non_association_bytes.get()) {
+ // All the storage for the `essential` and `property_index` parts (assuming a valid ipma box size)
+ total_association_bytes = difference;
+ } else {
+ return Err(Error::InvalidData(
+ "ipma box below minimum size for entry_count",
+ ));
+ }
+
+ let max_association_bytes_per_entry: U16 = MAX_IPMA_ASSOCIATION_COUNT * num_association_bytes;
+ let max_total_association_bytes: U32MulU16 = entry_count * max_association_bytes_per_entry;
+ let max_bytes_left: U64 = total_non_association_bytes + max_total_association_bytes;
+
+ if bytes_left > max_bytes_left.get() {
+ return Err(Error::InvalidData(
+ "ipma box exceeds maximum size for entry_count",
+ ));
+ }
+
+ let total_associations: u64 = total_association_bytes / u64::from(num_association_bytes.get());
+
+ Ok(total_associations.try_into()?)
+}
+
+/// Parse an ItemPropertyAssociation box
+///
+/// See ISOBMFF (ISO 14496-12:2020 § 8.11.14.1
+fn read_ipma<T: Read>(
+ src: &mut BMFFBox<T>,
+ strictness: ParseStrictness,
+ version: u8,
+ flags: u32,
+) -> Result<TryVec<ItemPropertyAssociationEntry>> {
+ let entry_count = be_u32(src)?;
+ let num_association_bytes =
+ std::num::NonZeroU8::new(if flags & 1 == 1 { 2 } else { 1 }).unwrap();
+
+ let total_associations = calculate_ipma_total_associations(
+ version,
+ src.bytes_left(),
+ U32::new(entry_count),
+ num_association_bytes,
+ )?;
+ // Assuming most items will have at least `MIN_PROPERTIES` and knowing the
+ // total number of item -> property associations (`total_associations`),
+ // we can provide a good estimate for how many elements we'll need in this
+ // vector, even though we don't know precisely how many items there will be
+ // properties for.
+ let mut entries = TryVec::<ItemPropertyAssociationEntry>::with_capacity(
+ total_associations / ItemPropertiesBox::MIN_PROPERTIES,
+ )?;
+
+ for _ in 0..entry_count {
+ let item_id = ItemId::read(src, version)?;
+
+ if let Some(previous_association) = entries.last() {
+ #[allow(clippy::comparison_chain)]
+ if previous_association.item_id > item_id {
+ return Err(Error::InvalidData(
+ "Each ItemPropertyAssociation box shall be ordered by increasing item_ID",
+ ));
+ } else if previous_association.item_id == item_id {
+ return Err(Error::InvalidData("There shall be at most one association box for each item_ID, in any ItemPropertyAssociation box"));
+ }
+ }
+
+ let association_count = src.read_u8()?;
+ let mut associations = TryVec::with_capacity(association_count.to_usize())?;
+ for _ in 0..association_count {
+ let association = src
+ .take(num_association_bytes.get().into())
+ .read_into_try_vec()?;
+ let mut association = BitReader::new(association.as_slice());
+ let essential = association.read_bool()?;
+ let property_index =
+ PropertyIndex(association.read_u16(association.remaining().try_into()?)?);
+ associations.push(Association {
+ essential,
+ property_index,
+ })?;
+ }
+
+ entries.push(ItemPropertyAssociationEntry {
+ item_id,
+ associations,
+ })?;
+ }
+
+ check_parser_state!(src.content);
+
+ if version != 0 {
+ if let Some(ItemPropertyAssociationEntry {
+ item_id: max_item_id,
+ ..
+ }) = entries.last()
+ {
+ if *max_item_id <= ItemId(u16::MAX.into()) {
+ fail_if(
+ strictness == ParseStrictness::Strict,
+ "The ipma version 0 should be used unless 32-bit item_ID values are needed \
+ per ISOBMFF (ISO 14496-12:2020 § 8.11.14.1",
+ )?;
+ }
+ }
+ }
+
+ trace!("read_ipma -> {:#?}", entries);
+
+ Ok(entries)
+}
+
+/// Parse an ItemPropertyContainerBox
+///
+/// For unsupported properties that we know about, return specific
+/// [`Status`] UnsupportedXXXX variants. Unless running in
+/// [`ParseStrictness::Permissive`] mode, in which case, unsupported properties
+/// will be ignored.
+///
+/// See ISOBMFF (ISO 14496-12:2020 § 8.11.14.1
+fn read_ipco<T: Read>(
+ src: &mut BMFFBox<T>,
+ strictness: ParseStrictness,
+) -> Result<TryHashMap<PropertyIndex, ItemProperty>> {
+ let mut properties = TryHashMap::with_capacity(ItemPropertiesBox::MIN_PROPERTIES)?;
+
+ let mut index = PropertyIndex(1); // ipma uses 1-based indexing
+ let mut iter = src.box_iter();
+ while let Some(mut b) = iter.next_box()? {
+ if let Some(property) = match b.head.name {
+ BoxType::AuxiliaryTypeProperty => Some(ItemProperty::AuxiliaryType(read_auxc(&mut b)?)),
+ BoxType::AV1CodecConfigurationBox => Some(ItemProperty::AV1Config(read_av1c(&mut b)?)),
+ BoxType::AV1LayeredImageIndexingProperty
+ if strictness != ParseStrictness::Permissive =>
+ {
+ return Err(Error::from(Status::UnsupportedA1lx))
+ }
+ BoxType::CleanApertureBox if strictness != ParseStrictness::Permissive => {
+ return Err(Error::from(Status::UnsupportedClap))
+ }
+ BoxType::ColourInformationBox => {
+ Some(ItemProperty::Colour(read_colr(&mut b, strictness)?))
+ }
+ BoxType::ImageMirror => Some(ItemProperty::Mirroring(read_imir(&mut b)?)),
+ BoxType::ImageRotation => Some(ItemProperty::Rotation(read_irot(&mut b)?)),
+ BoxType::ImageSpatialExtentsProperty => {
+ Some(ItemProperty::ImageSpatialExtents(read_ispe(&mut b)?))
+ }
+ BoxType::LayerSelectorProperty if strictness != ParseStrictness::Permissive => {
+ return Err(Error::from(Status::UnsupportedLsel))
+ }
+ BoxType::OperatingPointSelectorProperty
+ if strictness != ParseStrictness::Permissive =>
+ {
+ return Err(Error::from(Status::UnsupportedA1op))
+ }
+ BoxType::PixelInformationBox => Some(ItemProperty::Channels(read_pixi(&mut b)?)),
+ other_box_type => {
+ // Though we don't do anything with other property types, we still store
+ // a record at the index to identify invalid indices in ipma boxes
+ skip_box_remain(&mut b)?;
+ let item_property = ItemProperty::Unsupported(other_box_type);
+ debug!("Storing empty record {:?}", item_property);
+ Some(item_property)
+ }
+ } {
+ properties.insert(index, property)?;
+ }
+
+ index = PropertyIndex(
+ index
+ .0
+ .checked_add(1) // must include ignored properties to have correct indexes
+ .ok_or(Error::InvalidData("ipco index overflow"))?,
+ );
+
+ check_parser_state!(b.content);
+ }
+
+ Ok(properties)
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct ImageSpatialExtentsProperty {
+ image_width: u32,
+ image_height: u32,
+}
+
+/// Parse image spatial extents property
+///
+/// See HEIF (ISO 23008-12:2017) § 6.5.3.1
+fn read_ispe<T: Read>(src: &mut BMFFBox<T>) -> Result<ImageSpatialExtentsProperty> {
+ if read_fullbox_version_no_flags(src)? != 0 {
+ return Err(Error::Unsupported("ispe version"));
+ }
+
+ let image_width = be_u32(src)?;
+ let image_height = be_u32(src)?;
+
+ Ok(ImageSpatialExtentsProperty {
+ image_width,
+ image_height,
+ })
+}
+
+#[derive(Debug)]
+pub struct PixelInformation {
+ bits_per_channel: TryVec<u8>,
+}
+
+/// Parse pixel information
+/// See HEIF (ISO 23008-12:2017) § 6.5.6
+fn read_pixi<T: Read>(src: &mut BMFFBox<T>) -> Result<PixelInformation> {
+ let version = read_fullbox_version_no_flags(src)?;
+ if version != 0 {
+ return Err(Error::Unsupported("pixi version"));
+ }
+
+ let num_channels = src.read_u8()?;
+ let mut bits_per_channel = TryVec::with_capacity(num_channels.to_usize())?;
+ let num_channels_read = src.try_read_to_end(&mut bits_per_channel)?;
+
+ if u8::try_from(num_channels_read)? != num_channels {
+ return Err(Error::InvalidData("invalid num_channels"));
+ }
+
+ check_parser_state!(src.content);
+ Ok(PixelInformation { bits_per_channel })
+}
+
+/// Despite [Rec. ITU-T H.273] (12/2016) defining the CICP fields as having a
+/// range of 0-255, and only a small fraction of those values being used,
+/// ISOBMFF (ISO 14496-12:2020) § 12.1.5 defines them as 16-bit values in the
+/// `colr` box. Since we have no use for the additional range, and it would
+/// complicate matters later, we fallibly convert before storing the input.
+///
+/// [Rec. ITU-T H.273]: https://www.itu.int/rec/T-REC-H.273-201612-I/en
+#[repr(C)]
+#[derive(Debug)]
+pub struct NclxColourInformation {
+ colour_primaries: u8,
+ transfer_characteristics: u8,
+ matrix_coefficients: u8,
+ full_range_flag: bool,
+}
+
+/// The raw bytes of the ICC profile
+#[repr(C)]
+pub struct IccColourInformation {
+ bytes: TryVec<u8>,
+}
+
+impl fmt::Debug for IccColourInformation {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct("IccColourInformation")
+ .field("data", &format_args!("{} bytes", self.bytes.len()))
+ .finish()
+ }
+}
+
+#[repr(C)]
+#[derive(Debug)]
+pub enum ColourInformation {
+ Nclx(NclxColourInformation),
+ Icc(IccColourInformation, FourCC),
+}
+
+impl ColourInformation {
+ fn colour_type(&self) -> FourCC {
+ match self {
+ Self::Nclx(_) => FourCC::from(*b"nclx"),
+ Self::Icc(_, colour_type) => colour_type.clone(),
+ }
+ }
+}
+
+/// Parse colour information
+/// See ISOBMFF (ISO 14496-12:2020) § 12.1.5
+fn read_colr<T: Read>(
+ src: &mut BMFFBox<T>,
+ strictness: ParseStrictness,
+) -> Result<ColourInformation> {
+ let colour_type = be_u32(src)?.to_be_bytes();
+
+ match &colour_type {
+ b"nclx" => {
+ const NUM_RESERVED_BITS: u8 = 7;
+ let colour_primaries = be_u16(src)?.try_into()?;
+ let transfer_characteristics = be_u16(src)?.try_into()?;
+ let matrix_coefficients = be_u16(src)?.try_into()?;
+ let bytes = src.read_into_try_vec()?;
+ let mut bit_reader = BitReader::new(&bytes);
+ let full_range_flag = bit_reader.read_bool()?;
+ if bit_reader.remaining() != NUM_RESERVED_BITS.into() {
+ error!(
+ "read_colr expected {} reserved bits, found {}",
+ NUM_RESERVED_BITS,
+ bit_reader.remaining()
+ );
+ return Err(Error::InvalidData("Unexpected size for colr box"));
+ }
+ if bit_reader.read_u8(NUM_RESERVED_BITS)? != 0 {
+ fail_if(
+ strictness != ParseStrictness::Permissive,
+ "The 7 reserved bits at the end of the ColourInformationBox \
+ for colour_type == 'nclx' must be 0 \
+ per ISOBMFF (ISO 14496-12:2020) § 12.1.5.2",
+ )?;
+ }
+
+ Ok(ColourInformation::Nclx(NclxColourInformation {
+ colour_primaries,
+ transfer_characteristics,
+ matrix_coefficients,
+ full_range_flag,
+ }))
+ }
+ b"rICC" | b"prof" => Ok(ColourInformation::Icc(
+ IccColourInformation {
+ bytes: src.read_into_try_vec()?,
+ },
+ FourCC::from(colour_type),
+ )),
+ _ => {
+ error!("read_colr colour_type: {:?}", colour_type);
+ Err(Error::InvalidData(
+ "Unsupported colour_type for ColourInformationBox",
+ ))
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy, Debug)]
+/// Rotation in the positive (that is, anticlockwise) direction
+/// Visualized in terms of starting with (⥠) UPWARDS HARPOON WITH BARB LEFT FROM BAR
+/// similar to a DIGIT ONE (1)
+pub enum ImageRotation {
+ /// ⥠ UPWARDS HARPOON WITH BARB LEFT FROM BAR
+ D0,
+ /// ⥞ LEFTWARDS HARPOON WITH BARB DOWN FROM BAR
+ D90,
+ /// ⥝ DOWNWARDS HARPOON WITH BARB RIGHT FROM BAR
+ D180,
+ /// ⥛ RIGHTWARDS HARPOON WITH BARB UP FROM BAR
+ D270,
+}
+
+/// Parse image rotation box
+/// See HEIF (ISO 23008-12:2017) § 6.5.10
+fn read_irot<T: Read>(src: &mut BMFFBox<T>) -> Result<ImageRotation> {
+ let irot = src.read_into_try_vec()?;
+ let mut irot = BitReader::new(&irot);
+ let _reserved = irot.read_u8(6)?;
+ let image_rotation = match irot.read_u8(2)? {
+ 0 => ImageRotation::D0,
+ 1 => ImageRotation::D90,
+ 2 => ImageRotation::D180,
+ 3 => ImageRotation::D270,
+ _ => unreachable!(),
+ };
+
+ check_parser_state!(src.content);
+
+ Ok(image_rotation)
+}
+
+/// The axis about which the image is mirrored (opposite of flip)
+/// Visualized in terms of starting with (⥠) UPWARDS HARPOON WITH BARB LEFT FROM BAR
+/// similar to a DIGIT ONE (1)
+#[repr(C)]
+#[derive(Debug)]
+pub enum ImageMirror {
+ /// top and bottom parts exchanged
+ /// ⥡ DOWNWARDS HARPOON WITH BARB LEFT FROM BAR
+ TopBottom,
+ /// left and right parts exchanged
+ /// ⥜ UPWARDS HARPOON WITH BARB RIGHT FROM BAR
+ LeftRight,
+}
+
+/// Parse image mirroring box
+/// See HEIF (ISO 23008-12:2017) § 6.5.12<br />
+/// Note: [ISO/IEC 23008-12:2017/DAmd 2](https://www.iso.org/standard/81688.html)
+/// reverses the interpretation of the 'imir' box in § 6.5.12.3:
+/// > `axis` specifies a vertical (`axis` = 0) or horizontal (`axis` = 1) axis
+/// > for the mirroring operation.
+///
+/// is replaced with:
+/// > `mode` specifies how the mirroring is performed: 0 indicates that the top
+/// > and bottom parts of the image are exchanged; 1 specifies that the left and
+/// > right parts are exchanged.
+/// >
+/// > NOTE: In Exif, orientation tag can be used to signal mirroring operations.
+/// > Exif orientation tag 4 corresponds to `mode` = 0 of `ImageMirror`, and
+/// > Exif orientation tag 2 corresponds to `mode` = 1 accordingly.
+///
+/// This implementation conforms to the text in Draft Amendment 2, which is the
+/// opposite of the published standard as of 4 June 2021.
+fn read_imir<T: Read>(src: &mut BMFFBox<T>) -> Result<ImageMirror> {
+ let imir = src.read_into_try_vec()?;
+ let mut imir = BitReader::new(&imir);
+ let _reserved = imir.read_u8(7)?;
+ let image_mirror = match imir.read_u8(1)? {
+ 0 => ImageMirror::TopBottom,
+ 1 => ImageMirror::LeftRight,
+ _ => unreachable!(),
+ };
+
+ check_parser_state!(src.content);
+
+ Ok(image_mirror)
+}
+
+/// See HEIF (ISO 23008-12:2017) § 6.5.8
+#[derive(Debug, PartialEq)]
+pub struct AuxiliaryTypeProperty {
+ aux_type: TryString,
+ aux_subtype: TryString,
+}
+
+/// Parse image properties for auxiliary images
+/// See HEIF (ISO 23008-12:2017) § 6.5.8
+fn read_auxc<T: Read>(src: &mut BMFFBox<T>) -> Result<AuxiliaryTypeProperty> {
+ let version = read_fullbox_version_no_flags(src)?;
+ if version != 0 {
+ return Err(Error::Unsupported("auxC version"));
+ }
+
+ let mut aux = TryString::new();
+ src.try_read_to_end(&mut aux)?;
+
+ let (aux_type, aux_subtype): (TryString, TryVec<u8>);
+ if let Some(nul_byte_pos) = aux.iter().position(|&b| b == b'\0') {
+ let (a, b) = aux.as_slice().split_at(nul_byte_pos);
+ aux_type = a.try_into()?;
+ aux_subtype = (&b[1..]).try_into()?;
+ } else {
+ aux_type = aux;
+ aux_subtype = TryVec::new();
+ }
+
+ Ok(AuxiliaryTypeProperty {
+ aux_type,
+ aux_subtype,
+ })
+}
+
+/// Parse an item location box inside a meta box
+/// See ISOBMFF (ISO 14496-12:2020) § 8.11.3
+fn read_iloc<T: Read>(src: &mut BMFFBox<T>) -> Result<TryHashMap<ItemId, ItemLocationBoxItem>> {
+ let version: IlocVersion = read_fullbox_version_no_flags(src)?.try_into()?;
+
+ let iloc = src.read_into_try_vec()?;
+ let mut iloc = BitReader::new(&iloc);
+
+ let offset_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?;
+ let length_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?;
+ let base_offset_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?;
+
+ let index_size: Option<IlocFieldSize> = match version {
+ IlocVersion::One | IlocVersion::Two => Some(iloc.read_u8(4)?.try_into()?),
+ IlocVersion::Zero => {
+ let _reserved = iloc.read_u8(4)?;
+ None
+ }
+ };
+
+ let item_count = match version {
+ IlocVersion::Zero | IlocVersion::One => iloc.read_u32(16)?,
+ IlocVersion::Two => iloc.read_u32(32)?,
+ };
+
+ let mut items = TryHashMap::with_capacity(item_count.to_usize())?;
+
+ for _ in 0..item_count {
+ let item_id = ItemId(match version {
+ IlocVersion::Zero | IlocVersion::One => iloc.read_u32(16)?,
+ IlocVersion::Two => iloc.read_u32(32)?,
+ });
+
+ // The spec isn't entirely clear how an `iloc` should be interpreted for version 0,
+ // which has no `construction_method` field. It does say:
+ // "For maximum compatibility, version 0 of this box should be used in preference to
+ // version 1 with `construction_method==0`, or version 2 when possible."
+ // We take this to imply version 0 can be interpreted as using file offsets.
+ let construction_method = match version {
+ IlocVersion::Zero => ConstructionMethod::File,
+ IlocVersion::One | IlocVersion::Two => {
+ let _reserved = iloc.read_u16(12)?;
+ match iloc.read_u16(4)? {
+ 0 => ConstructionMethod::File,
+ 1 => ConstructionMethod::Idat,
+ 2 => return Err(Error::Unsupported("construction_method 'item_offset' is not supported")),
+ _ => return Err(Error::InvalidData("construction_method is taken from the set 0, 1 or 2 per ISOBMFF (ISO 14496-12:2020) § 8.11.3.3"))
+ }
+ }
+ };
+
+ let data_reference_index = iloc.read_u16(16)?;
+
+ if data_reference_index != 0 {
+ return Err(Error::Unsupported(
+ "external file references (iloc.data_reference_index != 0) are not supported",
+ ));
+ }
+
+ let base_offset = iloc.read_u64(base_offset_size.as_bits())?;
+ let extent_count = iloc.read_u16(16)?;
+
+ if extent_count < 1 {
+ return Err(Error::InvalidData(
+ "extent_count must have a value 1 or greater per ISOBMFF (ISO 14496-12:2020) § 8.11.3.3",
+ ));
+ }
+
+ // "If only one extent is used (extent_count = 1) then either or both of the
+ // offset and length may be implied"
+ if extent_count != 1
+ && (offset_size == IlocFieldSize::Zero || length_size == IlocFieldSize::Zero)
+ {
+ return Err(Error::InvalidData(
+ "extent_count != 1 requires explicit offset and length per ISOBMFF (ISO 14496-12:2020) § 8.11.3.3",
+ ));
+ }
+
+ let mut extents = TryVec::with_capacity(extent_count.to_usize())?;
+
+ for _ in 0..extent_count {
+ // Parsed but currently ignored, see `Extent`
+ let _extent_index = match &index_size {
+ None | Some(IlocFieldSize::Zero) => None,
+ Some(index_size) => {
+ debug_assert!(version == IlocVersion::One || version == IlocVersion::Two);
+ Some(iloc.read_u64(index_size.as_bits())?)
+ }
+ };
+
+ // Per ISOBMFF (ISO 14496-12:2020) § 8.11.3.1:
+ // "If the offset is not identified (the field has a length of zero), then the
+ // beginning of the source (offset 0) is implied"
+ // This behavior will follow from BitReader::read_u64(0) -> 0.
+ let extent_offset = iloc.read_u64(offset_size.as_bits())?;
+ let extent_length = iloc.read_u64(length_size.as_bits())?.try_into()?;
+
+ // "If the length is not specified, or specified as zero, then the entire length of
+ // the source is implied" (ibid)
+ let offset = base_offset
+ .checked_add(extent_offset)
+ .ok_or(Error::InvalidData("offset calculation overflow"))?;
+ let extent = if extent_length == 0 {
+ Extent::ToEnd { offset }
+ } else {
+ Extent::WithLength {
+ offset,
+ len: extent_length,
+ }
+ };
+
+ extents.push(extent)?;
+ }
+
+ let loc = ItemLocationBoxItem {
+ construction_method,
+ extents,
+ };
+
+ if items.insert(item_id, loc)?.is_some() {
+ return Err(Error::InvalidData("duplicate item_ID in iloc"));
+ }
+ }
+
+ if iloc.remaining() == 0 {
+ Ok(items)
+ } else {
+ Err(Error::InvalidData("invalid iloc size"))
+ }
+}
+
+/// Read the contents of a box, including sub boxes.
+pub fn read_mp4<T: Read>(f: &mut T) -> Result<MediaContext> {
+ let mut context = None;
+ let mut found_ftyp = false;
+ // TODO(kinetik): Top-level parsing should handle zero-sized boxes
+ // rather than throwing an error.
+ let mut iter = BoxIter::new(f);
+ while let Some(mut b) = iter.next_box()? {
+ // box ordering: ftyp before any variable length box (inc. moov),
+ // but may not be first box in file if file signatures etc. present
+ // fragmented mp4 order: ftyp, moov, pairs of moof/mdat (1-multiple), mfra
+
+ // "special": uuid, wide (= 8 bytes)
+ // isom: moov, mdat, free, skip, udta, ftyp, moof, mfra
+ // iso2: pdin, meta
+ // iso3: meco
+ // iso5: styp, sidx, ssix, prft
+ // unknown, maybe: id32
+
+ // qt: pnot
+
+ // possibly allow anything where all printable and/or all lowercase printable
+ // "four printable characters from the ISO 8859-1 character set"
+ match b.head.name {
+ BoxType::FileTypeBox => {
+ let ftyp = read_ftyp(&mut b)?;
+ found_ftyp = true;
+ debug!("{:?}", ftyp);
+ }
+ BoxType::MovieBox => {
+ context = Some(read_moov(&mut b, context)?);
+ }
+ #[cfg(feature = "meta-xml")]
+ BoxType::MetadataBox => {
+ if let Some(ctx) = &mut context {
+ ctx.metadata = Some(read_meta(&mut b));
+ }
+ }
+ _ => skip_box_content(&mut b)?,
+ };
+ check_parser_state!(b.content);
+ if context.is_some() {
+ debug!(
+ "found moov {}, could stop pure 'moov' parser now",
+ if found_ftyp {
+ "and ftyp"
+ } else {
+ "but no ftyp"
+ }
+ );
+ }
+ }
+
+ // XXX(kinetik): This isn't perfect, as a "moov" with no contents is
+ // treated as okay but we haven't found anything useful. Needs more
+ // thought for clearer behaviour here.
+ context.ok_or(Error::NoMoov)
+}
+
+/// Parse a Movie Header Box
+/// See ISOBMFF (ISO 14496-12:2020) § 8.2.2
+fn parse_mvhd<T: Read>(f: &mut BMFFBox<T>) -> Result<Option<MediaTimeScale>> {
+ let mvhd = read_mvhd(f)?;
+ debug!("{:?}", mvhd);
+ if mvhd.timescale == 0 {
+ return Err(Error::InvalidData("zero timescale in mdhd"));
+ }
+ let timescale = Some(MediaTimeScale(u64::from(mvhd.timescale)));
+ Ok(timescale)
+}
+
+/// Parse a Movie Box
+/// See ISOBMFF (ISO 14496-12:2020) § 8.2.1
+/// Note that despite the spec indicating "exactly one" moov box should exist at
+/// the file container level, we support reading and merging multiple moov boxes
+/// such as with tests/test_case_1185230.mp4.
+fn read_moov<T: Read>(f: &mut BMFFBox<T>, context: Option<MediaContext>) -> Result<MediaContext> {
+ let MediaContext {
+ mut timescale,
+ mut tracks,
+ mut mvex,
+ mut psshs,
+ mut userdata,
+ #[cfg(feature = "meta-xml")]
+ metadata,
+ } = context.unwrap_or_default();
+
+ let mut iter = f.box_iter();
+ while let Some(mut b) = iter.next_box()? {
+ match b.head.name {
+ BoxType::MovieHeaderBox => {
+ timescale = parse_mvhd(&mut b)?;
+ }
+ BoxType::TrackBox => {
+ let mut track = Track::new(tracks.len());
+ read_trak(&mut b, &mut track)?;
+ tracks.push(track)?;
+ }
+ BoxType::MovieExtendsBox => {
+ mvex = Some(read_mvex(&mut b)?);
+ debug!("{:?}", mvex);
+ }
+ BoxType::ProtectionSystemSpecificHeaderBox => {
+ let pssh = read_pssh(&mut b)?;
+ debug!("{:?}", pssh);
+ psshs.push(pssh)?;
+ }
+ BoxType::UserdataBox => {
+ userdata = Some(read_udta(&mut b));
+ debug!("{:?}", userdata);
+ if let Some(Err(_)) = userdata {
+ // There was an error parsing userdata. Such failures are not fatal to overall
+ // parsing, just skip the rest of the box.
+ skip_box_remain(&mut b)?;
+ }
+ }
+ _ => skip_box_content(&mut b)?,
+ };
+ check_parser_state!(b.content);
+ }
+
+ Ok(MediaContext {
+ timescale,
+ tracks,
+ mvex,
+ psshs,
+ userdata,
+ #[cfg(feature = "meta-xml")]
+ metadata,
+ })
+}
+
+fn read_pssh<T: Read>(src: &mut BMFFBox<T>) -> Result<ProtectionSystemSpecificHeaderBox> {
+ let len = src.bytes_left();
+ let mut box_content = read_buf(src, len)?;
+ let (system_id, kid, data) = {
+ let pssh = &mut Cursor::new(&box_content);
+
+ let (version, _) = read_fullbox_extra(pssh)?;
+
+ let system_id = read_buf(pssh, 16)?;
+
+ let mut kid = TryVec::<ByteData>::new();
+ if version > 0 {
+ let count = be_u32(pssh)?;
+ for _ in 0..count {
+ let item = read_buf(pssh, 16)?;
+ kid.push(item)?;
+ }
+ }
+
+ let data_size = be_u32(pssh)?;
+ let data = read_buf(pssh, data_size.into())?;
+
+ (system_id, kid, data)
+ };
+
+ let mut pssh_box = TryVec::new();
+ write_be_u32(&mut pssh_box, src.head.size.try_into()?)?;
+ pssh_box.extend_from_slice(b"pssh")?;
+ pssh_box.append(&mut box_content)?;
+
+ Ok(ProtectionSystemSpecificHeaderBox {
+ system_id,
+ kid,
+ data,
+ box_content: pssh_box,
+ })
+}
+
+/// Parse a Movie Extends Box
+/// See ISOBMFF (ISO 14496-12:2020) § 8.8.1
+fn read_mvex<T: Read>(src: &mut BMFFBox<T>) -> Result<MovieExtendsBox> {
+ let mut iter = src.box_iter();
+ let mut fragment_duration = None;
+ while let Some(mut b) = iter.next_box()? {
+ match b.head.name {
+ BoxType::MovieExtendsHeaderBox => {
+ let duration = read_mehd(&mut b)?;
+ fragment_duration = Some(duration);
+ }
+ _ => skip_box_content(&mut b)?,
+ }
+ }
+ Ok(MovieExtendsBox { fragment_duration })
+}
+
+fn read_mehd<T: Read>(src: &mut BMFFBox<T>) -> Result<MediaScaledTime> {
+ let (version, _) = read_fullbox_extra(src)?;
+ let fragment_duration = match version {
+ 1 => be_u64(src)?,
+ 0 => u64::from(be_u32(src)?),
+ _ => return Err(Error::InvalidData("unhandled mehd version")),
+ };
+ Ok(MediaScaledTime(fragment_duration))
+}
+
+/// Parse a Track Box
+/// See ISOBMFF (ISO 14496-12:2020) § 8.3.1.
+fn read_trak<T: Read>(f: &mut BMFFBox<T>, track: &mut Track) -> Result<()> {
+ let mut iter = f.box_iter();
+ while let Some(mut b) = iter.next_box()? {
+ match b.head.name {
+ BoxType::TrackHeaderBox => {
+ let tkhd = read_tkhd(&mut b)?;
+ track.track_id = Some(tkhd.track_id);
+ track.tkhd = Some(tkhd.clone());
+ debug!("{:?}", tkhd);
+ }
+ BoxType::EditBox => read_edts(&mut b, track)?,
+ BoxType::MediaBox => read_mdia(&mut b, track)?,
+ _ => skip_box_content(&mut b)?,
+ };
+ check_parser_state!(b.content);
+ }
+ Ok(())
+}
+
+fn read_edts<T: Read>(f: &mut BMFFBox<T>, track: &mut Track) -> Result<()> {
+ let mut iter = f.box_iter();
+ while let Some(mut b) = iter.next_box()? {
+ match b.head.name {
+ BoxType::EditListBox => {
+ let elst = read_elst(&mut b)?;
+ if elst.edits.is_empty() {
+ debug!("empty edit list");
+ continue;
+ }
+ let mut empty_duration = 0;
+ let mut idx = 0;
+ if elst.edits[idx].media_time == -1 {
+ if elst.edits.len() < 2 {
+ debug!("expected additional edit, ignoring edit list");
+ continue;
+ }
+ empty_duration = elst.edits[idx].segment_duration;
+ idx += 1;
+ }
+ track.empty_duration = Some(MediaScaledTime(empty_duration));
+ let media_time = elst.edits[idx].media_time;
+ if media_time < 0 {
+ debug!("unexpected negative media time in edit");
+ }
+ track.media_time = Some(TrackScaledTime::<u64>(
+ std::cmp::max(0, media_time) as u64,
+ track.id,
+ ));
+ if elst.edits.len() > 2 {
+ debug!("ignoring edit list with {} entries", elst.edits.len());
+ }
+ debug!("{:?}", elst);
+ }
+ _ => skip_box_content(&mut b)?,
+ };
+ check_parser_state!(b.content);
+ }
+ Ok(())
+}
+
+#[allow(clippy::type_complexity)] // Allow the complex return, maybe rework in future
+fn parse_mdhd<T: Read>(
+ f: &mut BMFFBox<T>,
+ track: &mut Track,
+) -> Result<(
+ MediaHeaderBox,
+ Option<TrackScaledTime<u64>>,
+ Option<TrackTimeScale<u64>>,
+)> {
+ let mdhd = read_mdhd(f)?;
+ let duration = match mdhd.duration {
+ std::u64::MAX => None,
+ duration => Some(TrackScaledTime::<u64>(duration, track.id)),
+ };
+ if mdhd.timescale == 0 {
+ return Err(Error::InvalidData("zero timescale in mdhd"));
+ }
+ let timescale = Some(TrackTimeScale::<u64>(u64::from(mdhd.timescale), track.id));
+ Ok((mdhd, duration, timescale))
+}
+
+fn read_mdia<T: Read>(f: &mut BMFFBox<T>, track: &mut Track) -> Result<()> {
+ let mut iter = f.box_iter();
+ while let Some(mut b) = iter.next_box()? {
+ match b.head.name {
+ BoxType::MediaHeaderBox => {
+ let (mdhd, duration, timescale) = parse_mdhd(&mut b, track)?;
+ track.duration = duration;
+ track.timescale = timescale;
+ debug!("{:?}", mdhd);
+ }
+ BoxType::HandlerBox => {
+ let hdlr = read_hdlr(&mut b, ParseStrictness::Permissive)?;
+
+ match hdlr.handler_type.value.as_ref() {
+ b"vide" => track.track_type = TrackType::Video,
+ b"soun" => track.track_type = TrackType::Audio,
+ b"meta" => track.track_type = TrackType::Metadata,
+ _ => (),
+ }
+ debug!("{:?}", hdlr);
+ }
+ BoxType::MediaInformationBox => read_minf(&mut b, track)?,
+ _ => skip_box_content(&mut b)?,
+ };
+ check_parser_state!(b.content);
+ }
+ Ok(())
+}
+
+fn read_minf<T: Read>(f: &mut BMFFBox<T>, track: &mut Track) -> Result<()> {
+ let mut iter = f.box_iter();
+ while let Some(mut b) = iter.next_box()? {
+ match b.head.name {
+ BoxType::SampleTableBox => read_stbl(&mut b, track)?,
+ _ => skip_box_content(&mut b)?,
+ };
+ check_parser_state!(b.content);
+ }
+ Ok(())
+}
+
+fn read_stbl<T: Read>(f: &mut BMFFBox<T>, track: &mut Track) -> Result<()> {
+ let mut iter = f.box_iter();
+ while let Some(mut b) = iter.next_box()? {
+ match b.head.name {
+ BoxType::SampleDescriptionBox => {
+ let stsd = read_stsd(&mut b, track)?;
+ debug!("{:?}", stsd);
+ track.stsd = Some(stsd);
+ }
+ BoxType::TimeToSampleBox => {
+ let stts = read_stts(&mut b)?;
+ debug!("{:?}", stts);
+ track.stts = Some(stts);
+ }
+ BoxType::SampleToChunkBox => {
+ let stsc = read_stsc(&mut b)?;
+ debug!("{:?}", stsc);
+ track.stsc = Some(stsc);
+ }
+ BoxType::SampleSizeBox => {
+ let stsz = read_stsz(&mut b)?;
+ debug!("{:?}", stsz);
+ track.stsz = Some(stsz);
+ }
+ BoxType::ChunkOffsetBox => {
+ let stco = read_stco(&mut b)?;
+ debug!("{:?}", stco);
+ track.stco = Some(stco);
+ }
+ BoxType::ChunkLargeOffsetBox => {
+ let co64 = read_co64(&mut b)?;
+ debug!("{:?}", co64);
+ track.stco = Some(co64);
+ }
+ BoxType::SyncSampleBox => {
+ let stss = read_stss(&mut b)?;
+ debug!("{:?}", stss);
+ track.stss = Some(stss);
+ }
+ BoxType::CompositionOffsetBox => {
+ let ctts = read_ctts(&mut b)?;
+ debug!("{:?}", ctts);
+ track.ctts = Some(ctts);
+ }
+ _ => skip_box_content(&mut b)?,
+ };
+ check_parser_state!(b.content);
+ }
+ Ok(())
+}
+
+/// Parse an ftyp box.
+/// See ISOBMFF (ISO 14496-12:2020) § 4.3
+fn read_ftyp<T: Read>(src: &mut BMFFBox<T>) -> Result<FileTypeBox> {
+ let major = be_u32(src)?;
+ let minor = be_u32(src)?;
+ let bytes_left = src.bytes_left();
+ if bytes_left % 4 != 0 {
+ return Err(Error::InvalidData("invalid ftyp size"));
+ }
+ // Is a brand_count of zero valid?
+ let brand_count = bytes_left / 4;
+ let mut brands = TryVec::with_capacity(brand_count.try_into()?)?;
+ for _ in 0..brand_count {
+ brands.push(be_u32(src)?.into())?;
+ }
+ Ok(FileTypeBox {
+ major_brand: From::from(major),
+ minor_version: minor,
+ compatible_brands: brands,
+ })
+}
+
+/// Parse an mvhd box.
+fn read_mvhd<T: Read>(src: &mut BMFFBox<T>) -> Result<MovieHeaderBox> {
+ let (version, _) = read_fullbox_extra(src)?;
+ match version {
+ // 64 bit creation and modification times.
+ 1 => {
+ skip(src, 16)?;
+ }
+ // 32 bit creation and modification times.
+ 0 => {
+ skip(src, 8)?;
+ }
+ _ => return Err(Error::InvalidData("unhandled mvhd version")),
+ }
+ let timescale = be_u32(src)?;
+ let duration = match version {
+ 1 => be_u64(src)?,
+ 0 => {
+ let d = be_u32(src)?;
+ if d == std::u32::MAX {
+ std::u64::MAX
+ } else {
+ u64::from(d)
+ }
+ }
+ _ => return Err(Error::InvalidData("unhandled mvhd version")),
+ };
+ // Skip remaining fields.
+ skip(src, 80)?;
+ Ok(MovieHeaderBox {
+ timescale,
+ duration,
+ })
+}
+
+/// Parse a tkhd box.
+fn read_tkhd<T: Read>(src: &mut BMFFBox<T>) -> Result<TrackHeaderBox> {
+ let (version, flags) = read_fullbox_extra(src)?;
+ let disabled = flags & 0x1u32 == 0 || flags & 0x2u32 == 0;
+ match version {
+ // 64 bit creation and modification times.
+ 1 => {
+ skip(src, 16)?;
+ }
+ // 32 bit creation and modification times.
+ 0 => {
+ skip(src, 8)?;
+ }
+ _ => return Err(Error::InvalidData("unhandled tkhd version")),
+ }
+ let track_id = be_u32(src)?;
+ skip(src, 4)?;
+ let duration = match version {
+ 1 => be_u64(src)?,
+ 0 => u64::from(be_u32(src)?),
+ _ => return Err(Error::InvalidData("unhandled tkhd version")),
+ };
+ // Skip uninteresting fields.
+ skip(src, 16)?;
+
+ let matrix = Matrix {
+ a: be_i32(src)?,
+ b: be_i32(src)?,
+ u: be_i32(src)?,
+ c: be_i32(src)?,
+ d: be_i32(src)?,
+ v: be_i32(src)?,
+ x: be_i32(src)?,
+ y: be_i32(src)?,
+ w: be_i32(src)?,
+ };
+
+ let width = be_u32(src)?;
+ let height = be_u32(src)?;
+ Ok(TrackHeaderBox {
+ track_id,
+ disabled,
+ duration,
+ width,
+ height,
+ matrix,
+ })
+}
+
+/// Parse a elst box.
+/// See ISOBMFF (ISO 14496-12:2020) § 8.6.6
+fn read_elst<T: Read>(src: &mut BMFFBox<T>) -> Result<EditListBox> {
+ let (version, _) = read_fullbox_extra(src)?;
+ let edit_count = be_u32(src)?;
+ let mut edits = TryVec::with_capacity(edit_count.to_usize())?;
+ for _ in 0..edit_count {
+ let (segment_duration, media_time) = match version {
+ 1 => {
+ // 64 bit segment duration and media times.
+ (be_u64(src)?, be_i64(src)?)
+ }
+ 0 => {
+ // 32 bit segment duration and media times.
+ (u64::from(be_u32(src)?), i64::from(be_i32(src)?))
+ }
+ _ => return Err(Error::InvalidData("unhandled elst version")),
+ };
+ let media_rate_integer = be_i16(src)?;
+ let media_rate_fraction = be_i16(src)?;
+ edits.push(Edit {
+ segment_duration,
+ media_time,
+ media_rate_integer,
+ media_rate_fraction,
+ })?;
+ }
+
+ // Padding could be added in some contents.
+ skip_box_remain(src)?;
+
+ Ok(EditListBox { edits })
+}
+
+/// Parse a mdhd box.
+fn read_mdhd<T: Read>(src: &mut BMFFBox<T>) -> Result<MediaHeaderBox> {
+ let (version, _) = read_fullbox_extra(src)?;
+ let (timescale, duration) = match version {
+ 1 => {
+ // Skip 64-bit creation and modification times.
+ skip(src, 16)?;
+
+ // 64 bit duration.
+ (be_u32(src)?, be_u64(src)?)
+ }
+ 0 => {
+ // Skip 32-bit creation and modification times.
+ skip(src, 8)?;
+
+ // 32 bit duration.
+ let timescale = be_u32(src)?;
+ let duration = {
+ // Since we convert the 32-bit duration to 64-bit by
+ // upcasting, we need to preserve the special all-1s
+ // ("unknown") case by hand.
+ let d = be_u32(src)?;
+ if d == std::u32::MAX {
+ std::u64::MAX
+ } else {
+ u64::from(d)
+ }
+ };
+ (timescale, duration)
+ }
+ _ => return Err(Error::InvalidData("unhandled mdhd version")),
+ };
+
+ // Skip uninteresting fields.
+ skip(src, 4)?;
+
+ Ok(MediaHeaderBox {
+ timescale,
+ duration,
+ })
+}
+
+/// Parse a stco box.
+/// See ISOBMFF (ISO 14496-12:2020) § 8.7.5
+fn read_stco<T: Read>(src: &mut BMFFBox<T>) -> Result<ChunkOffsetBox> {
+ let (_, _) = read_fullbox_extra(src)?;
+ let offset_count = be_u32(src)?;
+ let mut offsets = TryVec::with_capacity(offset_count.to_usize())?;
+ for _ in 0..offset_count {
+ offsets.push(be_u32(src)?.into())?;
+ }
+
+ // Padding could be added in some contents.
+ skip_box_remain(src)?;
+
+ Ok(ChunkOffsetBox { offsets })
+}
+
+/// Parse a co64 box.
+/// See ISOBMFF (ISO 14496-12:2020) § 8.7.5
+fn read_co64<T: Read>(src: &mut BMFFBox<T>) -> Result<ChunkOffsetBox> {
+ let (_, _) = read_fullbox_extra(src)?;
+ let offset_count = be_u32(src)?;
+ let mut offsets = TryVec::with_capacity(offset_count.to_usize())?;
+ for _ in 0..offset_count {
+ offsets.push(be_u64(src)?)?;
+ }
+
+ // Padding could be added in some contents.
+ skip_box_remain(src)?;
+
+ Ok(ChunkOffsetBox { offsets })
+}
+
+/// Parse a stss box.
+/// See ISOBMFF (ISO 14496-12:2020) § 8.6.2
+fn read_stss<T: Read>(src: &mut BMFFBox<T>) -> Result<SyncSampleBox> {
+ let (_, _) = read_fullbox_extra(src)?;
+ let sample_count = be_u32(src)?;
+ let mut samples = TryVec::with_capacity(sample_count.to_usize())?;
+ for _ in 0..sample_count {
+ samples.push(be_u32(src)?)?;
+ }
+
+ // Padding could be added in some contents.
+ skip_box_remain(src)?;
+
+ Ok(SyncSampleBox { samples })
+}
+
+/// Parse a stsc box.
+/// See ISOBMFF (ISO 14496-12:2020) § 8.7.4
+fn read_stsc<T: Read>(src: &mut BMFFBox<T>) -> Result<SampleToChunkBox> {
+ let (_, _) = read_fullbox_extra(src)?;
+ let sample_count = be_u32(src)?;
+ let mut samples = TryVec::with_capacity(sample_count.to_usize())?;
+ for _ in 0..sample_count {
+ let first_chunk = be_u32(src)?;
+ let samples_per_chunk = be_u32(src)?;
+ let sample_description_index = be_u32(src)?;
+ samples.push(SampleToChunk {
+ first_chunk,
+ samples_per_chunk,
+ sample_description_index,
+ })?;
+ }
+
+ // Padding could be added in some contents.
+ skip_box_remain(src)?;
+
+ Ok(SampleToChunkBox { samples })
+}
+
+/// Parse a Composition Time to Sample Box
+/// See ISOBMFF (ISO 14496-12:2020) § 8.6.1.3
+fn read_ctts<T: Read>(src: &mut BMFFBox<T>) -> Result<CompositionOffsetBox> {
+ let (version, _) = read_fullbox_extra(src)?;
+
+ let counts = be_u32(src)?;
+
+ if counts
+ .checked_mul(8)
+ .map_or(true, |bytes| u64::from(bytes) > src.bytes_left())
+ {
+ return Err(Error::InvalidData("insufficient data in 'ctts' box"));
+ }
+
+ let mut offsets = TryVec::with_capacity(counts.to_usize())?;
+ for _ in 0..counts {
+ let (sample_count, time_offset) = match version {
+ // According to spec, Version0 shoule be used when version == 0;
+ // however, some buggy contents have negative value when version == 0.
+ // So we always use Version1 here.
+ 0..=1 => {
+ let count = be_u32(src)?;
+ let offset = TimeOffsetVersion::Version1(be_i32(src)?);
+ (count, offset)
+ }
+ _ => {
+ return Err(Error::InvalidData("unsupported version in 'ctts' box"));
+ }
+ };
+ offsets.push(TimeOffset {
+ sample_count,
+ time_offset,
+ })?;
+ }
+
+ check_parser_state!(src.content);
+
+ Ok(CompositionOffsetBox { samples: offsets })
+}
+
+/// Parse a stsz box.
+/// See ISOBMFF (ISO 14496-12:2020) § 8.7.3.2
+fn read_stsz<T: Read>(src: &mut BMFFBox<T>) -> Result<SampleSizeBox> {
+ let (_, _) = read_fullbox_extra(src)?;
+ let sample_size = be_u32(src)?;
+ let sample_count = be_u32(src)?;
+ let mut sample_sizes = TryVec::new();
+ if sample_size == 0 {
+ sample_sizes.reserve(sample_count.to_usize())?;
+ for _ in 0..sample_count {
+ sample_sizes.push(be_u32(src)?)?;
+ }
+ }
+
+ // Padding could be added in some contents.
+ skip_box_remain(src)?;
+
+ Ok(SampleSizeBox {
+ sample_size,
+ sample_sizes,
+ })
+}
+
+/// Parse a stts box.
+/// See ISOBMFF (ISO 14496-12:2020) § 8.6.1.2
+fn read_stts<T: Read>(src: &mut BMFFBox<T>) -> Result<TimeToSampleBox> {
+ let (_, _) = read_fullbox_extra(src)?;
+ let sample_count = be_u32(src)?;
+ let mut samples = TryVec::with_capacity(sample_count.to_usize())?;
+ for _ in 0..sample_count {
+ let sample_count = be_u32(src)?;
+ let sample_delta = be_u32(src)?;
+ samples.push(Sample {
+ sample_count,
+ sample_delta,
+ })?;
+ }
+
+ // Padding could be added in some contents.
+ skip_box_remain(src)?;
+
+ Ok(TimeToSampleBox { samples })
+}
+
+/// Parse a VPx Config Box.
+fn read_vpcc<T: Read>(src: &mut BMFFBox<T>) -> Result<VPxConfigBox> {
+ let (version, _) = read_fullbox_extra(src)?;
+ let supported_versions = [0, 1];
+ if !supported_versions.contains(&version) {
+ return Err(Error::Unsupported("unknown vpcC version"));
+ }
+
+ let profile = src.read_u8()?;
+ let level = src.read_u8()?;
+ let (
+ bit_depth,
+ colour_primaries,
+ chroma_subsampling,
+ transfer_characteristics,
+ matrix_coefficients,
+ video_full_range_flag,
+ ) = if version == 0 {
+ let (bit_depth, colour_primaries) = {
+ let byte = src.read_u8()?;
+ ((byte >> 4) & 0x0f, byte & 0x0f)
+ };
+ // Note, transfer_characteristics was known as transfer_function in v0
+ let (chroma_subsampling, transfer_characteristics, video_full_range_flag) = {
+ let byte = src.read_u8()?;
+ ((byte >> 4) & 0x0f, (byte >> 1) & 0x07, (byte & 1) == 1)
+ };
+ (
+ bit_depth,
+ colour_primaries,
+ chroma_subsampling,
+ transfer_characteristics,
+ None,
+ video_full_range_flag,
+ )
+ } else {
+ let (bit_depth, chroma_subsampling, video_full_range_flag) = {
+ let byte = src.read_u8()?;
+ ((byte >> 4) & 0x0f, (byte >> 1) & 0x07, (byte & 1) == 1)
+ };
+ let colour_primaries = src.read_u8()?;
+ let transfer_characteristics = src.read_u8()?;
+ let matrix_coefficients = src.read_u8()?;
+
+ (
+ bit_depth,
+ colour_primaries,
+ chroma_subsampling,
+ transfer_characteristics,
+ Some(matrix_coefficients),
+ video_full_range_flag,
+ )
+ };
+
+ let codec_init_size = be_u16(src)?;
+ let codec_init = read_buf(src, codec_init_size.into())?;
+
+ // TODO(rillian): validate field value ranges.
+ Ok(VPxConfigBox {
+ profile,
+ level,
+ bit_depth,
+ colour_primaries,
+ chroma_subsampling,
+ transfer_characteristics,
+ matrix_coefficients,
+ video_full_range_flag,
+ codec_init,
+ })
+}
+
+/// See [AV1-ISOBMFF § 2.3.3](https://aomediacodec.github.io/av1-isobmff/#av1codecconfigurationbox-syntax)
+fn read_av1c<T: Read>(src: &mut BMFFBox<T>) -> Result<AV1ConfigBox> {
+ // We want to store the raw config as well as a structured (parsed) config, so create a copy of
+ // the raw config so we have it later, and then parse the structured data from that.
+ let raw_config = src.read_into_try_vec()?;
+ let mut raw_config_slice = raw_config.as_slice();
+ let marker_byte = raw_config_slice.read_u8()?;
+ if marker_byte & 0x80 != 0x80 {
+ return Err(Error::Unsupported("missing av1C marker bit"));
+ }
+ if marker_byte & 0x7f != 0x01 {
+ return Err(Error::Unsupported("missing av1C marker bit"));
+ }
+ let profile_byte = raw_config_slice.read_u8()?;
+ let profile = (profile_byte & 0xe0) >> 5;
+ let level = profile_byte & 0x1f;
+ let flags_byte = raw_config_slice.read_u8()?;
+ let tier = (flags_byte & 0x80) >> 7;
+ let bit_depth = match flags_byte & 0x60 {
+ 0x60 => 12,
+ 0x40 => 10,
+ _ => 8,
+ };
+ let monochrome = flags_byte & 0x10 == 0x10;
+ let chroma_subsampling_x = (flags_byte & 0x08) >> 3;
+ let chroma_subsampling_y = (flags_byte & 0x04) >> 2;
+ let chroma_sample_position = flags_byte & 0x03;
+ let delay_byte = raw_config_slice.read_u8()?;
+ let initial_presentation_delay_present = (delay_byte & 0x10) == 0x10;
+ let initial_presentation_delay_minus_one = if initial_presentation_delay_present {
+ delay_byte & 0x0f
+ } else {
+ 0
+ };
+
+ Ok(AV1ConfigBox {
+ profile,
+ level,
+ tier,
+ bit_depth,
+ monochrome,
+ chroma_subsampling_x,
+ chroma_subsampling_y,
+ chroma_sample_position,
+ initial_presentation_delay_present,
+ initial_presentation_delay_minus_one,
+ raw_config,
+ })
+}
+
+fn read_flac_metadata<T: Read>(src: &mut BMFFBox<T>) -> Result<FLACMetadataBlock> {
+ let temp = src.read_u8()?;
+ let block_type = temp & 0x7f;
+ let length = be_u24(src)?.into();
+ if length > src.bytes_left() {
+ return Err(Error::InvalidData(
+ "FLACMetadataBlock larger than parent box",
+ ));
+ }
+ let data = read_buf(src, length)?;
+ Ok(FLACMetadataBlock { block_type, data })
+}
+
+/// See MPEG-4 Systems (ISO 14496-1:2010) § 7.2.6.5
+fn find_descriptor(data: &[u8], esds: &mut ES_Descriptor) -> Result<()> {
+ // Tags for elementary stream description
+ const ESDESCR_TAG: u8 = 0x03;
+ const DECODER_CONFIG_TAG: u8 = 0x04;
+ const DECODER_SPECIFIC_TAG: u8 = 0x05;
+
+ let mut remains = data;
+
+ // Descriptor length should be more than 2 bytes.
+ while remains.len() > 2 {
+ let des = &mut Cursor::new(remains);
+ let tag = des.read_u8()?;
+
+ // See MPEG-4 Systems (ISO 14496-1:2010) § 8.3.3 for interpreting size of expandable classes
+
+ let mut end: u32 = 0; // It's u8 without declaration type that is incorrect.
+ // MSB of extend_or_len indicates more bytes, up to 4 bytes.
+ for _ in 0..4 {
+ if des.position() == remains.len().to_u64() {
+ // There's nothing more to read, the 0x80 was actually part of
+ // the content, and not an extension size.
+ end = des.position() as u32;
+ break;
+ }
+ let extend_or_len = des.read_u8()?;
+ end = (end << 7) + u32::from(extend_or_len & 0x7F);
+ if (extend_or_len & 0b1000_0000) == 0 {
+ end += des.position() as u32;
+ break;
+ }
+ }
+
+ if end.to_usize() > remains.len() || u64::from(end) < des.position() {
+ return Err(Error::InvalidData("Invalid descriptor."));
+ }
+
+ let descriptor = &remains[des.position().try_into()?..end.to_usize()];
+
+ match tag {
+ ESDESCR_TAG => {
+ read_es_descriptor(descriptor, esds)?;
+ }
+ DECODER_CONFIG_TAG => {
+ read_dc_descriptor(descriptor, esds)?;
+ }
+ DECODER_SPECIFIC_TAG => {
+ read_ds_descriptor(descriptor, esds)?;
+ }
+ _ => {
+ debug!("Unsupported descriptor, tag {}", tag);
+ }
+ }
+
+ remains = &remains[end.to_usize()..remains.len()];
+ debug!("remains.len(): {}", remains.len());
+ }
+
+ Ok(())
+}
+
+fn get_audio_object_type(bit_reader: &mut BitReader) -> Result<u16> {
+ let mut audio_object_type: u16 = ReadInto::read(bit_reader, 5)?;
+
+ // Extend audio object type, for example, HE-AAC.
+ if audio_object_type == 31 {
+ let audio_object_type_ext: u16 = ReadInto::read(bit_reader, 6)?;
+ audio_object_type = 32 + audio_object_type_ext;
+ }
+ Ok(audio_object_type)
+}
+
+/// See MPEG-4 Systems (ISO 14496-1:2010) § 7.2.6.7 and probably 14496-3 somewhere?
+fn read_ds_descriptor(data: &[u8], esds: &mut ES_Descriptor) -> Result<()> {
+ #[cfg(feature = "mp4v")]
+ // Check if we are in a Visual esda Box.
+ if esds.video_codec != CodecType::Unknown {
+ esds.decoder_specific_data.extend_from_slice(data)?;
+ return Ok(());
+ }
+
+ // We are in an Audio esda Box.
+ let frequency_table = vec![
+ (0x0, 96000),
+ (0x1, 88200),
+ (0x2, 64000),
+ (0x3, 48000),
+ (0x4, 44100),
+ (0x5, 32000),
+ (0x6, 24000),
+ (0x7, 22050),
+ (0x8, 16000),
+ (0x9, 12000),
+ (0xa, 11025),
+ (0xb, 8000),
+ (0xc, 7350),
+ ];
+
+ let bit_reader = &mut BitReader::new(data);
+
+ let mut audio_object_type = get_audio_object_type(bit_reader)?;
+
+ let sample_index: u32 = ReadInto::read(bit_reader, 4)?;
+
+ // Sample frequency could be from table, or retrieved from stream directly
+ // if index is 0x0f.
+ let sample_frequency = match sample_index {
+ 0x0F => Some(ReadInto::read(bit_reader, 24)?),
+ _ => frequency_table
+ .iter()
+ .find(|item| item.0 == sample_index)
+ .map(|x| x.1),
+ };
+
+ let channel_configuration: u16 = ReadInto::read(bit_reader, 4)?;
+
+ let extended_audio_object_type = match audio_object_type {
+ 5 | 29 => Some(5),
+ _ => None,
+ };
+
+ if audio_object_type == 5 || audio_object_type == 29 {
+ // We have an explicit signaling for BSAC extension, should the decoder
+ // decode the BSAC extension (all Gecko's AAC decoders do), then this is
+ // what the stream will actually look like once decoded.
+ let _extended_sample_index = ReadInto::read(bit_reader, 4)?;
+ let _extended_sample_frequency: Option<u32> = match _extended_sample_index {
+ 0x0F => Some(ReadInto::read(bit_reader, 24)?),
+ _ => frequency_table
+ .iter()
+ .find(|item| item.0 == sample_index)
+ .map(|x| x.1),
+ };
+ audio_object_type = get_audio_object_type(bit_reader)?;
+ let _extended_channel_configuration = match audio_object_type {
+ 22 => ReadInto::read(bit_reader, 4)?,
+ _ => channel_configuration,
+ };
+ };
+
+ match audio_object_type {
+ 1..=4 | 6 | 7 | 17 | 19..=23 => {
+ if sample_frequency.is_none() {
+ return Err(Error::Unsupported("unknown frequency"));
+ }
+
+ // parsing GASpecificConfig
+
+ // If the sampling rate is not one of the rates listed in the right
+ // column in Table 4.82, the sampling frequency dependent tables
+ // (code tables, scale factor band tables etc.) must be deduced in
+ // order for the bitstream payload to be parsed. Since a given
+ // sampling frequency is associated with only one sampling frequency
+ // table, and since maximum flexibility is desired in the range of
+ // possible sampling frequencies, the following table shall be used
+ // to associate an implied sampling frequency with the desired
+ // sampling frequency dependent tables.
+ let sample_frequency_value = match sample_frequency.unwrap() {
+ 0..=9390 => 8000,
+ 9391..=11501 => 11025,
+ 11502..=13855 => 12000,
+ 13856..=18782 => 16000,
+ 18783..=23003 => 22050,
+ 23004..=27712 => 24000,
+ 27713..=37565 => 32000,
+ 37566..=46008 => 44100,
+ 46009..=55425 => 48000,
+ 55426..=75131 => 64000,
+ 75132..=92016 => 88200,
+ _ => 96000,
+ };
+
+ bit_reader.skip(1)?; // frameLengthFlag
+ let depend_on_core_order: u8 = ReadInto::read(bit_reader, 1)?;
+ if depend_on_core_order > 0 {
+ bit_reader.skip(14)?; // codeCoderDelay
+ }
+ bit_reader.skip(1)?; // extensionFlag
+
+ let channel_counts = match channel_configuration {
+ 0 => {
+ debug!("Parsing program_config_element for channel counts");
+
+ bit_reader.skip(4)?; // element_instance_tag
+ bit_reader.skip(2)?; // object_type
+ bit_reader.skip(4)?; // sampling_frequency_index
+ let num_front_channel: u8 = ReadInto::read(bit_reader, 4)?;
+ let num_side_channel: u8 = ReadInto::read(bit_reader, 4)?;
+ let num_back_channel: u8 = ReadInto::read(bit_reader, 4)?;
+ let num_lfe_channel: u8 = ReadInto::read(bit_reader, 2)?;
+ bit_reader.skip(3)?; // num_assoc_data
+ bit_reader.skip(4)?; // num_valid_cc
+
+ let mono_mixdown_present: bool = ReadInto::read(bit_reader, 1)?;
+ if mono_mixdown_present {
+ bit_reader.skip(4)?; // mono_mixdown_element_number
+ }
+
+ let stereo_mixdown_present: bool = ReadInto::read(bit_reader, 1)?;
+ if stereo_mixdown_present {
+ bit_reader.skip(4)?; // stereo_mixdown_element_number
+ }
+
+ let matrix_mixdown_idx_present: bool = ReadInto::read(bit_reader, 1)?;
+ if matrix_mixdown_idx_present {
+ bit_reader.skip(2)?; // matrix_mixdown_idx
+ bit_reader.skip(1)?; // pseudo_surround_enable
+ }
+ let mut _channel_counts = 0;
+ _channel_counts += read_surround_channel_count(bit_reader, num_front_channel)?;
+ _channel_counts += read_surround_channel_count(bit_reader, num_side_channel)?;
+ _channel_counts += read_surround_channel_count(bit_reader, num_back_channel)?;
+ _channel_counts += read_surround_channel_count(bit_reader, num_lfe_channel)?;
+ _channel_counts
+ }
+ 1..=7 => channel_configuration,
+ // Amendment 4 of the AAC standard in 2013 below
+ 11 => 7, // 6.1 Amendment 4 of the AAC standard in 2013
+ 12 | 14 => 8, // 7.1 (a/d) of ITU BS.2159
+ _ => {
+ return Err(Error::Unsupported("invalid channel configuration"));
+ }
+ };
+
+ esds.audio_object_type = Some(audio_object_type);
+ esds.extended_audio_object_type = extended_audio_object_type;
+ esds.audio_sample_rate = Some(sample_frequency_value);
+ esds.audio_channel_count = Some(channel_counts);
+ if !esds.decoder_specific_data.is_empty() {
+ return Err(Error::InvalidData(
+ "There can be only one DecSpecificInfoTag descriptor",
+ ));
+ }
+ esds.decoder_specific_data.extend_from_slice(data)?;
+
+ Ok(())
+ }
+ _ => Err(Error::Unsupported("unknown aac audio object type")),
+ }
+}
+
+fn read_surround_channel_count(bit_reader: &mut BitReader, channels: u8) -> Result<u16> {
+ let mut count = 0;
+ for _ in 0..channels {
+ let is_cpe: bool = ReadInto::read(bit_reader, 1)?;
+ count += if is_cpe { 2 } else { 1 };
+ bit_reader.skip(4)?;
+ }
+ Ok(count)
+}
+
+/// See MPEG-4 Systems (ISO 14496-1:2010) § 7.2.6.6
+fn read_dc_descriptor(data: &[u8], esds: &mut ES_Descriptor) -> Result<()> {
+ let des = &mut Cursor::new(data);
+ let object_profile = des.read_u8()?;
+
+ #[cfg(feature = "mp4v")]
+ {
+ esds.video_codec = match object_profile {
+ 0x20..=0x24 => CodecType::MP4V,
+ _ => CodecType::Unknown,
+ };
+ }
+
+ // Skip uninteresting fields.
+ skip(des, 12)?;
+
+ if data.len().to_u64() > des.position() {
+ find_descriptor(&data[des.position().try_into()?..data.len()], esds)?;
+ }
+
+ esds.audio_codec = match object_profile {
+ 0x40 | 0x41 => CodecType::AAC,
+ 0x69 | 0x6B => CodecType::MP3,
+ _ => CodecType::Unknown,
+ };
+
+ debug!(
+ "read_dc_descriptor: esds.audio_codec = {:?}",
+ esds.audio_codec
+ );
+
+ Ok(())
+}
+
+/// See MPEG-4 Systems (ISO 14496-1:2010) § 7.2.6.5
+fn read_es_descriptor(data: &[u8], esds: &mut ES_Descriptor) -> Result<()> {
+ let des = &mut Cursor::new(data);
+
+ skip(des, 2)?;
+
+ let esds_flags = des.read_u8()?;
+
+ // Stream dependency flag, first bit from left most.
+ if esds_flags & 0x80 > 0 {
+ // Skip uninteresting fields.
+ skip(des, 2)?;
+ }
+
+ // Url flag, second bit from left most.
+ if esds_flags & 0x40 > 0 {
+ // Skip uninteresting fields.
+ let skip_es_len = u64::from(des.read_u8()?) + 2;
+ skip(des, skip_es_len)?;
+ }
+
+ if data.len().to_u64() > des.position() {
+ find_descriptor(&data[des.position().try_into()?..data.len()], esds)?;
+ }
+
+ Ok(())
+}
+
+/// See MP4 (ISO 14496-14:2020) § 6.7.2
+fn read_esds<T: Read>(src: &mut BMFFBox<T>) -> Result<ES_Descriptor> {
+ let (_, _) = read_fullbox_extra(src)?;
+
+ let esds_array = read_buf(src, src.bytes_left())?;
+
+ let mut es_data = ES_Descriptor::default();
+ find_descriptor(&esds_array, &mut es_data)?;
+
+ es_data.codec_esds = esds_array;
+
+ Ok(es_data)
+}
+
+/// Parse `FLACSpecificBox`.
+/// See [Encapsulation of FLAC in ISO Base Media File Format](https://github.com/xiph/flac/blob/master/doc/isoflac.txt) § 3.3.2
+fn read_dfla<T: Read>(src: &mut BMFFBox<T>) -> Result<FLACSpecificBox> {
+ let (version, flags) = read_fullbox_extra(src)?;
+ if version != 0 {
+ return Err(Error::Unsupported("unknown dfLa (FLAC) version"));
+ }
+ if flags != 0 {
+ return Err(Error::InvalidData("no-zero dfLa (FLAC) flags"));
+ }
+ let mut blocks = TryVec::new();
+ while src.bytes_left() > 0 {
+ let block = read_flac_metadata(src)?;
+ blocks.push(block)?;
+ }
+ // The box must have at least one meta block, and the first block
+ // must be the METADATA_BLOCK_STREAMINFO
+ if blocks.is_empty() {
+ return Err(Error::InvalidData("FLACSpecificBox missing metadata"));
+ } else if blocks[0].block_type != 0 {
+ return Err(Error::InvalidData(
+ "FLACSpecificBox must have STREAMINFO metadata first",
+ ));
+ } else if blocks[0].data.len() != 34 {
+ return Err(Error::InvalidData(
+ "FLACSpecificBox STREAMINFO block is the wrong size",
+ ));
+ }
+ Ok(FLACSpecificBox { version, blocks })
+}
+
+/// Parse `OpusSpecificBox`.
+fn read_dops<T: Read>(src: &mut BMFFBox<T>) -> Result<OpusSpecificBox> {
+ let version = src.read_u8()?;
+ if version != 0 {
+ return Err(Error::Unsupported("unknown dOps (Opus) version"));
+ }
+
+ let output_channel_count = src.read_u8()?;
+ let pre_skip = be_u16(src)?;
+ let input_sample_rate = be_u32(src)?;
+ let output_gain = be_i16(src)?;
+ let channel_mapping_family = src.read_u8()?;
+
+ let channel_mapping_table = if channel_mapping_family == 0 {
+ None
+ } else {
+ let stream_count = src.read_u8()?;
+ let coupled_count = src.read_u8()?;
+ let channel_mapping = read_buf(src, output_channel_count.into())?;
+
+ Some(ChannelMappingTable {
+ stream_count,
+ coupled_count,
+ channel_mapping,
+ })
+ };
+
+ // TODO(kinetik): validate field value ranges.
+ Ok(OpusSpecificBox {
+ version,
+ output_channel_count,
+ pre_skip,
+ input_sample_rate,
+ output_gain,
+ channel_mapping_family,
+ channel_mapping_table,
+ })
+}
+
+/// Re-serialize the Opus codec-specific config data as an `OpusHead` packet.
+///
+/// Some decoders expect the initialization data in the format used by the
+/// Ogg and WebM encapsulations. To support this we prepend the `OpusHead`
+/// tag and byte-swap the data from big- to little-endian relative to the
+/// dOps box.
+pub fn serialize_opus_header<W: byteorder::WriteBytesExt + std::io::Write>(
+ opus: &OpusSpecificBox,
+ dst: &mut W,
+) -> Result<()> {
+ match dst.write(b"OpusHead") {
+ Err(e) => return Err(Error::from(e)),
+ Ok(bytes) => {
+ if bytes != 8 {
+ return Err(Error::InvalidData("Couldn't write OpusHead tag."));
+ }
+ }
+ }
+ // In mp4 encapsulation, the version field is 0, but in ogg
+ // it is 1. While decoders generally accept zero as well, write
+ // out the version of the header we're supporting rather than
+ // whatever we parsed out of mp4.
+ dst.write_u8(1)?;
+ dst.write_u8(opus.output_channel_count)?;
+ dst.write_u16::<byteorder::LittleEndian>(opus.pre_skip)?;
+ dst.write_u32::<byteorder::LittleEndian>(opus.input_sample_rate)?;
+ dst.write_i16::<byteorder::LittleEndian>(opus.output_gain)?;
+ dst.write_u8(opus.channel_mapping_family)?;
+ match opus.channel_mapping_table {
+ None => {}
+ Some(ref table) => {
+ dst.write_u8(table.stream_count)?;
+ dst.write_u8(table.coupled_count)?;
+ match dst.write(&table.channel_mapping) {
+ Err(e) => return Err(Error::from(e)),
+ Ok(bytes) => {
+ if bytes != table.channel_mapping.len() {
+ return Err(Error::InvalidData(
+ "Couldn't write channel mapping table data.",
+ ));
+ }
+ }
+ }
+ }
+ };
+ Ok(())
+}
+
+/// Parse `ALACSpecificBox`.
+fn read_alac<T: Read>(src: &mut BMFFBox<T>) -> Result<ALACSpecificBox> {
+ let (version, flags) = read_fullbox_extra(src)?;
+ if version != 0 {
+ return Err(Error::Unsupported("unknown alac (ALAC) version"));
+ }
+ if flags != 0 {
+ return Err(Error::InvalidData("no-zero alac (ALAC) flags"));
+ }
+
+ let length = match src.bytes_left() {
+ x @ 24 | x @ 48 => x,
+ _ => {
+ return Err(Error::InvalidData(
+ "ALACSpecificBox magic cookie is the wrong size",
+ ))
+ }
+ };
+ let data = read_buf(src, length)?;
+
+ Ok(ALACSpecificBox { version, data })
+}
+
+/// Parse a Handler Reference Box.<br />
+/// See ISOBMFF (ISO 14496-12:2020) § 8.4.3<br />
+/// See [\[ISOBMFF\]: reserved (field = 0;) handling is ambiguous](https://github.com/MPEGGroup/FileFormat/issues/36)
+fn read_hdlr<T: Read>(src: &mut BMFFBox<T>, strictness: ParseStrictness) -> Result<HandlerBox> {
+ if read_fullbox_version_no_flags(src)? != 0 {
+ return Err(Error::Unsupported("hdlr version"));
+ }
+
+ let pre_defined = be_u32(src)?;
+ if pre_defined != 0 {
+ fail_if(
+ strictness == ParseStrictness::Strict,
+ "The HandlerBox 'pre_defined' field shall be 0 \
+ per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2",
+ )?;
+ }
+
+ let handler_type = FourCC::from(be_u32(src)?);
+
+ for _ in 1..=3 {
+ let reserved = be_u32(src)?;
+ if reserved != 0 {
+ fail_if(
+ strictness == ParseStrictness::Strict,
+ "The HandlerBox 'reserved' fields shall be 0 \
+ per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2",
+ )?;
+ }
+ }
+
+ match std::str::from_utf8(src.read_into_try_vec()?.as_slice()) {
+ Ok(name) => {
+ match name.bytes().filter(|&b| b == b'\0').count() {
+ 0 => fail_if(
+ strictness != ParseStrictness::Permissive,
+ "The HandlerBox 'name' field shall be null-terminated \
+ per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2",
+ )?,
+ 1 => (),
+ _ =>
+ // See https://github.com/MPEGGroup/FileFormat/issues/35
+ {
+ fail_if(
+ strictness == ParseStrictness::Strict,
+ "The HandlerBox 'name' field shall have a NUL byte \
+ only in the final position \
+ per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2",
+ )?
+ }
+ }
+ }
+ Err(_) => fail_if(
+ strictness != ParseStrictness::Permissive,
+ "The HandlerBox 'name' field shall be valid utf8 \
+ per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2",
+ )?,
+ }
+
+ Ok(HandlerBox { handler_type })
+}
+
+/// Parse an video description inside an stsd box.
+fn read_video_sample_entry<T: Read>(src: &mut BMFFBox<T>) -> Result<SampleEntry> {
+ let name = src.get_header().name;
+ let codec_type = match name {
+ BoxType::AVCSampleEntry | BoxType::AVC3SampleEntry => CodecType::H264,
+ BoxType::MP4VideoSampleEntry => CodecType::MP4V,
+ BoxType::VP8SampleEntry => CodecType::VP8,
+ BoxType::VP9SampleEntry => CodecType::VP9,
+ BoxType::AV1SampleEntry => CodecType::AV1,
+ BoxType::ProtectedVisualSampleEntry => CodecType::EncryptedVideo,
+ BoxType::H263SampleEntry => CodecType::H263,
+ _ => {
+ debug!("Unsupported video codec, box {:?} found", name);
+ CodecType::Unknown
+ }
+ };
+
+ // Skip uninteresting fields.
+ skip(src, 6)?;
+
+ let data_reference_index = be_u16(src)?;
+
+ // Skip uninteresting fields.
+ skip(src, 16)?;
+
+ let width = be_u16(src)?;
+ let height = be_u16(src)?;
+
+ // Skip uninteresting fields.
+ skip(src, 50)?;
+
+ // Skip clap/pasp/etc. for now.
+ let mut codec_specific = None;
+ let mut protection_info = TryVec::new();
+ let mut iter = src.box_iter();
+ while let Some(mut b) = iter.next_box()? {
+ match b.head.name {
+ BoxType::AVCConfigurationBox => {
+ if (name != BoxType::AVCSampleEntry
+ && name != BoxType::AVC3SampleEntry
+ && name != BoxType::ProtectedVisualSampleEntry)
+ || codec_specific.is_some()
+ {
+ return Err(Error::InvalidData("malformed video sample entry"));
+ }
+ let avcc_size = b
+ .head
+ .size
+ .checked_sub(b.head.offset)
+ .expect("offset invalid");
+ let avcc = read_buf(&mut b.content, avcc_size)?;
+ debug!("{:?} (avcc)", avcc);
+ // TODO(kinetik): Parse avcC box? For now we just stash the data.
+ codec_specific = Some(VideoCodecSpecific::AVCConfig(avcc));
+ }
+ BoxType::H263SpecificBox => {
+ if (name != BoxType::H263SampleEntry) || codec_specific.is_some() {
+ return Err(Error::InvalidData("malformed video sample entry"));
+ }
+ let h263_dec_spec_struc_size = b
+ .head
+ .size
+ .checked_sub(b.head.offset)
+ .expect("offset invalid");
+ let h263_dec_spec_struc = read_buf(&mut b.content, h263_dec_spec_struc_size)?;
+ debug!("{:?} (h263DecSpecStruc)", h263_dec_spec_struc);
+
+ codec_specific = Some(VideoCodecSpecific::H263Config(h263_dec_spec_struc));
+ }
+ BoxType::VPCodecConfigurationBox => {
+ // vpcC
+ if (name != BoxType::VP8SampleEntry
+ && name != BoxType::VP9SampleEntry
+ && name != BoxType::ProtectedVisualSampleEntry)
+ || codec_specific.is_some()
+ {
+ return Err(Error::InvalidData("malformed video sample entry"));
+ }
+ let vpcc = read_vpcc(&mut b)?;
+ codec_specific = Some(VideoCodecSpecific::VPxConfig(vpcc));
+ }
+ BoxType::AV1CodecConfigurationBox => {
+ if name != BoxType::AV1SampleEntry && name != BoxType::ProtectedVisualSampleEntry {
+ return Err(Error::InvalidData("malformed video sample entry"));
+ }
+ let av1c = read_av1c(&mut b)?;
+ codec_specific = Some(VideoCodecSpecific::AV1Config(av1c));
+ }
+ BoxType::ESDBox => {
+ if name != BoxType::MP4VideoSampleEntry || codec_specific.is_some() {
+ return Err(Error::InvalidData("malformed video sample entry"));
+ }
+ #[cfg(not(feature = "mp4v"))]
+ {
+ let (_, _) = read_fullbox_extra(&mut b.content)?;
+ // Subtract 4 extra to offset the members of fullbox not
+ // accounted for in head.offset
+ let esds_size = b
+ .head
+ .size
+ .checked_sub(b.head.offset + 4)
+ .expect("offset invalid");
+ let esds = read_buf(&mut b.content, esds_size)?;
+ codec_specific = Some(VideoCodecSpecific::ESDSConfig(esds));
+ }
+ #[cfg(feature = "mp4v")]
+ {
+ // Read ES_Descriptor inside an esds box.
+ // See ISOBMFF (ISO 14496-1:2010 §7.2.6.5)
+ let esds = read_esds(&mut b)?;
+ codec_specific =
+ Some(VideoCodecSpecific::ESDSConfig(esds.decoder_specific_data));
+ }
+ }
+ BoxType::ProtectionSchemeInfoBox => {
+ if name != BoxType::ProtectedVisualSampleEntry {
+ return Err(Error::InvalidData("malformed video sample entry"));
+ }
+ let sinf = read_sinf(&mut b)?;
+ debug!("{:?} (sinf)", sinf);
+ protection_info.push(sinf)?;
+ }
+ _ => {
+ debug!("Unsupported video codec, box {:?} found", b.head.name);
+ skip_box_content(&mut b)?;
+ }
+ }
+ check_parser_state!(b.content);
+ }
+
+ Ok(
+ codec_specific.map_or(SampleEntry::Unknown, |codec_specific| {
+ SampleEntry::Video(VideoSampleEntry {
+ codec_type,
+ data_reference_index,
+ width,
+ height,
+ codec_specific,
+ protection_info,
+ })
+ }),
+ )
+}
+
+fn read_qt_wave_atom<T: Read>(src: &mut BMFFBox<T>) -> Result<ES_Descriptor> {
+ let mut codec_specific = None;
+ let mut iter = src.box_iter();
+ while let Some(mut b) = iter.next_box()? {
+ match b.head.name {
+ BoxType::ESDBox => {
+ let esds = read_esds(&mut b)?;
+ codec_specific = Some(esds);
+ }
+ _ => skip_box_content(&mut b)?,
+ }
+ }
+
+ codec_specific.ok_or(Error::InvalidData("malformed audio sample entry"))
+}
+
+/// Parse an audio description inside an stsd box.
+/// See ISOBMFF (ISO 14496-12:2020) § 12.2.3
+fn read_audio_sample_entry<T: Read>(src: &mut BMFFBox<T>) -> Result<SampleEntry> {
+ let name = src.get_header().name;
+
+ // Skip uninteresting fields.
+ skip(src, 6)?;
+
+ let data_reference_index = be_u16(src)?;
+
+ // XXX(kinetik): This is "reserved" in BMFF, but some old QT MOV variant
+ // uses it, need to work out if we have to support it. Without checking
+ // here and reading extra fields after samplerate (or bailing with an
+ // error), the parser loses sync completely.
+ let version = be_u16(src)?;
+
+ // Skip uninteresting fields.
+ skip(src, 6)?;
+
+ let mut channelcount = u32::from(be_u16(src)?);
+ let samplesize = be_u16(src)?;
+
+ // Skip uninteresting fields.
+ skip(src, 4)?;
+
+ let mut samplerate = f64::from(be_u32(src)? >> 16); // 16.16 fixed point;
+
+ match version {
+ 0 => (),
+ 1 => {
+ // Quicktime sound sample description version 1.
+ // Skip uninteresting fields.
+ skip(src, 16)?;
+ }
+ 2 => {
+ // Quicktime sound sample description version 2.
+ skip(src, 4)?;
+ samplerate = f64::from_bits(be_u64(src)?);
+ channelcount = be_u32(src)?;
+ skip(src, 20)?;
+ }
+ _ => {
+ return Err(Error::Unsupported(
+ "unsupported non-isom audio sample entry",
+ ))
+ }
+ }
+
+ let (mut codec_type, mut codec_specific) = match name {
+ BoxType::MP3AudioSampleEntry => (CodecType::MP3, Some(AudioCodecSpecific::MP3)),
+ BoxType::LPCMAudioSampleEntry => (CodecType::LPCM, Some(AudioCodecSpecific::LPCM)),
+ // Some mp4 file with AMR doesn't have AMRSpecificBox "damr" in followed while loop,
+ // we use empty box by default.
+ #[cfg(feature = "3gpp")]
+ BoxType::AMRNBSampleEntry => (
+ CodecType::AMRNB,
+ Some(AudioCodecSpecific::AMRSpecificBox(Default::default())),
+ ),
+ #[cfg(feature = "3gpp")]
+ BoxType::AMRWBSampleEntry => (
+ CodecType::AMRWB,
+ Some(AudioCodecSpecific::AMRSpecificBox(Default::default())),
+ ),
+ _ => (CodecType::Unknown, None),
+ };
+ let mut protection_info = TryVec::new();
+ let mut iter = src.box_iter();
+ while let Some(mut b) = iter.next_box()? {
+ match b.head.name {
+ BoxType::ESDBox => {
+ if (name != BoxType::MP4AudioSampleEntry
+ && name != BoxType::ProtectedAudioSampleEntry)
+ || codec_specific.is_some()
+ {
+ return Err(Error::InvalidData("malformed audio sample entry"));
+ }
+ let esds = read_esds(&mut b)?;
+ codec_type = esds.audio_codec;
+ codec_specific = Some(AudioCodecSpecific::ES_Descriptor(esds));
+ }
+ BoxType::FLACSpecificBox => {
+ if (name != BoxType::FLACSampleEntry && name != BoxType::ProtectedAudioSampleEntry)
+ || codec_specific.is_some()
+ {
+ return Err(Error::InvalidData("malformed audio sample entry"));
+ }
+ let dfla = read_dfla(&mut b)?;
+ codec_type = CodecType::FLAC;
+ codec_specific = Some(AudioCodecSpecific::FLACSpecificBox(dfla));
+ }
+ BoxType::OpusSpecificBox => {
+ if (name != BoxType::OpusSampleEntry && name != BoxType::ProtectedAudioSampleEntry)
+ || codec_specific.is_some()
+ {
+ return Err(Error::InvalidData("malformed audio sample entry"));
+ }
+ let dops = read_dops(&mut b)?;
+ codec_type = CodecType::Opus;
+ codec_specific = Some(AudioCodecSpecific::OpusSpecificBox(dops));
+ }
+ BoxType::ALACSpecificBox => {
+ if name != BoxType::ALACSpecificBox || codec_specific.is_some() {
+ return Err(Error::InvalidData("malformed audio sample entry"));
+ }
+ let alac = read_alac(&mut b)?;
+ codec_type = CodecType::ALAC;
+ codec_specific = Some(AudioCodecSpecific::ALACSpecificBox(alac));
+ }
+ BoxType::QTWaveAtom => {
+ let qt_esds = read_qt_wave_atom(&mut b)?;
+ codec_type = qt_esds.audio_codec;
+ codec_specific = Some(AudioCodecSpecific::ES_Descriptor(qt_esds));
+ }
+ BoxType::ProtectionSchemeInfoBox => {
+ if name != BoxType::ProtectedAudioSampleEntry {
+ return Err(Error::InvalidData("malformed audio sample entry"));
+ }
+ let sinf = read_sinf(&mut b)?;
+ debug!("{:?} (sinf)", sinf);
+ codec_type = CodecType::EncryptedAudio;
+ protection_info.push(sinf)?;
+ }
+ #[cfg(feature = "3gpp")]
+ BoxType::AMRSpecificBox => {
+ if codec_type != CodecType::AMRNB && codec_type != CodecType::AMRWB {
+ return Err(Error::InvalidData("malformed audio sample entry"));
+ }
+ let amr_dec_spec_struc_size = b
+ .head
+ .size
+ .checked_sub(b.head.offset)
+ .expect("offset invalid");
+ let amr_dec_spec_struc = read_buf(&mut b.content, amr_dec_spec_struc_size)?;
+ debug!("{:?} (AMRDecSpecStruc)", amr_dec_spec_struc);
+ codec_specific = Some(AudioCodecSpecific::AMRSpecificBox(amr_dec_spec_struc));
+ }
+ _ => {
+ debug!("Unsupported audio codec, box {:?} found", b.head.name);
+ skip_box_content(&mut b)?;
+ }
+ }
+ check_parser_state!(b.content);
+ }
+
+ Ok(
+ codec_specific.map_or(SampleEntry::Unknown, |codec_specific| {
+ SampleEntry::Audio(AudioSampleEntry {
+ codec_type,
+ data_reference_index,
+ channelcount,
+ samplesize,
+ samplerate,
+ codec_specific,
+ protection_info,
+ })
+ }),
+ )
+}
+
+/// Parse a stsd box.
+/// See ISOBMFF (ISO 14496-12:2020) § 8.5.2
+/// See MP4 (ISO 14496-14:2020) § 6.7.2
+fn read_stsd<T: Read>(src: &mut BMFFBox<T>, track: &mut Track) -> Result<SampleDescriptionBox> {
+ let (_, _) = read_fullbox_extra(src)?;
+
+ let description_count = be_u32(src)?;
+ let mut descriptions = TryVec::new();
+
+ {
+ let mut iter = src.box_iter();
+ while let Some(mut b) = iter.next_box()? {
+ let description = match track.track_type {
+ TrackType::Video => read_video_sample_entry(&mut b),
+ TrackType::Audio => read_audio_sample_entry(&mut b),
+ TrackType::Metadata => Err(Error::Unsupported("metadata track")),
+ TrackType::Unknown => Err(Error::Unsupported("unknown track type")),
+ };
+ let description = match description {
+ Ok(desc) => desc,
+ Err(Error::Unsupported(_)) => {
+ // read_{audio,video}_desc may have returned Unsupported
+ // after partially reading the box content, so we can't
+ // simply use skip_box_content here.
+ let to_skip = b.bytes_left();
+ skip(&mut b, to_skip)?;
+ SampleEntry::Unknown
+ }
+ Err(e) => return Err(e),
+ };
+ descriptions.push(description)?;
+ check_parser_state!(b.content);
+ if descriptions.len() == description_count.to_usize() {
+ break;
+ }
+ }
+ }
+
+ // Padding could be added in some contents.
+ skip_box_remain(src)?;
+
+ Ok(SampleDescriptionBox { descriptions })
+}
+
+fn read_sinf<T: Read>(src: &mut BMFFBox<T>) -> Result<ProtectionSchemeInfoBox> {
+ let mut sinf = ProtectionSchemeInfoBox::default();
+
+ let mut iter = src.box_iter();
+ while let Some(mut b) = iter.next_box()? {
+ match b.head.name {
+ BoxType::OriginalFormatBox => {
+ sinf.original_format = FourCC::from(be_u32(&mut b)?);
+ }
+ BoxType::SchemeTypeBox => {
+ sinf.scheme_type = Some(read_schm(&mut b)?);
+ }
+ BoxType::SchemeInformationBox => {
+ // We only need tenc box in schi box so far.
+ sinf.tenc = read_schi(&mut b)?;
+ }
+ _ => skip_box_content(&mut b)?,
+ }
+ check_parser_state!(b.content);
+ }
+
+ Ok(sinf)
+}
+
+fn read_schi<T: Read>(src: &mut BMFFBox<T>) -> Result<Option<TrackEncryptionBox>> {
+ let mut tenc = None;
+ let mut iter = src.box_iter();
+ while let Some(mut b) = iter.next_box()? {
+ match b.head.name {
+ BoxType::TrackEncryptionBox => {
+ if tenc.is_some() {
+ return Err(Error::InvalidData(
+ "tenc box should be only one at most in sinf box",
+ ));
+ }
+ tenc = Some(read_tenc(&mut b)?);
+ }
+ _ => skip_box_content(&mut b)?,
+ }
+ }
+
+ Ok(tenc)
+}
+
+fn read_tenc<T: Read>(src: &mut BMFFBox<T>) -> Result<TrackEncryptionBox> {
+ let (version, _) = read_fullbox_extra(src)?;
+
+ // reserved byte
+ skip(src, 1)?;
+ // the next byte is used to signal the default pattern in version >= 1
+ let (default_crypt_byte_block, default_skip_byte_block) = match version {
+ 0 => {
+ skip(src, 1)?;
+ (None, None)
+ }
+ _ => {
+ let pattern_byte = src.read_u8()?;
+ let crypt_bytes = pattern_byte >> 4;
+ let skip_bytes = pattern_byte & 0x0f;
+ (Some(crypt_bytes), Some(skip_bytes))
+ }
+ };
+ let default_is_encrypted = src.read_u8()?;
+ let default_iv_size = src.read_u8()?;
+ let default_kid = read_buf(src, 16)?;
+ // If default_is_encrypted == 1 && default_iv_size == 0 we expect a default_constant_iv
+ let default_constant_iv = match (default_is_encrypted, default_iv_size) {
+ (1, 0) => {
+ let default_constant_iv_size = src.read_u8()?;
+ Some(read_buf(src, default_constant_iv_size.into())?)
+ }
+ _ => None,
+ };
+
+ Ok(TrackEncryptionBox {
+ is_encrypted: default_is_encrypted,
+ iv_size: default_iv_size,
+ kid: default_kid,
+ crypt_byte_block_count: default_crypt_byte_block,
+ skip_byte_block_count: default_skip_byte_block,
+ constant_iv: default_constant_iv,
+ })
+}
+
+fn read_schm<T: Read>(src: &mut BMFFBox<T>) -> Result<SchemeTypeBox> {
+ // Flags can be used to signal presence of URI in the box, but we don't
+ // use the URI so don't bother storing the flags.
+ let (_, _) = read_fullbox_extra(src)?;
+ let scheme_type = FourCC::from(be_u32(src)?);
+ let scheme_version = be_u32(src)?;
+ // Null terminated scheme URI may follow, but we don't use it right now.
+ skip_box_remain(src)?;
+ Ok(SchemeTypeBox {
+ scheme_type,
+ scheme_version,
+ })
+}
+
+/// Parse a metadata box inside a moov, trak, or mdia box.
+/// See ISOBMFF (ISO 14496-12:2020) § 8.10.1.
+fn read_udta<T: Read>(src: &mut BMFFBox<T>) -> Result<UserdataBox> {
+ let mut iter = src.box_iter();
+ let mut udta = UserdataBox { meta: None };
+
+ while let Some(mut b) = iter.next_box()? {
+ match b.head.name {
+ BoxType::MetadataBox => {
+ let meta = read_meta(&mut b)?;
+ udta.meta = Some(meta);
+ }
+ _ => skip_box_content(&mut b)?,
+ };
+ check_parser_state!(b.content);
+ }
+ Ok(udta)
+}
+
+/// Parse the meta box
+/// See ISOBMFF (ISO 14496-12:2020) § 8.11.1
+fn read_meta<T: Read>(src: &mut BMFFBox<T>) -> Result<MetadataBox> {
+ let (_, _) = read_fullbox_extra(src)?;
+ let mut iter = src.box_iter();
+ let mut meta = MetadataBox::default();
+ while let Some(mut b) = iter.next_box()? {
+ match b.head.name {
+ BoxType::MetadataItemListEntry => read_ilst(&mut b, &mut meta)?,
+ #[cfg(feature = "meta-xml")]
+ BoxType::MetadataXMLBox => read_xml_(&mut b, &mut meta)?,
+ #[cfg(feature = "meta-xml")]
+ BoxType::MetadataBXMLBox => read_bxml(&mut b, &mut meta)?,
+ _ => skip_box_content(&mut b)?,
+ };
+ check_parser_state!(b.content);
+ }
+ Ok(meta)
+}
+
+/// Parse a XML box inside a meta box
+/// See ISOBMFF (ISO 14496-12:2020) § 8.11.2
+#[cfg(feature = "meta-xml")]
+fn read_xml_<T: Read>(src: &mut BMFFBox<T>, meta: &mut MetadataBox) -> Result<()> {
+ if read_fullbox_version_no_flags(src)? != 0 {
+ return Err(Error::Unsupported("unsupported XmlBox version"));
+ }
+ meta.xml = Some(XmlBox::StringXmlBox(src.read_into_try_vec()?));
+ Ok(())
+}
+
+/// Parse a Binary XML box inside a meta box
+/// See ISOBMFF (ISO 14496-12:2020) § 8.11.2
+#[cfg(feature = "meta-xml")]
+fn read_bxml<T: Read>(src: &mut BMFFBox<T>, meta: &mut MetadataBox) -> Result<()> {
+ if read_fullbox_version_no_flags(src)? != 0 {
+ return Err(Error::Unsupported("unsupported XmlBox version"));
+ }
+ meta.xml = Some(XmlBox::BinaryXmlBox(src.read_into_try_vec()?));
+ Ok(())
+}
+
+/// Parse a metadata box inside a udta box
+fn read_ilst<T: Read>(src: &mut BMFFBox<T>, meta: &mut MetadataBox) -> Result<()> {
+ let mut iter = src.box_iter();
+ while let Some(mut b) = iter.next_box()? {
+ match b.head.name {
+ BoxType::AlbumEntry => meta.album = read_ilst_string_data(&mut b)?,
+ BoxType::ArtistEntry | BoxType::ArtistLowercaseEntry => {
+ meta.artist = read_ilst_string_data(&mut b)?
+ }
+ BoxType::AlbumArtistEntry => meta.album_artist = read_ilst_string_data(&mut b)?,
+ BoxType::CommentEntry => meta.comment = read_ilst_string_data(&mut b)?,
+ BoxType::DateEntry => meta.year = read_ilst_string_data(&mut b)?,
+ BoxType::TitleEntry => meta.title = read_ilst_string_data(&mut b)?,
+ BoxType::CustomGenreEntry => {
+ meta.genre = read_ilst_string_data(&mut b)?.map(Genre::CustomGenre)
+ }
+ BoxType::StandardGenreEntry => {
+ meta.genre = read_ilst_u8_data(&mut b)?
+ .and_then(|gnre| Some(Genre::StandardGenre(gnre.get(1).copied()?)))
+ }
+ BoxType::ComposerEntry => meta.composer = read_ilst_string_data(&mut b)?,
+ BoxType::EncoderEntry => meta.encoder = read_ilst_string_data(&mut b)?,
+ BoxType::EncodedByEntry => meta.encoded_by = read_ilst_string_data(&mut b)?,
+ BoxType::CopyrightEntry => meta.copyright = read_ilst_string_data(&mut b)?,
+ BoxType::GroupingEntry => meta.grouping = read_ilst_string_data(&mut b)?,
+ BoxType::CategoryEntry => meta.category = read_ilst_string_data(&mut b)?,
+ BoxType::KeywordEntry => meta.keyword = read_ilst_string_data(&mut b)?,
+ BoxType::PodcastUrlEntry => meta.podcast_url = read_ilst_string_data(&mut b)?,
+ BoxType::PodcastGuidEntry => meta.podcast_guid = read_ilst_string_data(&mut b)?,
+ BoxType::DescriptionEntry => meta.description = read_ilst_string_data(&mut b)?,
+ BoxType::LongDescriptionEntry => meta.long_description = read_ilst_string_data(&mut b)?,
+ BoxType::LyricsEntry => meta.lyrics = read_ilst_string_data(&mut b)?,
+ BoxType::TVNetworkNameEntry => meta.tv_network_name = read_ilst_string_data(&mut b)?,
+ BoxType::TVEpisodeNameEntry => meta.tv_episode_name = read_ilst_string_data(&mut b)?,
+ BoxType::TVShowNameEntry => meta.tv_show_name = read_ilst_string_data(&mut b)?,
+ BoxType::PurchaseDateEntry => meta.purchase_date = read_ilst_string_data(&mut b)?,
+ BoxType::RatingEntry => meta.rating = read_ilst_string_data(&mut b)?,
+ BoxType::OwnerEntry => meta.owner = read_ilst_string_data(&mut b)?,
+ BoxType::HDVideoEntry => meta.hd_video = read_ilst_bool_data(&mut b)?,
+ BoxType::SortNameEntry => meta.sort_name = read_ilst_string_data(&mut b)?,
+ BoxType::SortArtistEntry => meta.sort_artist = read_ilst_string_data(&mut b)?,
+ BoxType::SortAlbumEntry => meta.sort_album = read_ilst_string_data(&mut b)?,
+ BoxType::SortAlbumArtistEntry => {
+ meta.sort_album_artist = read_ilst_string_data(&mut b)?
+ }
+ BoxType::SortComposerEntry => meta.sort_composer = read_ilst_string_data(&mut b)?,
+ BoxType::TrackNumberEntry => {
+ if let Some(trkn) = read_ilst_u8_data(&mut b)? {
+ meta.track_number = trkn.get(3).copied();
+ meta.total_tracks = trkn.get(5).copied();
+ };
+ }
+ BoxType::DiskNumberEntry => {
+ if let Some(disk) = read_ilst_u8_data(&mut b)? {
+ meta.disc_number = disk.get(3).copied();
+ meta.total_discs = disk.get(5).copied();
+ };
+ }
+ BoxType::TempoEntry => {
+ meta.beats_per_minute =
+ read_ilst_u8_data(&mut b)?.and_then(|tmpo| tmpo.get(1).copied())
+ }
+ BoxType::CompilationEntry => meta.compilation = read_ilst_bool_data(&mut b)?,
+ BoxType::AdvisoryEntry => {
+ meta.advisory = read_ilst_u8_data(&mut b)?.and_then(|rtng| {
+ Some(match rtng.get(0)? {
+ 2 => AdvisoryRating::Clean,
+ 0 => AdvisoryRating::Inoffensive,
+ r => AdvisoryRating::Explicit(*r),
+ })
+ })
+ }
+ BoxType::MediaTypeEntry => {
+ meta.media_type = read_ilst_u8_data(&mut b)?.and_then(|stik| {
+ Some(match stik.get(0)? {
+ 0 => MediaType::Movie,
+ 1 => MediaType::Normal,
+ 2 => MediaType::AudioBook,
+ 5 => MediaType::WhackedBookmark,
+ 6 => MediaType::MusicVideo,
+ 9 => MediaType::ShortFilm,
+ 10 => MediaType::TVShow,
+ 11 => MediaType::Booklet,
+ s => MediaType::Unknown(*s),
+ })
+ })
+ }
+ BoxType::PodcastEntry => meta.podcast = read_ilst_bool_data(&mut b)?,
+ BoxType::TVSeasonNumberEntry => {
+ meta.tv_season = read_ilst_u8_data(&mut b)?.and_then(|tvsn| tvsn.get(3).copied())
+ }
+ BoxType::TVEpisodeNumberEntry => {
+ meta.tv_episode_number =
+ read_ilst_u8_data(&mut b)?.and_then(|tves| tves.get(3).copied())
+ }
+ BoxType::GaplessPlaybackEntry => meta.gapless_playback = read_ilst_bool_data(&mut b)?,
+ BoxType::CoverArtEntry => meta.cover_art = read_ilst_multiple_u8_data(&mut b).ok(),
+ _ => skip_box_content(&mut b)?,
+ };
+ check_parser_state!(b.content);
+ }
+ Ok(())
+}
+
+fn read_ilst_bool_data<T: Read>(src: &mut BMFFBox<T>) -> Result<Option<bool>> {
+ Ok(read_ilst_u8_data(src)?.and_then(|d| Some(d.get(0)? == &1)))
+}
+
+fn read_ilst_string_data<T: Read>(src: &mut BMFFBox<T>) -> Result<Option<TryString>> {
+ read_ilst_u8_data(src)
+}
+
+fn read_ilst_u8_data<T: Read>(src: &mut BMFFBox<T>) -> Result<Option<TryVec<u8>>> {
+ // For all non-covr atoms, there must only be one data atom.
+ Ok(read_ilst_multiple_u8_data(src)?.pop())
+}
+
+fn read_ilst_multiple_u8_data<T: Read>(src: &mut BMFFBox<T>) -> Result<TryVec<TryVec<u8>>> {
+ let mut iter = src.box_iter();
+ let mut data = TryVec::new();
+ while let Some(mut b) = iter.next_box()? {
+ match b.head.name {
+ BoxType::MetadataItemDataEntry => {
+ data.push(read_ilst_data(&mut b)?)?;
+ }
+ _ => skip_box_content(&mut b)?,
+ };
+ check_parser_state!(b.content);
+ }
+ Ok(data)
+}
+
+fn read_ilst_data<T: Read>(src: &mut BMFFBox<T>) -> Result<TryVec<u8>> {
+ // Skip past the padding bytes
+ skip(&mut src.content, src.head.offset)?;
+ let size = src.content.limit();
+ read_buf(&mut src.content, size)
+}
+
+/// Skip a number of bytes that we don't care to parse.
+fn skip<T: Read>(src: &mut T, bytes: u64) -> Result<()> {
+ std::io::copy(&mut src.take(bytes), &mut std::io::sink())?;
+ Ok(())
+}
+
+/// Read size bytes into a Vector or return error.
+fn read_buf<T: Read>(src: &mut T, size: u64) -> Result<TryVec<u8>> {
+ let buf = src.take(size).read_into_try_vec()?;
+ if buf.len().to_u64() != size {
+ return Err(Error::InvalidData("failed buffer read"));
+ }
+
+ Ok(buf)
+}
+
+fn be_i16<T: ReadBytesExt>(src: &mut T) -> Result<i16> {
+ src.read_i16::<byteorder::BigEndian>().map_err(From::from)
+}
+
+fn be_i32<T: ReadBytesExt>(src: &mut T) -> Result<i32> {
+ src.read_i32::<byteorder::BigEndian>().map_err(From::from)
+}
+
+fn be_i64<T: ReadBytesExt>(src: &mut T) -> Result<i64> {
+ src.read_i64::<byteorder::BigEndian>().map_err(From::from)
+}
+
+fn be_u16<T: ReadBytesExt>(src: &mut T) -> Result<u16> {
+ src.read_u16::<byteorder::BigEndian>().map_err(From::from)
+}
+
+fn be_u24<T: ReadBytesExt>(src: &mut T) -> Result<u32> {
+ src.read_u24::<byteorder::BigEndian>().map_err(From::from)
+}
+
+fn be_u32<T: ReadBytesExt>(src: &mut T) -> Result<u32> {
+ src.read_u32::<byteorder::BigEndian>().map_err(From::from)
+}
+
+fn be_u64<T: ReadBytesExt>(src: &mut T) -> Result<u64> {
+ src.read_u64::<byteorder::BigEndian>().map_err(From::from)
+}
+
+fn write_be_u32<T: WriteBytesExt>(des: &mut T, num: u32) -> Result<()> {
+ des.write_u32::<byteorder::BigEndian>(num)
+ .map_err(From::from)
+}
--- /dev/null
+//! Module for parsing ISO Base Media Format aka video/mp4 streams.
+//! Internal unit tests.
+
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use super::read_mp4;
+use super::Error;
+use super::ParseStrictness;
+use fallible_collections::TryRead as _;
+
+use std::convert::TryInto as _;
+use std::io::Cursor;
+use std::io::Read as _;
+extern crate test_assembler;
+use self::test_assembler::*;
+
+use boxes::BoxType;
+
+enum BoxSize {
+ Short(u32),
+ Long(u64),
+ UncheckedShort(u32),
+ UncheckedLong(u64),
+ Auto,
+}
+
+#[allow(clippy::trivially_copy_pass_by_ref)] // TODO: Consider reworking to a copy
+fn make_box<F>(size: BoxSize, name: &[u8; 4], func: F) -> Cursor<Vec<u8>>
+where
+ F: Fn(Section) -> Section,
+{
+ let mut section = Section::new();
+ let box_size = Label::new();
+ section = match size {
+ BoxSize::Short(size) | BoxSize::UncheckedShort(size) => section.B32(size),
+ BoxSize::Long(_) | BoxSize::UncheckedLong(_) => section.B32(1),
+ BoxSize::Auto => section.B32(&box_size),
+ };
+ section = section.append_bytes(name);
+ section = match size {
+ // The spec allows the 32-bit size to be 0 to indicate unknown
+ // length streams. It's not clear if this is valid when using a
+ // 64-bit size, so prohibit it for now.
+ BoxSize::Long(size) => {
+ assert!(size > 0);
+ section.B64(size)
+ }
+ BoxSize::UncheckedLong(size) => section.B64(size),
+ _ => section,
+ };
+ section = func(section);
+ match size {
+ BoxSize::Short(size) => {
+ if size > 0 {
+ assert_eq!(u64::from(size), section.size())
+ }
+ }
+ BoxSize::Long(size) => assert_eq!(size, section.size()),
+ BoxSize::Auto => {
+ assert!(
+ section.size() <= u64::from(u32::max_value()),
+ "Tried to use a long box with BoxSize::Auto"
+ );
+ box_size.set_const(section.size());
+ }
+ // Skip checking BoxSize::Unchecked* cases.
+ _ => (),
+ }
+ Cursor::new(section.get_contents().unwrap())
+}
+
+fn make_uuid_box<F>(size: BoxSize, uuid: &[u8; 16], func: F) -> Cursor<Vec<u8>>
+where
+ F: Fn(Section) -> Section,
+{
+ make_box(size, b"uuid", |mut s| {
+ for b in uuid {
+ s = s.B8(*b);
+ }
+ func(s)
+ })
+}
+
+#[allow(clippy::trivially_copy_pass_by_ref)] // TODO: Consider reworking to a copy
+fn make_fullbox<F>(size: BoxSize, name: &[u8; 4], version: u8, func: F) -> Cursor<Vec<u8>>
+where
+ F: Fn(Section) -> Section,
+{
+ make_box(size, name, |s| func(s.B8(version).B8(0).B8(0).B8(0)))
+}
+
+#[test]
+fn read_box_header_short() {
+ let mut stream = make_box(BoxSize::Short(8), b"test", |s| s);
+ let header = super::read_box_header(&mut stream).unwrap();
+ assert_eq!(header.name, BoxType::UnknownBox(0x7465_7374)); // "test"
+ assert_eq!(header.size, 8);
+ assert!(header.uuid.is_none());
+}
+
+#[test]
+fn read_box_header_long() {
+ let mut stream = make_box(BoxSize::Long(16), b"test", |s| s);
+ let header = super::read_box_header(&mut stream).unwrap();
+ assert_eq!(header.name, BoxType::UnknownBox(0x7465_7374)); // "test"
+ assert_eq!(header.size, 16);
+ assert!(header.uuid.is_none());
+}
+
+#[test]
+fn read_box_header_short_unknown_size() {
+ let mut stream = make_box(BoxSize::Short(0), b"test", |s| s);
+ match super::read_box_header(&mut stream) {
+ Err(Error::Unsupported(s)) => assert_eq!(s, "unknown sized box"),
+ _ => panic!("unexpected result reading box with unknown size"),
+ };
+}
+
+#[test]
+fn read_box_header_short_invalid_size() {
+ let mut stream = make_box(BoxSize::UncheckedShort(2), b"test", |s| s);
+ match super::read_box_header(&mut stream) {
+ Err(Error::InvalidData(s)) => assert_eq!(s, "malformed size"),
+ _ => panic!("unexpected result reading box with invalid size"),
+ };
+}
+
+#[test]
+fn read_box_header_long_invalid_size() {
+ let mut stream = make_box(BoxSize::UncheckedLong(2), b"test", |s| s);
+ match super::read_box_header(&mut stream) {
+ Err(Error::InvalidData(s)) => assert_eq!(s, "malformed wide size"),
+ _ => panic!("unexpected result reading box with invalid size"),
+ };
+}
+
+#[test]
+fn read_box_header_uuid() {
+ const HEADER_UUID: [u8; 16] = [
+ 0x85, 0xc0, 0xb6, 0x87, 0x82, 0x0f, 0x11, 0xe0, 0x81, 0x11, 0xf4, 0xce, 0x46, 0x2b, 0x6a,
+ 0x48,
+ ];
+
+ let mut stream = make_uuid_box(BoxSize::Short(24), &HEADER_UUID, |s| s);
+ let mut iter = super::BoxIter::new(&mut stream);
+ let stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::UuidBox);
+ assert_eq!(stream.head.size, 24);
+ assert!(stream.head.uuid.is_some());
+ assert_eq!(stream.head.uuid.unwrap(), HEADER_UUID);
+}
+
+#[test]
+fn read_box_header_truncated_uuid() {
+ const HEADER_UUID: [u8; 16] = [
+ 0x85, 0xc0, 0xb6, 0x87, 0x82, 0x0f, 0x11, 0xe0, 0x81, 0x11, 0xf4, 0xce, 0x46, 0x2b, 0x6a,
+ 0x48,
+ ];
+
+ let mut stream = make_uuid_box(BoxSize::UncheckedShort(23), &HEADER_UUID, |s| s);
+ let mut iter = super::BoxIter::new(&mut stream);
+ let stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::UuidBox);
+ assert_eq!(stream.head.size, 23);
+ assert!(stream.head.uuid.is_none());
+}
+
+#[test]
+fn read_ftyp() {
+ let mut stream = make_box(BoxSize::Short(24), b"ftyp", |s| {
+ s.append_bytes(b"mp42")
+ .B32(0) // minor version
+ .append_bytes(b"isom")
+ .append_bytes(b"mp42")
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::FileTypeBox);
+ assert_eq!(stream.head.size, 24);
+ let parsed = super::read_ftyp(&mut stream).unwrap();
+ assert_eq!(parsed.major_brand, b"mp42"); // mp42
+ assert_eq!(parsed.minor_version, 0);
+ assert_eq!(parsed.compatible_brands.len(), 2);
+ assert_eq!(parsed.compatible_brands[0], b"isom"); // isom
+ assert_eq!(parsed.compatible_brands[1], b"mp42"); // mp42
+}
+
+#[test]
+fn read_truncated_ftyp() {
+ // We declare a 24 byte box, but only write 20 bytes.
+ let mut stream = make_box(BoxSize::UncheckedShort(24), b"ftyp", |s| {
+ s.append_bytes(b"mp42")
+ .B32(0) // minor version
+ .append_bytes(b"isom")
+ });
+ match read_mp4(&mut stream) {
+ Err(Error::UnexpectedEOF) => (),
+ Ok(_) => panic!("expected an error result"),
+ _ => panic!("expected a different error result"),
+ }
+}
+
+#[test]
+fn read_ftyp_case() {
+ // Brands in BMFF are represented as a u32, so it would seem clear that
+ // 0x6d703432 ("mp42") is not equal to 0x4d503432 ("MP42"), but some
+ // demuxers treat these as case-insensitive strings, e.g. street.mp4's
+ // major brand is "MP42". I haven't seen case-insensitive
+ // compatible_brands (which we also test here), but it doesn't seem
+ // unlikely given the major_brand behaviour.
+ let mut stream = make_box(BoxSize::Auto, b"ftyp", |s| {
+ s.append_bytes(b"MP42")
+ .B32(0) // minor version
+ .append_bytes(b"ISOM")
+ .append_bytes(b"MP42")
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::FileTypeBox);
+ assert_eq!(stream.head.size, 24);
+ let parsed = super::read_ftyp(&mut stream).unwrap();
+ assert_eq!(parsed.major_brand, b"MP42");
+ assert_eq!(parsed.minor_version, 0);
+ assert_eq!(parsed.compatible_brands.len(), 2);
+ assert_eq!(parsed.compatible_brands[0], b"ISOM"); // ISOM
+ assert_eq!(parsed.compatible_brands[1], b"MP42"); // MP42
+}
+
+#[test]
+fn read_elst_v0() {
+ let mut stream = make_fullbox(BoxSize::Short(28), b"elst", 0, |s| {
+ s.B32(1) // list count
+ // first entry
+ .B32(1234) // duration
+ .B32(5678) // time
+ .B16(12) // rate integer
+ .B16(34) // rate fraction
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::EditListBox);
+ assert_eq!(stream.head.size, 28);
+ let parsed = super::read_elst(&mut stream).unwrap();
+ assert_eq!(parsed.edits.len(), 1);
+ assert_eq!(parsed.edits[0].segment_duration, 1234);
+ assert_eq!(parsed.edits[0].media_time, 5678);
+ assert_eq!(parsed.edits[0].media_rate_integer, 12);
+ assert_eq!(parsed.edits[0].media_rate_fraction, 34);
+}
+
+#[test]
+fn read_elst_v1() {
+ let mut stream = make_fullbox(BoxSize::Short(56), b"elst", 1, |s| {
+ s.B32(2) // list count
+ // first entry
+ .B64(1234) // duration
+ .B64(5678) // time
+ .B16(12) // rate integer
+ .B16(34) // rate fraction
+ // second entry
+ .B64(1234) // duration
+ .B64(5678) // time
+ .B16(12) // rate integer
+ .B16(34) // rate fraction
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::EditListBox);
+ assert_eq!(stream.head.size, 56);
+ let parsed = super::read_elst(&mut stream).unwrap();
+ assert_eq!(parsed.edits.len(), 2);
+ assert_eq!(parsed.edits[1].segment_duration, 1234);
+ assert_eq!(parsed.edits[1].media_time, 5678);
+ assert_eq!(parsed.edits[1].media_rate_integer, 12);
+ assert_eq!(parsed.edits[1].media_rate_fraction, 34);
+}
+
+#[test]
+fn read_mdhd_v0() {
+ let mut stream = make_fullbox(BoxSize::Short(32), b"mdhd", 0, |s| {
+ s.B32(0)
+ .B32(0)
+ .B32(1234) // timescale
+ .B32(5678) // duration
+ .B32(0)
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::MediaHeaderBox);
+ assert_eq!(stream.head.size, 32);
+ let parsed = super::read_mdhd(&mut stream).unwrap();
+ assert_eq!(parsed.timescale, 1234);
+ assert_eq!(parsed.duration, 5678);
+}
+
+#[test]
+fn read_mdhd_v1() {
+ let mut stream = make_fullbox(BoxSize::Short(44), b"mdhd", 1, |s| {
+ s.B64(0)
+ .B64(0)
+ .B32(1234) // timescale
+ .B64(5678) // duration
+ .B32(0)
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::MediaHeaderBox);
+ assert_eq!(stream.head.size, 44);
+ let parsed = super::read_mdhd(&mut stream).unwrap();
+ assert_eq!(parsed.timescale, 1234);
+ assert_eq!(parsed.duration, 5678);
+}
+
+#[test]
+fn read_mdhd_unknown_duration() {
+ let mut stream = make_fullbox(BoxSize::Short(32), b"mdhd", 0, |s| {
+ s.B32(0)
+ .B32(0)
+ .B32(1234) // timescale
+ .B32(::std::u32::MAX) // duration
+ .B32(0)
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::MediaHeaderBox);
+ assert_eq!(stream.head.size, 32);
+ let parsed = super::read_mdhd(&mut stream).unwrap();
+ assert_eq!(parsed.timescale, 1234);
+ assert_eq!(parsed.duration, ::std::u64::MAX);
+}
+
+#[test]
+fn read_mdhd_invalid_timescale() {
+ let mut stream = make_fullbox(BoxSize::Short(44), b"mdhd", 1, |s| {
+ s.B64(0)
+ .B64(0)
+ .B32(0) // timescale
+ .B64(5678) // duration
+ .B32(0)
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::MediaHeaderBox);
+ assert_eq!(stream.head.size, 44);
+ let r = super::parse_mdhd(&mut stream, &mut super::Track::new(0));
+ assert!(r.is_err());
+}
+
+#[test]
+fn read_mvhd_v0() {
+ let mut stream = make_fullbox(BoxSize::Short(108), b"mvhd", 0, |s| {
+ s.B32(0).B32(0).B32(1234).B32(5678).append_repeated(0, 80)
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::MovieHeaderBox);
+ assert_eq!(stream.head.size, 108);
+ let parsed = super::read_mvhd(&mut stream).unwrap();
+ assert_eq!(parsed.timescale, 1234);
+ assert_eq!(parsed.duration, 5678);
+}
+
+#[test]
+fn read_mvhd_v1() {
+ let mut stream = make_fullbox(BoxSize::Short(120), b"mvhd", 1, |s| {
+ s.B64(0).B64(0).B32(1234).B64(5678).append_repeated(0, 80)
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::MovieHeaderBox);
+ assert_eq!(stream.head.size, 120);
+ let parsed = super::read_mvhd(&mut stream).unwrap();
+ assert_eq!(parsed.timescale, 1234);
+ assert_eq!(parsed.duration, 5678);
+}
+
+#[test]
+fn read_mvhd_invalid_timescale() {
+ let mut stream = make_fullbox(BoxSize::Short(120), b"mvhd", 1, |s| {
+ s.B64(0).B64(0).B32(0).B64(5678).append_repeated(0, 80)
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::MovieHeaderBox);
+ assert_eq!(stream.head.size, 120);
+ let r = super::parse_mvhd(&mut stream);
+ assert!(r.is_err());
+}
+
+#[test]
+fn read_mvhd_unknown_duration() {
+ let mut stream = make_fullbox(BoxSize::Short(108), b"mvhd", 0, |s| {
+ s.B32(0)
+ .B32(0)
+ .B32(1234)
+ .B32(::std::u32::MAX)
+ .append_repeated(0, 80)
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::MovieHeaderBox);
+ assert_eq!(stream.head.size, 108);
+ let parsed = super::read_mvhd(&mut stream).unwrap();
+ assert_eq!(parsed.timescale, 1234);
+ assert_eq!(parsed.duration, ::std::u64::MAX);
+}
+
+#[test]
+fn read_vpcc_version_0() {
+ let data_length = 12u16;
+ let mut stream = make_fullbox(BoxSize::Auto, b"vpcC", 0, |s| {
+ s.B8(2)
+ .B8(0)
+ .B8(0x82)
+ .B8(0)
+ .B16(data_length)
+ .append_repeated(42, data_length as usize)
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::VPCodecConfigurationBox);
+ let r = super::read_vpcc(&mut stream);
+ assert!(r.is_ok());
+}
+
+// TODO: it'd be better to find a real sample here.
+#[test]
+#[allow(clippy::unusual_byte_groupings)] // Allow odd grouping for test readability.
+fn read_vpcc_version_1() {
+ let data_length = 12u16;
+ let mut stream = make_fullbox(BoxSize::Auto, b"vpcC", 1, |s| {
+ s.B8(2) // profile
+ .B8(0) // level
+ .B8(0b1000_011_0) // bitdepth (4 bits), chroma (3 bits), video full range (1 bit)
+ .B8(1) // color primaries
+ .B8(1) // transfer characteristics
+ .B8(1) // matrix
+ .B16(data_length)
+ .append_repeated(42, data_length as usize)
+ });
+
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::VPCodecConfigurationBox);
+ let r = super::read_vpcc(&mut stream);
+ match r {
+ Ok(vpcc) => {
+ assert_eq!(vpcc.bit_depth, 8);
+ assert_eq!(vpcc.chroma_subsampling, 3);
+ assert!(!vpcc.video_full_range_flag);
+ assert_eq!(vpcc.matrix_coefficients.unwrap(), 1);
+ }
+ _ => panic!("vpcc parsing error"),
+ }
+}
+
+#[test]
+fn read_hdlr() {
+ let mut stream = make_fullbox(BoxSize::Short(45), b"hdlr", 0, |s| {
+ s.B32(0)
+ .append_bytes(b"vide")
+ .B32(0)
+ .B32(0)
+ .B32(0)
+ .append_bytes(b"VideoHandler")
+ .B8(0) // null-terminate string
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::HandlerBox);
+ assert_eq!(stream.head.size, 45);
+ let parsed = super::read_hdlr(&mut stream, ParseStrictness::Normal).unwrap();
+ assert_eq!(parsed.handler_type, b"vide");
+}
+
+#[test]
+fn read_hdlr_multiple_nul_in_name() {
+ let mut stream = make_fullbox(BoxSize::Short(45), b"hdlr", 0, |s| {
+ s.B32(0)
+ .append_bytes(b"vide")
+ .B32(0)
+ .B32(0)
+ .B32(0)
+ .append_bytes(b"Vide\0Handler")
+ .B8(0) // null-terminate string
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::HandlerBox);
+ assert_eq!(stream.head.size, 45);
+ assert!(super::read_hdlr(&mut stream, ParseStrictness::Strict).is_err());
+}
+
+#[test]
+fn read_hdlr_short_name() {
+ let mut stream = make_fullbox(BoxSize::Short(33), b"hdlr", 0, |s| {
+ s.B32(0).append_bytes(b"vide").B32(0).B32(0).B32(0).B8(0) // null-terminate string
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::HandlerBox);
+ assert_eq!(stream.head.size, 33);
+ let parsed = super::read_hdlr(&mut stream, ParseStrictness::Normal).unwrap();
+ assert_eq!(parsed.handler_type, b"vide");
+}
+
+#[test]
+fn read_hdlr_unsupported_version() {
+ let mut stream = make_fullbox(BoxSize::Short(32), b"hdlr", 1, |s| {
+ s.B32(0).append_bytes(b"vide").B32(0).B32(0).B32(0)
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::HandlerBox);
+ assert_eq!(stream.head.size, 32);
+ match super::read_hdlr(&mut stream, ParseStrictness::Normal) {
+ Err(Error::Unsupported(msg)) => assert_eq!("hdlr version", msg),
+ result => {
+ eprintln!("{:?}", result);
+ panic!("expected Error::Unsupported")
+ }
+ }
+}
+
+#[test]
+fn read_hdlr_invalid_pre_defined_field() {
+ let mut stream = make_fullbox(BoxSize::Short(32), b"hdlr", 0, |s| {
+ s.B32(1).append_bytes(b"vide").B32(0).B32(0).B32(0)
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::HandlerBox);
+ assert_eq!(stream.head.size, 32);
+ match super::read_hdlr(&mut stream, ParseStrictness::Strict) {
+ Err(Error::InvalidData(msg)) => assert_eq!(
+ "The HandlerBox 'pre_defined' field shall be 0 \
+ per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2",
+ msg
+ ),
+ result => {
+ eprintln!("{:?}", result);
+ panic!("expected Error::InvalidData")
+ }
+ }
+}
+
+#[test]
+fn read_hdlr_invalid_reserved_field() {
+ let mut stream = make_fullbox(BoxSize::Short(32), b"hdlr", 0, |s| {
+ s.B32(0).append_bytes(b"vide").B32(0).B32(1).B32(0)
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::HandlerBox);
+ assert_eq!(stream.head.size, 32);
+ match super::read_hdlr(&mut stream, ParseStrictness::Strict) {
+ Err(Error::InvalidData(msg)) => assert_eq!(
+ "The HandlerBox 'reserved' fields shall be 0 \
+ per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2",
+ msg
+ ),
+ result => {
+ eprintln!("{:?}", result);
+ panic!("expected Error::InvalidData")
+ }
+ }
+}
+
+#[test]
+fn read_hdlr_zero_length_name() {
+ let mut stream = make_fullbox(BoxSize::Short(32), b"hdlr", 0, |s| {
+ s.B32(0).append_bytes(b"vide").B32(0).B32(0).B32(0)
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::HandlerBox);
+ assert_eq!(stream.head.size, 32);
+ match super::read_hdlr(&mut stream, ParseStrictness::Normal) {
+ Err(Error::InvalidData(msg)) => assert_eq!(
+ "The HandlerBox 'name' field shall be null-terminated \
+ per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2",
+ msg
+ ),
+ result => {
+ eprintln!("{:?}", result);
+ panic!("expected Error::InvalidData")
+ }
+ }
+}
+
+#[test]
+fn read_hdlr_zero_length_name_permissive() {
+ let mut stream = make_fullbox(BoxSize::Short(32), b"hdlr", 0, |s| {
+ s.B32(0).append_bytes(b"vide").B32(0).B32(0).B32(0)
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::HandlerBox);
+ assert_eq!(stream.head.size, 32);
+ let parsed = super::read_hdlr(&mut stream, ParseStrictness::Permissive).unwrap();
+ assert_eq!(parsed.handler_type, b"vide");
+}
+
+fn flac_streaminfo() -> Vec<u8> {
+ vec![
+ 0x10, 0x00, 0x10, 0x00, 0x00, 0x0a, 0x11, 0x00, 0x38, 0x32, 0x0a, 0xc4, 0x42, 0xf0, 0x00,
+ 0xc9, 0xdf, 0xae, 0xb5, 0x66, 0xfc, 0x02, 0x15, 0xa3, 0xb1, 0x54, 0x61, 0x47, 0x0f, 0xfb,
+ 0x05, 0x00, 0x33, 0xad,
+ ]
+}
+
+#[test]
+fn read_flac() {
+ let mut stream = make_box(BoxSize::Auto, b"fLaC", |s| {
+ s.append_repeated(0, 6) // reserved
+ .B16(1) // data reference index
+ .B32(0) // reserved
+ .B32(0) // reserved
+ .B16(2) // channel count
+ .B16(16) // bits per sample
+ .B16(0) // pre_defined
+ .B16(0) // reserved
+ .B32(44100 << 16) // Sample rate
+ .append_bytes(
+ &make_dfla(
+ FlacBlockType::StreamInfo,
+ true,
+ &flac_streaminfo(),
+ FlacBlockLength::Correct,
+ )
+ .into_inner(),
+ )
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ let r = super::read_audio_sample_entry(&mut stream);
+ assert!(r.is_ok());
+}
+
+#[derive(Clone, Copy)]
+enum FlacBlockType {
+ StreamInfo = 0,
+ _Padding = 1,
+ _Application = 2,
+ _Seektable = 3,
+ _Comment = 4,
+ _Cuesheet = 5,
+ _Picture = 6,
+ _Reserved,
+ _Invalid = 127,
+}
+
+enum FlacBlockLength {
+ Correct,
+ Incorrect(usize),
+}
+
+fn make_dfla(
+ block_type: FlacBlockType,
+ last: bool,
+ data: &[u8],
+ data_length: FlacBlockLength,
+) -> Cursor<Vec<u8>> {
+ assert!(data.len() < 1 << 24);
+ make_fullbox(BoxSize::Auto, b"dfLa", 0, |s| {
+ let flag = if last { 1 } else { 0 };
+ let size = match data_length {
+ FlacBlockLength::Correct => (data.len() as u32) & 0x00ff_ffff,
+ FlacBlockLength::Incorrect(size) => {
+ assert!(size < 1 << 24);
+ (size as u32) & 0x00ff_ffff
+ }
+ };
+ let block_type = (block_type as u32) & 0x7f;
+ s.B32(flag << 31 | block_type << 24 | size)
+ .append_bytes(data)
+ })
+}
+
+#[test]
+fn read_dfla() {
+ let mut stream = make_dfla(
+ FlacBlockType::StreamInfo,
+ true,
+ &flac_streaminfo(),
+ FlacBlockLength::Correct,
+ );
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::FLACSpecificBox);
+ let dfla = super::read_dfla(&mut stream).unwrap();
+ assert_eq!(dfla.version, 0);
+}
+
+#[test]
+fn long_flac_metadata() {
+ let streaminfo = flac_streaminfo();
+ let mut stream = make_dfla(
+ FlacBlockType::StreamInfo,
+ true,
+ &streaminfo,
+ FlacBlockLength::Incorrect(streaminfo.len() + 4),
+ );
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::FLACSpecificBox);
+ let r = super::read_dfla(&mut stream);
+ assert!(r.is_err());
+}
+
+#[test]
+fn read_opus() {
+ let mut stream = make_box(BoxSize::Auto, b"Opus", |s| {
+ s.append_repeated(0, 6)
+ .B16(1) // data reference index
+ .B32(0)
+ .B32(0)
+ .B16(2) // channel count
+ .B16(16) // bits per sample
+ .B16(0)
+ .B16(0)
+ .B32(48000 << 16) // Sample rate is always 48 kHz for Opus.
+ .append_bytes(&make_dops().into_inner())
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ let r = super::read_audio_sample_entry(&mut stream);
+ assert!(r.is_ok());
+}
+
+fn make_dops() -> Cursor<Vec<u8>> {
+ make_box(BoxSize::Auto, b"dOps", |s| {
+ s.B8(0) // version
+ .B8(2) // channel count
+ .B16(348) // pre-skip
+ .B32(44100) // original sample rate
+ .B16(0) // gain
+ .B8(0) // channel mapping
+ })
+}
+
+#[test]
+fn read_dops() {
+ let mut stream = make_dops();
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ assert_eq!(stream.head.name, BoxType::OpusSpecificBox);
+ let r = super::read_dops(&mut stream);
+ assert!(r.is_ok());
+}
+
+#[test]
+fn serialize_opus_header() {
+ let opus = super::OpusSpecificBox {
+ version: 0,
+ output_channel_count: 1,
+ pre_skip: 342,
+ input_sample_rate: 24000,
+ output_gain: 0,
+ channel_mapping_family: 0,
+ channel_mapping_table: None,
+ };
+ let mut v = Vec::<u8>::new();
+ super::serialize_opus_header(&opus, &mut v).unwrap();
+ assert_eq!(v.len(), 19);
+ assert_eq!(
+ v,
+ vec![
+ 0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, 0x01, 0x01, 0x56, 0x01, 0xc0, 0x5d,
+ 0x00, 0x00, 0x00, 0x00, 0x00,
+ ]
+ );
+ let opus = super::OpusSpecificBox {
+ version: 0,
+ output_channel_count: 6,
+ pre_skip: 152,
+ input_sample_rate: 48000,
+ output_gain: 0,
+ channel_mapping_family: 1,
+ channel_mapping_table: Some(super::ChannelMappingTable {
+ stream_count: 4,
+ coupled_count: 2,
+ channel_mapping: vec![0, 4, 1, 2, 3, 5].into(),
+ }),
+ };
+ let mut v = Vec::<u8>::new();
+ super::serialize_opus_header(&opus, &mut v).unwrap();
+ assert_eq!(v.len(), 27);
+ assert_eq!(
+ v,
+ vec![
+ 0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, 0x01, 0x06, 0x98, 0x00, 0x80, 0xbb,
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x02, 0x00, 0x04, 0x01, 0x02, 0x03, 0x05,
+ ]
+ );
+}
+
+#[test]
+fn read_alac() {
+ let mut stream = make_box(BoxSize::Auto, b"alac", |s| {
+ s.append_repeated(0, 6) // reserved
+ .B16(1) // data reference index
+ .B32(0) // reserved
+ .B32(0) // reserved
+ .B16(2) // channel count
+ .B16(16) // bits per sample
+ .B16(0) // pre_defined
+ .B16(0) // reserved
+ .B32(44100 << 16) // Sample rate
+ .append_bytes(
+ &make_fullbox(BoxSize::Auto, b"alac", 0, |s| s.append_bytes(&[0xfa; 24]))
+ .into_inner(),
+ )
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ let r = super::read_audio_sample_entry(&mut stream);
+ assert!(r.is_ok());
+}
+
+#[test]
+fn esds_limit() {
+ let mut stream = make_box(BoxSize::Auto, b"mp4a", |s| {
+ s.append_repeated(0, 6)
+ .B16(1)
+ .B32(0)
+ .B32(0)
+ .B16(2)
+ .B16(16)
+ .B16(0)
+ .B16(0)
+ .B32(48000 << 16)
+ .B32(8)
+ .append_bytes(b"esds")
+ .append_repeated(0, 4)
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ match super::read_audio_sample_entry(&mut stream) {
+ Err(Error::UnexpectedEOF) => (),
+ Ok(_) => panic!("expected an error result"),
+ _ => panic!("expected a different error result"),
+ }
+}
+
+#[test]
+fn read_elst_zero_entries() {
+ let mut stream = make_fullbox(BoxSize::Auto, b"elst", 0, |s| s.B32(0).B16(12).B16(34));
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ match super::read_elst(&mut stream) {
+ Ok(elst) => assert_eq!(elst.edits.len(), 0),
+ _ => panic!("expected no error"),
+ }
+}
+
+fn make_elst() -> Cursor<Vec<u8>> {
+ make_fullbox(BoxSize::Auto, b"elst", 1, |s| {
+ s.B32(1)
+ // first entry
+ .B64(1234) // duration
+ .B64(0xffff_ffff_ffff_ffff) // time
+ .B16(12) // rate integer
+ .B16(34) // rate fraction
+ })
+}
+
+#[test]
+fn read_edts_bogus() {
+ // First edit list entry has a media_time of -1, so we expect a second
+ // edit list entry to be present to provide a valid media_time.
+ // Bogus edts are ignored.
+ let mut stream = make_box(BoxSize::Auto, b"edts", |s| {
+ s.append_bytes(&make_elst().into_inner())
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ let mut track = super::Track::new(0);
+ match super::read_edts(&mut stream, &mut track) {
+ Ok(_) => {
+ assert_eq!(track.media_time, None);
+ assert_eq!(track.empty_duration, None);
+ }
+ _ => panic!("expected no error"),
+ }
+}
+
+#[test]
+fn skip_padding_in_boxes() {
+ // Padding data could be added in the end of these boxes. Parser needs to skip
+ // them instead of returning error.
+ let box_names = vec![b"stts", b"stsc", b"stsz", b"stco", b"co64", b"stss"];
+
+ for name in box_names {
+ let mut stream = make_fullbox(BoxSize::Auto, name, 1, |s| {
+ s.append_repeated(0, 100) // add padding data
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ match name {
+ b"stts" => {
+ super::read_stts(&mut stream).expect("fail to skip padding: stts");
+ }
+ b"stsc" => {
+ super::read_stsc(&mut stream).expect("fail to skip padding: stsc");
+ }
+ b"stsz" => {
+ super::read_stsz(&mut stream).expect("fail to skip padding: stsz");
+ }
+ b"stco" => {
+ super::read_stco(&mut stream).expect("fail to skip padding: stco");
+ }
+ b"co64" => {
+ super::read_co64(&mut stream).expect("fail to skip padding: co64");
+ }
+ b"stss" => {
+ super::read_stss(&mut stream).expect("fail to skip padding: stss");
+ }
+ _ => (),
+ }
+ }
+}
+
+#[test]
+fn skip_padding_in_stsd() {
+ // Padding data could be added in the end of stsd boxes. Parser needs to skip
+ // them instead of returning error.
+ let avc = make_box(BoxSize::Auto, b"avc1", |s| {
+ s.append_repeated(0, 6)
+ .B16(1)
+ .append_repeated(0, 16)
+ .B16(320)
+ .B16(240)
+ .append_repeated(0, 14)
+ .append_repeated(0, 32)
+ .append_repeated(0, 4)
+ .B32(0xffff_ffff)
+ .append_bytes(b"avcC")
+ .append_repeated(0, 100)
+ })
+ .into_inner();
+ let mut stream = make_fullbox(BoxSize::Auto, b"stsd", 0, |s| {
+ s.B32(1)
+ .append_bytes(avc.as_slice())
+ .append_repeated(0, 100) // add padding data
+ });
+
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ super::read_stsd(&mut stream, &mut super::Track::new(0)).expect("fail to skip padding: stsd");
+}
+
+#[test]
+fn read_qt_wave_atom() {
+ let esds = make_fullbox(BoxSize::Auto, b"esds", 0, |s| {
+ s.B8(0x03) // elementary stream descriptor tag
+ .B8(0x12) // esds length
+ .append_repeated(0, 2)
+ .B8(0x00) // flags
+ .B8(0x04) // decoder config descriptor tag
+ .B8(0x0d) // dcds length
+ .B8(0x6b) // mp3
+ .append_repeated(0, 12)
+ })
+ .into_inner();
+ let chan = make_box(BoxSize::Auto, b"chan", |s| {
+ s.append_repeated(0, 10) // we don't care its data.
+ })
+ .into_inner();
+ let wave = make_box(BoxSize::Auto, b"wave", |s| s.append_bytes(esds.as_slice())).into_inner();
+ let mut stream = make_box(BoxSize::Auto, b"mp4a", |s| {
+ s.append_repeated(0, 6)
+ .B16(1) // data_reference_count
+ .B16(1) // verion: qt -> 1
+ .append_repeated(0, 6)
+ .B16(2)
+ .B16(16)
+ .append_repeated(0, 4)
+ .B32(48000 << 16)
+ .append_repeated(0, 16)
+ .append_bytes(wave.as_slice())
+ .append_bytes(chan.as_slice())
+ });
+
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ let sample_entry =
+ super::read_audio_sample_entry(&mut stream).expect("fail to read qt wave atom");
+ match sample_entry {
+ super::SampleEntry::Audio(sample_entry) => {
+ assert_eq!(sample_entry.codec_type, super::CodecType::MP3)
+ }
+ _ => panic!("fail to read audio sample enctry"),
+ }
+}
+
+#[test]
+fn read_descriptor_80() {
+ let aac_esds = vec![
+ 0x03, 0x80, 0x80, 0x80, 0x22, 0x00, 0x02, 0x00, 0x04, 0x80, 0x80, 0x80, 0x17, 0x40, 0x15,
+ 0x00, 0x00, 0x00, 0x00, 0x03, 0x22, 0xBC, 0x00, 0x01, 0xF5, 0x83, 0x05, 0x80, 0x80, 0x80,
+ 0x02, 0x11, 0x90, 0x06, 0x80, 0x80, 0x80, 0x01, 0x02,
+ ];
+ let aac_dc_descriptor = &aac_esds[31..33];
+ let mut stream = make_box(BoxSize::Auto, b"esds", |s| {
+ s.B32(0) // reserved
+ .append_bytes(aac_esds.as_slice())
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+
+ let es = super::read_esds(&mut stream).unwrap();
+
+ assert_eq!(es.audio_codec, super::CodecType::AAC);
+ assert_eq!(es.audio_object_type, Some(2));
+ assert_eq!(es.extended_audio_object_type, None);
+ assert_eq!(es.audio_sample_rate, Some(48000));
+ assert_eq!(es.audio_channel_count, Some(2));
+ assert_eq!(es.codec_esds, aac_esds);
+ assert_eq!(es.decoder_specific_data, aac_dc_descriptor);
+}
+
+#[test]
+fn read_esds() {
+ let aac_esds = vec![
+ 0x03, 0x24, 0x00, 0x00, 0x00, 0x04, 0x1c, 0x40, 0x15, 0x00, 0x12, 0x00, 0x00, 0x01, 0xf4,
+ 0x00, 0x00, 0x01, 0xf4, 0x00, 0x05, 0x0d, 0x13, 0x00, 0x05, 0x88, 0x05, 0x00, 0x48, 0x21,
+ 0x10, 0x00, 0x56, 0xe5, 0x98, 0x06, 0x01, 0x02,
+ ];
+ let aac_dc_descriptor = &aac_esds[22..35];
+
+ let mut stream = make_box(BoxSize::Auto, b"esds", |s| {
+ s.B32(0) // reserved
+ .append_bytes(aac_esds.as_slice())
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+
+ let es = super::read_esds(&mut stream).unwrap();
+
+ assert_eq!(es.audio_codec, super::CodecType::AAC);
+ assert_eq!(es.audio_object_type, Some(2));
+ assert_eq!(es.extended_audio_object_type, None);
+ assert_eq!(es.audio_sample_rate, Some(24000));
+ assert_eq!(es.audio_channel_count, Some(6));
+ assert_eq!(es.codec_esds, aac_esds);
+ assert_eq!(es.decoder_specific_data, aac_dc_descriptor);
+}
+
+#[test]
+fn read_esds_aac_type5() {
+ let aac_esds = vec![
+ 0x03, 0x80, 0x80, 0x80, 0x2F, 0x00, 0x00, 0x00, 0x04, 0x80, 0x80, 0x80, 0x21, 0x40, 0x15,
+ 0x00, 0x15, 0x00, 0x00, 0x03, 0xED, 0xAA, 0x00, 0x03, 0x6B, 0x00, 0x05, 0x80, 0x80, 0x80,
+ 0x0F, 0x2B, 0x01, 0x88, 0x02, 0xC4, 0x04, 0x90, 0x2C, 0x10, 0x8C, 0x80, 0x00, 0x00, 0xED,
+ 0x40, 0x06, 0x80, 0x80, 0x80, 0x01, 0x02,
+ ];
+
+ let aac_dc_descriptor = &aac_esds[31..46];
+
+ let mut stream = make_box(BoxSize::Auto, b"esds", |s| {
+ s.B32(0) // reserved
+ .append_bytes(aac_esds.as_slice())
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+
+ let es = super::read_esds(&mut stream).unwrap();
+
+ assert_eq!(es.audio_codec, super::CodecType::AAC);
+ assert_eq!(es.audio_object_type, Some(2));
+ assert_eq!(es.extended_audio_object_type, Some(5));
+ assert_eq!(es.audio_sample_rate, Some(24000));
+ assert_eq!(es.audio_channel_count, Some(8));
+ assert_eq!(es.codec_esds, aac_esds);
+ assert_eq!(es.decoder_specific_data, aac_dc_descriptor);
+}
+
+#[test]
+fn read_stsd_mp4v() {
+ let mp4v = vec![
+ 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, 0xd0, 0x01, 0xe0, 0x00, 0x48,
+ 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x20, 0x20, 0x20,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
+ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00,
+ 0x18, 0xff, 0xff, 0x00, 0x00, 0x00, 0x4c, 0x65, 0x73, 0x64, 0x73, 0x00, 0x00, 0x00, 0x00,
+ 0x03, 0x3e, 0x00, 0x00, 0x1f, 0x04, 0x36, 0x20, 0x11, 0x01, 0x77, 0x00, 0x00, 0x03, 0xe8,
+ 0x00, 0x00, 0x03, 0xe8, 0x00, 0x05, 0x27, 0x00, 0x00, 0x01, 0xb0, 0x05, 0x00, 0x00, 0x01,
+ 0xb5, 0x0e, 0xcf, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x20, 0x00, 0x86, 0xe0, 0x00,
+ 0x2e, 0xa6, 0x60, 0x16, 0xf4, 0x01, 0xf4, 0x24, 0xc8, 0x01, 0xe5, 0x16, 0x84, 0x3c, 0x14,
+ 0x63, 0x06, 0x01, 0x02,
+ ];
+ #[cfg(not(feature = "mp4v"))]
+ let esds_specific_data = &mp4v[90..];
+ #[cfg(feature = "mp4v")]
+ let esds_specific_data = &mp4v[112..151];
+ println!("esds_specific_data {:?}", esds_specific_data);
+
+ let mut stream = make_box(BoxSize::Auto, b"mp4v", |s| s.append_bytes(mp4v.as_slice()));
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+
+ let sample_entry = super::read_video_sample_entry(&mut stream).unwrap();
+
+ match sample_entry {
+ super::SampleEntry::Video(v) => {
+ assert_eq!(v.codec_type, super::CodecType::MP4V);
+ assert_eq!(v.width, 720);
+ assert_eq!(v.height, 480);
+ match v.codec_specific {
+ super::VideoCodecSpecific::ESDSConfig(esds_data) => {
+ assert_eq!(esds_data.as_slice(), esds_specific_data);
+ }
+ _ => panic!("it should be ESDSConfig!"),
+ }
+ }
+ _ => panic!("it should be a video sample entry!"),
+ }
+}
+
+#[test]
+fn read_esds_one_byte_extension_descriptor() {
+ let esds = vec![
+ 0x00, 0x03, 0x80, 0x1b, 0x00, 0x00, 0x00, 0x04, 0x80, 0x12, 0x40, 0x15, 0x00, 0x06, 0x00,
+ 0x00, 0x01, 0xfe, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x05, 0x80, 0x02, 0x11, 0x90, 0x06, 0x01,
+ 0x02,
+ ];
+
+ let mut stream = make_box(BoxSize::Auto, b"esds", |s| {
+ s.B32(0) // reserved
+ .append_bytes(esds.as_slice())
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+
+ let es = super::read_esds(&mut stream).unwrap();
+
+ assert_eq!(es.audio_codec, super::CodecType::AAC);
+ assert_eq!(es.audio_object_type, Some(2));
+ assert_eq!(es.extended_audio_object_type, None);
+ assert_eq!(es.audio_sample_rate, Some(48000));
+ assert_eq!(es.audio_channel_count, Some(2));
+}
+
+#[test]
+fn read_esds_byte_extension_descriptor() {
+ let mut stream = make_box(BoxSize::Auto, b"esds", |s| {
+ s.B32(0) // reserved
+ .B16(0x0003)
+ .B16(0x8181) // extension byte length 0x81
+ .append_repeated(0, 0x81)
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+
+ match super::read_esds(&mut stream) {
+ Ok(_) => (),
+ _ => panic!("fail to parse descriptor extension byte length"),
+ }
+}
+
+#[test]
+fn read_f4v_stsd() {
+ let mut stream = make_box(BoxSize::Auto, b".mp3", |s| {
+ s.append_repeated(0, 6)
+ .B16(1)
+ .B16(0)
+ .append_repeated(0, 6)
+ .B16(2)
+ .B16(16)
+ .append_repeated(0, 4)
+ .B32(48000 << 16)
+ });
+
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ let sample_entry =
+ super::read_audio_sample_entry(&mut stream).expect("failed to read f4v stsd atom");
+ match sample_entry {
+ super::SampleEntry::Audio(sample_entry) => {
+ assert_eq!(sample_entry.codec_type, super::CodecType::MP3)
+ }
+ _ => panic!("fail to read audio sample enctry"),
+ }
+}
+
+#[test]
+fn unknown_video_sample_entry() {
+ let unknown_codec = make_box(BoxSize::Auto, b"yyyy", |s| s.append_repeated(0, 16)).into_inner();
+ let mut stream = make_box(BoxSize::Auto, b"xxxx", |s| {
+ s.append_repeated(0, 6)
+ .B16(1)
+ .append_repeated(0, 16)
+ .B16(0)
+ .B16(0)
+ .append_repeated(0, 14)
+ .append_repeated(0, 32)
+ .append_repeated(0, 4)
+ .append_bytes(unknown_codec.as_slice())
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ match super::read_video_sample_entry(&mut stream) {
+ Ok(super::SampleEntry::Unknown) => (),
+ _ => panic!("expected a different error result"),
+ }
+}
+
+#[test]
+fn unknown_audio_sample_entry() {
+ let unknown_codec = make_box(BoxSize::Auto, b"yyyy", |s| s.append_repeated(0, 16)).into_inner();
+ let mut stream = make_box(BoxSize::Auto, b"xxxx", |s| {
+ s.append_repeated(0, 6)
+ .B16(1)
+ .B32(0)
+ .B32(0)
+ .B16(2)
+ .B16(16)
+ .B16(0)
+ .B16(0)
+ .B32(48000 << 16)
+ .append_bytes(unknown_codec.as_slice())
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+ match super::read_audio_sample_entry(&mut stream) {
+ Ok(super::SampleEntry::Unknown) => (),
+ _ => panic!("expected a different error result"),
+ }
+}
+
+#[test]
+fn read_esds_invalid_descriptor() {
+ // tag 0x06, 0xff, 0x7f is incorrect.
+ let esds = vec![
+ 0x03, 0x80, 0x80, 0x80, 0x22, 0x00, 0x00, 0x00, 0x04, 0x80, 0x80, 0x80, 0x14, 0x40, 0x01,
+ 0x00, 0x04, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x05, 0x80, 0x80, 0x80,
+ 0x02, 0xe8, 0x35, 0x06, 0xff, 0x7f, 0x00, 0x00,
+ ];
+
+ let mut stream = make_box(BoxSize::Auto, b"esds", |s| {
+ s.B32(0) // reserved
+ .append_bytes(esds.as_slice())
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+
+ match super::read_esds(&mut stream) {
+ Err(Error::InvalidData(s)) => assert_eq!(s, "Invalid descriptor."),
+ _ => panic!("unexpected result with invalid descriptor"),
+ }
+}
+
+#[test]
+fn read_esds_redundant_descriptor() {
+ // the '2' at the end is redundant data.
+ let esds = vec![
+ 3, 25, 0, 1, 0, 4, 19, 64, 21, 0, 0, 0, 0, 0, 0, 0, 0, 1, 119, 0, 5, 2, 18, 16, 6, 1, 2,
+ ];
+
+ let mut stream = make_box(BoxSize::Auto, b"esds", |s| {
+ s.B32(0) // reserved
+ .append_bytes(esds.as_slice())
+ });
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+
+ match super::read_esds(&mut stream) {
+ Ok(esds) => assert_eq!(esds.audio_codec, super::CodecType::AAC),
+ _ => panic!("unexpected result with invalid descriptor"),
+ }
+}
+
+#[test]
+fn read_stsd_lpcm() {
+ // Extract from sample converted by ffmpeg.
+ // "ffmpeg -i ./gizmo-short.mp4 -acodec pcm_s16le -ar 96000 -vcodec copy -f mov gizmo-short.mov"
+ let lpcm = vec![
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x03, 0x00, 0x10, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x48, 0x40, 0xf7, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x7f,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x02,
+ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x63, 0x68, 0x61, 0x6e, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x64, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ ];
+
+ let mut stream = make_box(BoxSize::Auto, b"lpcm", |s| s.append_bytes(lpcm.as_slice()));
+ let mut iter = super::BoxIter::new(&mut stream);
+ let mut stream = iter.next_box().unwrap().unwrap();
+
+ let sample_entry = super::read_audio_sample_entry(&mut stream).unwrap();
+
+ match sample_entry {
+ #[allow(clippy::float_cmp)] // The float comparison below is valid and intended.
+ super::SampleEntry::Audio(a) => {
+ assert_eq!(a.codec_type, super::CodecType::LPCM);
+ assert_eq!(a.samplerate, 96000.0);
+ assert_eq!(a.channelcount, 1);
+ match a.codec_specific {
+ super::AudioCodecSpecific::LPCM => (),
+ _ => panic!("it should be LPCM!"),
+ }
+ }
+ _ => panic!("it should be a audio sample entry!"),
+ }
+}
+
+#[test]
+fn read_to_end_() {
+ let mut src = b"1234567890".take(5);
+ let buf = src.read_into_try_vec().unwrap();
+ assert_eq!(buf.len(), 5);
+ assert_eq!(buf, b"12345".as_ref());
+}
+
+#[test]
+fn read_to_end_oom() {
+ let mut src = b"1234567890".take(std::usize::MAX.try_into().expect("usize < u64"));
+ assert!(src.read_into_try_vec().is_err());
+}
--- /dev/null
+/// Check if needed fields are still public.
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+extern crate mp4parse as mp4;
+
+use mp4::Error;
+use mp4::ParseStrictness;
+use std::convert::TryInto;
+use std::fs::File;
+use std::io::{Cursor, Read, Seek, SeekFrom};
+
+static MINI_MP4: &str = "tests/minimal.mp4";
+static MINI_MP4_WITH_METADATA: &str = "tests/metadata.mp4";
+static MINI_MP4_WITH_METADATA_STD_GENRE: &str = "tests/metadata_gnre.mp4";
+
+static AUDIO_EME_CENC_MP4: &str = "tests/bipbop-cenc-audioinit.mp4";
+static VIDEO_EME_CENC_MP4: &str = "tests/bipbop_480wp_1001kbps-cenc-video-key1-init.mp4";
+// The cbcs files were created via shaka-packager from Firefox's test suite's bipbop.mp4 using:
+// packager-win.exe
+// in=bipbop.mp4,stream=audio,init_segment=bipbop_cbcs_audio_init.mp4,segment_template=bipbop_cbcs_audio_$Number$.m4s
+// in=bipbop.mp4,stream=video,init_segment=bipbop_cbcs_video_init.mp4,segment_template=bipbop_cbcs_video_$Number$.m4s
+// --protection_scheme cbcs --enable_raw_key_encryption
+// --keys label=:key_id=7e571d047e571d047e571d047e571d21:key=7e5744447e5744447e5744447e574421
+// --iv 11223344556677889900112233445566
+// --generate_static_mpd --mpd_output bipbop_cbcs.mpd
+// note: only the init files are needed for these tests
+static AUDIO_EME_CBCS_MP4: &str = "tests/bipbop_cbcs_audio_init.mp4";
+static VIDEO_EME_CBCS_MP4: &str = "tests/bipbop_cbcs_video_init.mp4";
+static VIDEO_AV1_MP4: &str = "tests/tiny_av1.mp4";
+// This file contains invalid userdata in its copyright userdata. See
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1687357 for more information.
+static VIDEO_INVALID_USERDATA: &str = "tests/invalid_userdata.mp4";
+static IMAGE_AVIF: &str = "tests/valid.avif";
+static IMAGE_AVIF_EXTENTS: &str = "tests/multiple-extents.avif";
+static IMAGE_AVIF_ALPHA: &str = "tests/valid-alpha.avif";
+static IMAGE_AVIF_ALPHA_PREMULTIPLIED: &str = "tests/1x1-black-alpha-50pct-premultiplied.avif";
+static IMAGE_AVIF_CORRUPT: &str = "tests/corrupt/bug-1655846.avif";
+static IMAGE_AVIF_CORRUPT_2: &str = "tests/corrupt/bug-1661347.avif";
+static IMAGE_AVIF_IPMA_BAD_VERSION: &str = "tests/bad-ipma-version.avif";
+static IMAGE_AVIF_IPMA_BAD_FLAGS: &str = "tests/bad-ipma-flags.avif";
+static IMAGE_AVIF_IPMA_DUPLICATE_VERSION_AND_FLAGS: &str =
+ "tests/corrupt/ipma-duplicate-version-and-flags.avif";
+static IMAGE_AVIF_IPMA_DUPLICATE_ITEM_ID: &str = "tests/corrupt/ipma-duplicate-item_id.avif";
+static IMAGE_AVIF_IPMA_INVALID_PROPERTY_INDEX: &str =
+ "tests/corrupt/ipma-invalid-property-index.avif";
+static IMAGE_AVIF_NO_HDLR: &str = "tests/corrupt/hdlr-not-first.avif";
+static IMAGE_AVIF_HDLR_NOT_FIRST: &str = "tests/corrupt/no-hdlr.avif";
+static IMAGE_AVIF_HDLR_NOT_PICT: &str = "tests/corrupt/hdlr-not-pict.avif";
+static IMAGE_AVIF_HDLR_NONZERO_RESERVED: &str = "tests/hdlr-nonzero-reserved.avif";
+static IMAGE_AVIF_NO_MIF1: &str = "tests/no-mif1.avif";
+static IMAGE_AVIF_NO_PIXI: &str = "tests/corrupt/no-pixi.avif";
+static IMAGE_AVIF_NO_AV1C: &str = "tests/corrupt/no-av1C.avif";
+static IMAGE_AVIF_NO_ISPE: &str = "tests/corrupt/no-ispe.avif";
+static IMAGE_AVIF_NO_ALPHA_AV1C: &str = "tests/corrupt/no-alpha-av1C.avif";
+static IMAGE_AVIF_NO_ALPHA_PIXI: &str = "tests/corrupt/no-pixi-for-alpha.avif";
+static IMAGE_AVIF_AV1C_MISSING_ESSENTIAL: &str = "tests/av1C-missing-essential.avif";
+static IMAGE_AVIF_IMIR_MISSING_ESSENTIAL: &str = "tests/imir-missing-essential.avif";
+static IMAGE_AVIF_IROT_MISSING_ESSENTIAL: &str = "tests/irot-missing-essential.avif";
+static AVIF_TEST_DIRS: &[&str] = &["tests", "av1-avif/testFiles"];
+static AVIF_A1OP: &str =
+ "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_a1op.avif";
+static AVIF_A1LX: &str =
+ "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_grid_a1lx.avif";
+static AVIF_CLAP: &str = "tests/clap-basic-1_3x3-to-1x1.avif";
+static AVIF_GRID: &str = "av1-avif/testFiles/Microsoft/Summer_in_Tomsk_720p_5x4_grid.avif";
+static AVIF_LSEL: &str =
+ "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_lsel.avif";
+static AVIF_NO_PIXI_IMAGES: &[&str] = &[IMAGE_AVIF_NO_PIXI, IMAGE_AVIF_NO_ALPHA_PIXI];
+static AVIF_UNSUPPORTED_IMAGES: &[&str] = &[
+ AVIF_A1OP,
+ AVIF_A1LX,
+ AVIF_CLAP,
+ AVIF_GRID,
+ AVIF_LSEL,
+ "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_a1lx.avif",
+ "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_a1op_lsel.avif",
+ "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_grid_lsel.avif",
+ "av1-avif/testFiles/Xiph/abandoned_filmgrain.avif",
+ "av1-avif/testFiles/Xiph/fruits_2layer_thumbsize.avif",
+ "av1-avif/testFiles/Xiph/quebec_3layer_op2.avif",
+ "av1-avif/testFiles/Xiph/tiger_3layer_1res.avif",
+ "av1-avif/testFiles/Xiph/tiger_3layer_3res.avif",
+];
+/// See https://github.com/AOMediaCodec/av1-avif/issues/150
+static AV1_AVIF_CORRUPT_IMAGES: &[&str] = &[
+ "av1-avif/testFiles/Microsoft/Chimera_10bit_cropped_to_1920x1008.avif",
+ "av1-avif/testFiles/Microsoft/Chimera_10bit_cropped_to_1920x1008_with_HDR_metadata.avif",
+ "av1-avif/testFiles/Microsoft/Chimera_8bit_cropped_480x256.avif",
+ "av1-avif/testFiles/Link-U/kimono.crop.avif",
+ "av1-avif/testFiles/Link-U/kimono.mirror-vertical.rotate270.crop.avif",
+ "av1-avif/testFiles/Netflix/avis/Chimera-AV1-10bit-480x270.avif",
+];
+static AVIF_CORRUPT_IMAGES_DIR: &str = "tests/corrupt";
+// The 1 frame h263 3gp file can be generated by ffmpeg with command
+// "ffmpeg -i [input file] -f 3gp -vcodec h263 -vf scale=176x144 -frames:v 1 -an output.3gp"
+static VIDEO_H263_3GP: &str = "tests/bbb_sunflower_QCIF_30fps_h263_noaudio_1f.3gp";
+// The 1 frame AMR-NB 3gp file can be generated by ffmpeg with command
+// "ffmpeg -i [input file] -f 3gp -acodec amr_nb -ar 8000 -ac 1 -frames:a 1 -vn output.3gp"
+#[cfg(feature = "3gpp")]
+static AUDIO_AMRNB_3GP: &str = "tests/amr_nb_1f.3gp";
+// The 1 frame AMR-WB 3gp file can be generated by ffmpeg with command
+// "ffmpeg -i [input file] -f 3gp -acodec amr_wb -ar 16000 -ac 1 -frames:a 1 -vn output.3gp"
+#[cfg(feature = "3gpp")]
+static AUDIO_AMRWB_3GP: &str = "tests/amr_wb_1f.3gp";
+#[cfg(feature = "mp4v")]
+// The 1 frame mp4v mp4 file can be generated by ffmpeg with command
+// "ffmpeg -i [input file] -f mp4 -c:v mpeg4 -vf scale=176x144 -frames:v 1 -an output.mp4"
+static VIDEO_MP4V_MP4: &str = "tests/bbb_sunflower_QCIF_30fps_mp4v_noaudio_1f.mp4";
+
+// Adapted from https://github.com/GuillaumeGomez/audio-video-metadata/blob/9dff40f565af71d5502e03a2e78ae63df95cfd40/src/metadata.rs#L53
+#[test]
+fn public_api() {
+ let mut fd = File::open(MINI_MP4).expect("Unknown file");
+ let mut buf = Vec::new();
+ fd.read_to_end(&mut buf).expect("File error");
+
+ let mut c = Cursor::new(&buf);
+ let context = mp4::read_mp4(&mut c).expect("read_mp4 failed");
+ assert_eq!(context.timescale, Some(mp4::MediaTimeScale(1000)));
+ for track in context.tracks {
+ match track.track_type {
+ mp4::TrackType::Video => {
+ // track part
+ assert_eq!(track.duration, Some(mp4::TrackScaledTime(512, 0)));
+ assert_eq!(track.empty_duration, Some(mp4::MediaScaledTime(0)));
+ assert_eq!(track.media_time, Some(mp4::TrackScaledTime(0, 0)));
+ assert_eq!(track.timescale, Some(mp4::TrackTimeScale(12800, 0)));
+
+ // track.tkhd part
+ let tkhd = track.tkhd.unwrap();
+ assert!(!tkhd.disabled);
+ assert_eq!(tkhd.duration, 40);
+ assert_eq!(tkhd.width, 20_971_520);
+ assert_eq!(tkhd.height, 15_728_640);
+
+ // track.stsd part
+ let stsd = track.stsd.expect("expected an stsd");
+ let v = match stsd.descriptions.first().expect("expected a SampleEntry") {
+ mp4::SampleEntry::Video(v) => v,
+ _ => panic!("expected a VideoSampleEntry"),
+ };
+ assert_eq!(v.width, 320);
+ assert_eq!(v.height, 240);
+ assert_eq!(
+ match v.codec_specific {
+ mp4::VideoCodecSpecific::AVCConfig(ref avc) => {
+ assert!(!avc.is_empty());
+ "AVC"
+ }
+ mp4::VideoCodecSpecific::VPxConfig(ref vpx) => {
+ // We don't enter in here, we just check if fields are public.
+ assert!(vpx.bit_depth > 0);
+ assert!(vpx.colour_primaries > 0);
+ assert!(vpx.chroma_subsampling > 0);
+ assert!(!vpx.codec_init.is_empty());
+ "VPx"
+ }
+ mp4::VideoCodecSpecific::ESDSConfig(ref mp4v) => {
+ assert!(!mp4v.is_empty());
+ "MP4V"
+ }
+ mp4::VideoCodecSpecific::AV1Config(ref _av1c) => {
+ "AV1"
+ }
+ mp4::VideoCodecSpecific::H263Config(ref _h263) => {
+ "H263"
+ }
+ },
+ "AVC"
+ );
+ }
+ mp4::TrackType::Audio => {
+ // track part
+ assert_eq!(track.duration, Some(mp4::TrackScaledTime(2944, 1)));
+ assert_eq!(track.empty_duration, Some(mp4::MediaScaledTime(0)));
+ assert_eq!(track.media_time, Some(mp4::TrackScaledTime(1024, 1)));
+ assert_eq!(track.timescale, Some(mp4::TrackTimeScale(48000, 1)));
+
+ // track.tkhd part
+ let tkhd = track.tkhd.unwrap();
+ assert!(!tkhd.disabled);
+ assert_eq!(tkhd.duration, 62);
+ assert_eq!(tkhd.width, 0);
+ assert_eq!(tkhd.height, 0);
+
+ // track.stsd part
+ let stsd = track.stsd.expect("expected an stsd");
+ let a = match stsd.descriptions.first().expect("expected a SampleEntry") {
+ mp4::SampleEntry::Audio(a) => a,
+ _ => panic!("expected a AudioSampleEntry"),
+ };
+ assert_eq!(
+ match a.codec_specific {
+ mp4::AudioCodecSpecific::ES_Descriptor(ref esds) => {
+ assert_eq!(esds.audio_codec, mp4::CodecType::AAC);
+ assert_eq!(esds.audio_sample_rate.unwrap(), 48000);
+ assert_eq!(esds.audio_object_type.unwrap(), 2);
+ "ES"
+ }
+ mp4::AudioCodecSpecific::FLACSpecificBox(ref flac) => {
+ // STREAMINFO block must be present and first.
+ assert!(!flac.blocks.is_empty());
+ assert_eq!(flac.blocks[0].block_type, 0);
+ assert_eq!(flac.blocks[0].data.len(), 34);
+ "FLAC"
+ }
+ mp4::AudioCodecSpecific::OpusSpecificBox(ref opus) => {
+ // We don't enter in here, we just check if fields are public.
+ assert!(opus.version > 0);
+ "Opus"
+ }
+ mp4::AudioCodecSpecific::ALACSpecificBox(ref alac) => {
+ assert!(alac.data.len() == 24 || alac.data.len() == 48);
+ "ALAC"
+ }
+ mp4::AudioCodecSpecific::MP3 => {
+ "MP3"
+ }
+ mp4::AudioCodecSpecific::LPCM => {
+ "LPCM"
+ }
+ #[cfg(feature = "3gpp")]
+ mp4::AudioCodecSpecific::AMRSpecificBox(_) => {
+ "AMR"
+ }
+ },
+ "ES"
+ );
+ assert!(a.samplesize > 0);
+ assert!(a.samplerate > 0.0);
+ }
+ mp4::TrackType::Metadata | mp4::TrackType::Unknown => {}
+ }
+ }
+}
+
+#[test]
+fn public_metadata() {
+ let mut fd = File::open(MINI_MP4_WITH_METADATA).expect("Unknown file");
+ let mut buf = Vec::new();
+ fd.read_to_end(&mut buf).expect("File error");
+
+ let mut c = Cursor::new(&buf);
+ let context = mp4::read_mp4(&mut c).expect("read_mp4 failed");
+ let udta = context
+ .userdata
+ .expect("didn't find udta")
+ .expect("failed to parse udta");
+ let meta = udta.meta.expect("didn't find meta");
+ assert_eq!(meta.title.unwrap(), "Title");
+ assert_eq!(meta.artist.unwrap(), "Artist");
+ assert_eq!(meta.album_artist.unwrap(), "Album Artist");
+ assert_eq!(meta.comment.unwrap(), "Comments");
+ assert_eq!(meta.year.unwrap(), "2019");
+ assert_eq!(
+ meta.genre.unwrap(),
+ mp4::Genre::CustomGenre("Custom Genre".try_into().unwrap())
+ );
+ assert_eq!(meta.encoder.unwrap(), "Lavf56.40.101");
+ assert_eq!(meta.encoded_by.unwrap(), "Encoded-by");
+ assert_eq!(meta.copyright.unwrap(), "Copyright");
+ assert_eq!(meta.track_number.unwrap(), 3);
+ assert_eq!(meta.total_tracks.unwrap(), 6);
+ assert_eq!(meta.disc_number.unwrap(), 5);
+ assert_eq!(meta.total_discs.unwrap(), 10);
+ assert_eq!(meta.beats_per_minute.unwrap(), 128);
+ assert_eq!(meta.composer.unwrap(), "Composer");
+ assert!(meta.compilation.unwrap());
+ assert!(!meta.gapless_playback.unwrap());
+ assert!(!meta.podcast.unwrap());
+ assert_eq!(meta.advisory.unwrap(), mp4::AdvisoryRating::Clean);
+ assert_eq!(meta.media_type.unwrap(), mp4::MediaType::Normal);
+ assert_eq!(meta.rating.unwrap(), "50");
+ assert_eq!(meta.grouping.unwrap(), "Grouping");
+ assert_eq!(meta.category.unwrap(), "Category");
+ assert_eq!(meta.keyword.unwrap(), "Keyword");
+ assert_eq!(meta.description.unwrap(), "Description");
+ assert_eq!(meta.lyrics.unwrap(), "Lyrics");
+ assert_eq!(meta.long_description.unwrap(), "Long Description");
+ assert_eq!(meta.tv_episode_name.unwrap(), "Episode Name");
+ assert_eq!(meta.tv_network_name.unwrap(), "Network Name");
+ assert_eq!(meta.tv_episode_number.unwrap(), 15);
+ assert_eq!(meta.tv_season.unwrap(), 10);
+ assert_eq!(meta.tv_show_name.unwrap(), "Show Name");
+ assert!(meta.hd_video.unwrap());
+ assert_eq!(meta.owner.unwrap(), "Owner");
+ assert_eq!(meta.sort_name.unwrap(), "Sort Name");
+ assert_eq!(meta.sort_album.unwrap(), "Sort Album");
+ assert_eq!(meta.sort_artist.unwrap(), "Sort Artist");
+ assert_eq!(meta.sort_album_artist.unwrap(), "Sort Album Artist");
+ assert_eq!(meta.sort_composer.unwrap(), "Sort Composer");
+
+ // Check for valid JPEG header
+ let covers = meta.cover_art.unwrap();
+ let cover = &covers[0];
+ let mut bytes = [0u8; 4];
+ bytes[0] = cover[0];
+ bytes[1] = cover[1];
+ bytes[2] = cover[2];
+ assert_eq!(u32::from_le_bytes(bytes), 0x00ff_d8ff);
+}
+
+#[test]
+fn public_metadata_gnre() {
+ let mut fd = File::open(MINI_MP4_WITH_METADATA_STD_GENRE).expect("Unknown file");
+ let mut buf = Vec::new();
+ fd.read_to_end(&mut buf).expect("File error");
+
+ let mut c = Cursor::new(&buf);
+ let context = mp4::read_mp4(&mut c).expect("read_mp4 failed");
+ let udta = context
+ .userdata
+ .expect("didn't find udta")
+ .expect("failed to parse udta");
+ let meta = udta.meta.expect("didn't find meta");
+ assert_eq!(meta.title.unwrap(), "Title");
+ assert_eq!(meta.artist.unwrap(), "Artist");
+ assert_eq!(meta.album_artist.unwrap(), "Album Artist");
+ assert_eq!(meta.comment.unwrap(), "Comments");
+ assert_eq!(meta.year.unwrap(), "2019");
+ assert_eq!(meta.genre.unwrap(), mp4::Genre::StandardGenre(3));
+ assert_eq!(meta.encoder.unwrap(), "Lavf56.40.101");
+ assert_eq!(meta.encoded_by.unwrap(), "Encoded-by");
+ assert_eq!(meta.copyright.unwrap(), "Copyright");
+ assert_eq!(meta.track_number.unwrap(), 3);
+ assert_eq!(meta.total_tracks.unwrap(), 6);
+ assert_eq!(meta.disc_number.unwrap(), 5);
+ assert_eq!(meta.total_discs.unwrap(), 10);
+ assert_eq!(meta.beats_per_minute.unwrap(), 128);
+ assert_eq!(meta.composer.unwrap(), "Composer");
+ assert!(meta.compilation.unwrap());
+ assert!(!meta.gapless_playback.unwrap());
+ assert!(!meta.podcast.unwrap());
+ assert_eq!(meta.advisory.unwrap(), mp4::AdvisoryRating::Clean);
+ assert_eq!(meta.media_type.unwrap(), mp4::MediaType::Normal);
+ assert_eq!(meta.rating.unwrap(), "50");
+ assert_eq!(meta.grouping.unwrap(), "Grouping");
+ assert_eq!(meta.category.unwrap(), "Category");
+ assert_eq!(meta.keyword.unwrap(), "Keyword");
+ assert_eq!(meta.description.unwrap(), "Description");
+ assert_eq!(meta.lyrics.unwrap(), "Lyrics");
+ assert_eq!(meta.long_description.unwrap(), "Long Description");
+ assert_eq!(meta.tv_episode_name.unwrap(), "Episode Name");
+ assert_eq!(meta.tv_network_name.unwrap(), "Network Name");
+ assert_eq!(meta.tv_episode_number.unwrap(), 15);
+ assert_eq!(meta.tv_season.unwrap(), 10);
+ assert_eq!(meta.tv_show_name.unwrap(), "Show Name");
+ assert!(meta.hd_video.unwrap());
+ assert_eq!(meta.owner.unwrap(), "Owner");
+ assert_eq!(meta.sort_name.unwrap(), "Sort Name");
+ assert_eq!(meta.sort_album.unwrap(), "Sort Album");
+ assert_eq!(meta.sort_artist.unwrap(), "Sort Artist");
+ assert_eq!(meta.sort_album_artist.unwrap(), "Sort Album Artist");
+ assert_eq!(meta.sort_composer.unwrap(), "Sort Composer");
+
+ // Check for valid JPEG header
+ let covers = meta.cover_art.unwrap();
+ let cover = &covers[0];
+ let mut bytes = [0u8; 4];
+ bytes[0] = cover[0];
+ bytes[1] = cover[1];
+ bytes[2] = cover[2];
+ assert_eq!(u32::from_le_bytes(bytes), 0x00ff_d8ff);
+}
+
+#[test]
+fn public_invalid_metadata() {
+ // Test that reading userdata containing invalid metadata is not fatal to parsing and that
+ // expected values are still found.
+ let mut fd = File::open(VIDEO_INVALID_USERDATA).expect("Unknown file");
+ let mut buf = Vec::new();
+ fd.read_to_end(&mut buf).expect("File error");
+
+ let mut c = Cursor::new(&buf);
+ let context = mp4::read_mp4(&mut c).expect("read_mp4 failed");
+ // Should have userdata.
+ assert!(context.userdata.is_some());
+ // But it should contain an error.
+ assert!(context.userdata.unwrap().is_err());
+ // Smoke test that other data has been parsed. Don't check everything, just make sure some
+ // values are as expected.
+ assert_eq!(context.tracks.len(), 2);
+ for track in context.tracks {
+ match track.track_type {
+ mp4::TrackType::Video => {
+ // Check some of the values in the video tkhd.
+ let tkhd = track.tkhd.unwrap();
+ assert!(!tkhd.disabled);
+ assert_eq!(tkhd.duration, 231232);
+ assert_eq!(tkhd.width, 83_886_080);
+ assert_eq!(tkhd.height, 47_185_920);
+ }
+ mp4::TrackType::Audio => {
+ // Check some of the values in the audio tkhd.
+ let tkhd = track.tkhd.unwrap();
+ assert!(!tkhd.disabled);
+ assert_eq!(tkhd.duration, 231338);
+ assert_eq!(tkhd.width, 0);
+ assert_eq!(tkhd.height, 0);
+ }
+ _ => panic!("File should not contain other tracks."),
+ }
+ }
+}
+
+#[test]
+fn public_audio_tenc() {
+ let kid = vec![
+ 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d,
+ 0x04,
+ ];
+
+ let mut fd = File::open(AUDIO_EME_CENC_MP4).expect("Unknown file");
+ let mut buf = Vec::new();
+ fd.read_to_end(&mut buf).expect("File error");
+
+ let mut c = Cursor::new(&buf);
+ let context = mp4::read_mp4(&mut c).expect("read_mp4 failed");
+ for track in context.tracks {
+ let stsd = track.stsd.expect("expected an stsd");
+ let a = match stsd.descriptions.first().expect("expected a SampleEntry") {
+ mp4::SampleEntry::Audio(a) => a,
+ _ => panic!("expected a AudioSampleEntry"),
+ };
+ assert_eq!(a.codec_type, mp4::CodecType::EncryptedAudio);
+ match a.protection_info.iter().find(|sinf| sinf.tenc.is_some()) {
+ Some(p) => {
+ assert_eq!(p.original_format, b"mp4a");
+ if let Some(ref schm) = p.scheme_type {
+ assert_eq!(schm.scheme_type, b"cenc");
+ } else {
+ panic!("Expected scheme type info");
+ }
+ if let Some(ref tenc) = p.tenc {
+ assert!(tenc.is_encrypted > 0);
+ assert_eq!(tenc.iv_size, 16);
+ assert_eq!(tenc.kid, kid);
+ assert_eq!(tenc.crypt_byte_block_count, None);
+ assert_eq!(tenc.skip_byte_block_count, None);
+ assert_eq!(tenc.constant_iv, None);
+ } else {
+ panic!("Invalid test condition");
+ }
+ }
+ _ => {
+ panic!("Invalid test condition");
+ }
+ }
+ }
+}
+
+#[test]
+fn public_video_cenc() {
+ let system_id = vec![
+ 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb,
+ 0x4b,
+ ];
+
+ let kid = vec![
+ 0x7e, 0x57, 0x1d, 0x03, 0x7e, 0x57, 0x1d, 0x03, 0x7e, 0x57, 0x1d, 0x03, 0x7e, 0x57, 0x1d,
+ 0x11,
+ ];
+
+ let pssh_box = vec![
+ 0x00, 0x00, 0x00, 0x34, 0x70, 0x73, 0x73, 0x68, 0x01, 0x00, 0x00, 0x00, 0x10, 0x77, 0xef,
+ 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b, 0x00, 0x00,
+ 0x00, 0x01, 0x7e, 0x57, 0x1d, 0x03, 0x7e, 0x57, 0x1d, 0x03, 0x7e, 0x57, 0x1d, 0x03, 0x7e,
+ 0x57, 0x1d, 0x11, 0x00, 0x00, 0x00, 0x00,
+ ];
+
+ let mut fd = File::open(VIDEO_EME_CENC_MP4).expect("Unknown file");
+ let mut buf = Vec::new();
+ fd.read_to_end(&mut buf).expect("File error");
+
+ let mut c = Cursor::new(&buf);
+ let context = mp4::read_mp4(&mut c).expect("read_mp4 failed");
+ for track in context.tracks {
+ let stsd = track.stsd.expect("expected an stsd");
+ let v = match stsd.descriptions.first().expect("expected a SampleEntry") {
+ mp4::SampleEntry::Video(ref v) => v,
+ _ => panic!("expected a VideoSampleEntry"),
+ };
+ assert_eq!(v.codec_type, mp4::CodecType::EncryptedVideo);
+ match v.protection_info.iter().find(|sinf| sinf.tenc.is_some()) {
+ Some(p) => {
+ assert_eq!(p.original_format, b"avc1");
+ if let Some(ref schm) = p.scheme_type {
+ assert_eq!(schm.scheme_type, b"cenc");
+ } else {
+ panic!("Expected scheme type info");
+ }
+ if let Some(ref tenc) = p.tenc {
+ assert!(tenc.is_encrypted > 0);
+ assert_eq!(tenc.iv_size, 16);
+ assert_eq!(tenc.kid, kid);
+ assert_eq!(tenc.crypt_byte_block_count, None);
+ assert_eq!(tenc.skip_byte_block_count, None);
+ assert_eq!(tenc.constant_iv, None);
+ } else {
+ panic!("Invalid test condition");
+ }
+ }
+ _ => {
+ panic!("Invalid test condition");
+ }
+ }
+ }
+
+ for pssh in context.psshs {
+ assert_eq!(pssh.system_id, system_id);
+ for kid_id in pssh.kid {
+ assert_eq!(kid_id, kid);
+ }
+ assert!(pssh.data.is_empty());
+ assert_eq!(pssh.box_content, pssh_box);
+ }
+}
+
+#[test]
+fn public_audio_cbcs() {
+ let system_id = vec![
+ 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb,
+ 0x4b,
+ ];
+
+ let kid = vec![
+ 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d,
+ 0x21,
+ ];
+
+ let default_iv = vec![
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55,
+ 0x66,
+ ];
+
+ let pssh_box = vec![
+ 0x00, 0x00, 0x00, 0x34, 0x70, 0x73, 0x73, 0x68, 0x01, 0x00, 0x00, 0x00, 0x10, 0x77, 0xef,
+ 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b, 0x00, 0x00,
+ 0x00, 0x01, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e,
+ 0x57, 0x1d, 0x21, 0x00, 0x00, 0x00, 0x00,
+ ];
+
+ let mut fd = File::open(AUDIO_EME_CBCS_MP4).expect("Unknown file");
+ let mut buf = Vec::new();
+ fd.read_to_end(&mut buf).expect("File error");
+
+ let mut c = Cursor::new(&buf);
+ let context = mp4::read_mp4(&mut c).expect("read_mp4 failed");
+ for track in context.tracks {
+ let stsd = track.stsd.expect("expected an stsd");
+ assert_eq!(stsd.descriptions.len(), 2);
+ let mut found_encrypted_sample_description = false;
+ for description in stsd.descriptions {
+ match description {
+ mp4::SampleEntry::Audio(ref a) => {
+ if let Some(p) = a.protection_info.iter().find(|sinf| sinf.tenc.is_some()) {
+ found_encrypted_sample_description = true;
+ assert_eq!(p.original_format, b"mp4a");
+ if let Some(ref schm) = p.scheme_type {
+ assert_eq!(schm.scheme_type, b"cbcs");
+ } else {
+ panic!("Expected scheme type info");
+ }
+ if let Some(ref tenc) = p.tenc {
+ assert!(tenc.is_encrypted > 0);
+ assert_eq!(tenc.iv_size, 0);
+ assert_eq!(tenc.kid, kid);
+ // Note: 0 for both crypt and skip seems odd but
+ // that's what shaka-packager produced. It appears
+ // to indicate full encryption.
+ assert_eq!(tenc.crypt_byte_block_count, Some(0));
+ assert_eq!(tenc.skip_byte_block_count, Some(0));
+ assert_eq!(tenc.constant_iv, Some(default_iv.clone().into()));
+ } else {
+ panic!("Invalid test condition");
+ }
+ }
+ }
+ _ => {
+ panic!("expected a VideoSampleEntry");
+ }
+ }
+ }
+ assert!(
+ found_encrypted_sample_description,
+ "Should have found an encrypted sample description"
+ );
+ }
+
+ for pssh in context.psshs {
+ assert_eq!(pssh.system_id, system_id);
+ for kid_id in pssh.kid {
+ assert_eq!(kid_id, kid);
+ }
+ assert!(pssh.data.is_empty());
+ assert_eq!(pssh.box_content, pssh_box);
+ }
+}
+
+#[test]
+fn public_video_cbcs() {
+ let system_id = vec![
+ 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb,
+ 0x4b,
+ ];
+
+ let kid = vec![
+ 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d,
+ 0x21,
+ ];
+
+ let default_iv = vec![
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55,
+ 0x66,
+ ];
+
+ let pssh_box = vec![
+ 0x00, 0x00, 0x00, 0x34, 0x70, 0x73, 0x73, 0x68, 0x01, 0x00, 0x00, 0x00, 0x10, 0x77, 0xef,
+ 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b, 0x00, 0x00,
+ 0x00, 0x01, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e,
+ 0x57, 0x1d, 0x21, 0x00, 0x00, 0x00, 0x00,
+ ];
+
+ let mut fd = File::open(VIDEO_EME_CBCS_MP4).expect("Unknown file");
+ let mut buf = Vec::new();
+ fd.read_to_end(&mut buf).expect("File error");
+
+ let mut c = Cursor::new(&buf);
+ let context = mp4::read_mp4(&mut c).expect("read_mp4 failed");
+ for track in context.tracks {
+ let stsd = track.stsd.expect("expected an stsd");
+ assert_eq!(stsd.descriptions.len(), 2);
+ let mut found_encrypted_sample_description = false;
+ for description in stsd.descriptions {
+ match description {
+ mp4::SampleEntry::Video(ref v) => {
+ assert_eq!(v.width, 400);
+ assert_eq!(v.height, 300);
+ if let Some(p) = v.protection_info.iter().find(|sinf| sinf.tenc.is_some()) {
+ found_encrypted_sample_description = true;
+ assert_eq!(p.original_format, b"avc1");
+ if let Some(ref schm) = p.scheme_type {
+ assert_eq!(schm.scheme_type, b"cbcs");
+ } else {
+ panic!("Expected scheme type info");
+ }
+ if let Some(ref tenc) = p.tenc {
+ assert!(tenc.is_encrypted > 0);
+ assert_eq!(tenc.iv_size, 0);
+ assert_eq!(tenc.kid, kid);
+ assert_eq!(tenc.crypt_byte_block_count, Some(1));
+ assert_eq!(tenc.skip_byte_block_count, Some(9));
+ assert_eq!(tenc.constant_iv, Some(default_iv.clone().into()));
+ } else {
+ panic!("Invalid test condition");
+ }
+ }
+ }
+ _ => {
+ panic!("expected a VideoSampleEntry");
+ }
+ }
+ }
+ assert!(
+ found_encrypted_sample_description,
+ "Should have found an encrypted sample description"
+ );
+ }
+
+ for pssh in context.psshs {
+ assert_eq!(pssh.system_id, system_id);
+ for kid_id in pssh.kid {
+ assert_eq!(kid_id, kid);
+ }
+ assert!(pssh.data.is_empty());
+ assert_eq!(pssh.box_content, pssh_box);
+ }
+}
+
+#[test]
+fn public_video_av1() {
+ let mut fd = File::open(VIDEO_AV1_MP4).expect("Unknown file");
+ let mut buf = Vec::new();
+ fd.read_to_end(&mut buf).expect("File error");
+
+ let mut c = Cursor::new(&buf);
+ let context = mp4::read_mp4(&mut c).expect("read_mp4 failed");
+ for track in context.tracks {
+ // track part
+ assert_eq!(track.duration, Some(mp4::TrackScaledTime(512, 0)));
+ assert_eq!(track.empty_duration, Some(mp4::MediaScaledTime(0)));
+ assert_eq!(track.media_time, Some(mp4::TrackScaledTime(0, 0)));
+ assert_eq!(track.timescale, Some(mp4::TrackTimeScale(12288, 0)));
+
+ // track.tkhd part
+ let tkhd = track.tkhd.unwrap();
+ assert!(!tkhd.disabled);
+ assert_eq!(tkhd.duration, 42);
+ assert_eq!(tkhd.width, 4_194_304);
+ assert_eq!(tkhd.height, 4_194_304);
+
+ // track.stsd part
+ let stsd = track.stsd.expect("expected an stsd");
+ let v = match stsd.descriptions.first().expect("expected a SampleEntry") {
+ mp4::SampleEntry::Video(ref v) => v,
+ _ => panic!("expected a VideoSampleEntry"),
+ };
+ assert_eq!(v.codec_type, mp4::CodecType::AV1);
+ assert_eq!(v.width, 64);
+ assert_eq!(v.height, 64);
+
+ match v.codec_specific {
+ mp4::VideoCodecSpecific::AV1Config(ref av1c) => {
+ // TODO: test av1c fields once ffmpeg is updated
+ assert_eq!(av1c.profile, 0);
+ assert_eq!(av1c.level, 0);
+ assert_eq!(av1c.tier, 0);
+ assert_eq!(av1c.bit_depth, 8);
+ assert!(!av1c.monochrome);
+ assert_eq!(av1c.chroma_subsampling_x, 1);
+ assert_eq!(av1c.chroma_subsampling_y, 1);
+ assert_eq!(av1c.chroma_sample_position, 0);
+ assert!(!av1c.initial_presentation_delay_present);
+ assert_eq!(av1c.initial_presentation_delay_minus_one, 0);
+ }
+ _ => panic!("Invalid test condition"),
+ }
+ }
+}
+
+#[test]
+fn public_mp4_bug_1185230() {
+ let input = &mut File::open("tests/test_case_1185230.mp4").expect("Unknown file");
+ let context = mp4::read_mp4(input).expect("read_mp4 failed");
+ let number_video_tracks = context
+ .tracks
+ .iter()
+ .filter(|t| t.track_type == mp4::TrackType::Video)
+ .count();
+ let number_audio_tracks = context
+ .tracks
+ .iter()
+ .filter(|t| t.track_type == mp4::TrackType::Audio)
+ .count();
+ assert_eq!(number_video_tracks, 2);
+ assert_eq!(number_audio_tracks, 2);
+}
+
+#[test]
+fn public_mp4_ctts_overflow() {
+ let input = &mut File::open("tests/clusterfuzz-testcase-minimized-mp4-6093954524250112")
+ .expect("Unknown file");
+ assert_invalid_data(mp4::read_mp4(input), "insufficient data in 'ctts' box");
+}
+
+#[test]
+fn public_avif_primary_item() {
+ let input = &mut File::open(IMAGE_AVIF).expect("Unknown file");
+ let context = mp4::read_avif(input, ParseStrictness::Normal).expect("read_avif failed");
+ assert_eq!(
+ context.primary_item_coded_data(),
+ [
+ 0x12, 0x00, 0x0A, 0x07, 0x38, 0x00, 0x06, 0x90, 0x20, 0x20, 0x69, 0x32, 0x0C, 0x16,
+ 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x79, 0x4C, 0xD2, 0x02
+ ]
+ );
+}
+
+#[test]
+fn public_avif_primary_item_split_extents() {
+ let input = &mut File::open(IMAGE_AVIF_EXTENTS).expect("Unknown file");
+ let context = mp4::read_avif(input, ParseStrictness::Normal).expect("read_avif failed");
+ assert_eq!(context.primary_item_coded_data().len(), 52);
+}
+
+#[test]
+fn public_avif_alpha_item() {
+ let input = &mut File::open(IMAGE_AVIF_ALPHA).expect("Unknown file");
+ assert_avif_valid(input);
+}
+
+#[test]
+fn public_avif_alpha_non_premultiplied() {
+ let input = &mut File::open(IMAGE_AVIF_ALPHA).expect("Unknown file");
+ let context = mp4::read_avif(input, ParseStrictness::Normal).expect("read_avif failed");
+ assert!(!context.alpha_item_coded_data().is_empty());
+ assert!(!context.premultiplied_alpha);
+}
+
+#[test]
+fn public_avif_alpha_premultiplied() {
+ let input = &mut File::open(IMAGE_AVIF_ALPHA_PREMULTIPLIED).expect("Unknown file");
+ let context = mp4::read_avif(input, ParseStrictness::Normal).expect("read_avif failed");
+ assert!(!context.alpha_item_coded_data().is_empty());
+ assert!(context.premultiplied_alpha);
+ assert_avif_valid(input);
+}
+
+#[test]
+fn public_avif_bug_1655846() {
+ let input = &mut File::open(IMAGE_AVIF_CORRUPT).expect("Unknown file");
+ assert!(mp4::read_avif(input, ParseStrictness::Normal).is_err());
+}
+
+#[test]
+fn public_avif_bug_1661347() {
+ let input = &mut File::open(IMAGE_AVIF_CORRUPT_2).expect("Unknown file");
+ assert!(mp4::read_avif(input, ParseStrictness::Normal).is_err());
+}
+
+fn assert_invalid_data<T: std::fmt::Debug>(result: mp4::Result<T>, expected_msg: &str) {
+ match result {
+ Err(Error::InvalidData(msg)) if msg == expected_msg => {}
+ Err(Error::InvalidData(msg)) if msg != expected_msg => {
+ panic!(
+ "Error message mismatch\nExpected: {}\nFound: {}",
+ expected_msg, msg
+ );
+ }
+ r => panic!(
+ "Expected Err(Error::InvalidData({:?}), found {:?}",
+ expected_msg, r
+ ),
+ }
+}
+
+/// Check that input generates no errors in any parsing mode
+fn assert_avif_valid(input: &mut File) {
+ for strictness in &[
+ ParseStrictness::Permissive,
+ ParseStrictness::Normal,
+ ParseStrictness::Strict,
+ ] {
+ input.seek(SeekFrom::Start(0)).expect("rewind failed");
+ assert!(
+ mp4::read_avif(input, *strictness).is_ok(),
+ "read_avif with {:?} failed",
+ strictness
+ );
+ println!("{:?} succeeded", strictness);
+ }
+}
+
+/// Check that input generates the expected error only in strict parsing mode
+fn assert_avif_should(path: &str, expected_msg: &str) {
+ let input = &mut File::open(path).expect("Unknown file");
+ assert_invalid_data(mp4::read_avif(input, ParseStrictness::Strict), expected_msg);
+ input.seek(SeekFrom::Start(0)).expect("rewind failed");
+ mp4::read_avif(input, ParseStrictness::Normal).expect("ParseStrictness::Normal failed");
+ input.seek(SeekFrom::Start(0)).expect("rewind failed");
+ mp4::read_avif(input, ParseStrictness::Permissive).expect("ParseStrictness::Permissive failed");
+}
+
+/// Check that input generates the expected error unless in permissive parsing mode
+fn assert_avif_shall(path: &str, expected_msg: &str) {
+ let input = &mut File::open(path).expect("Unknown file");
+ assert_invalid_data(mp4::read_avif(input, ParseStrictness::Strict), expected_msg);
+ input.seek(SeekFrom::Start(0)).expect("rewind failed");
+ assert_invalid_data(mp4::read_avif(input, ParseStrictness::Normal), expected_msg);
+ input.seek(SeekFrom::Start(0)).expect("rewind failed");
+ mp4::read_avif(input, ParseStrictness::Permissive).expect("ParseStrictness::Permissive failed");
+}
+
+#[test]
+fn public_avif_ipma_missing_essential() {
+ let expected_msg = "All transformative properties associated with \
+ coded and derived images required or conditionally \
+ required by this document shall be marked as essential \
+ per MIAF (ISO 23000-22:2019) § 7.3.9";
+ assert_avif_should(IMAGE_AVIF_AV1C_MISSING_ESSENTIAL, expected_msg);
+ assert_avif_should(IMAGE_AVIF_IMIR_MISSING_ESSENTIAL, expected_msg);
+ assert_avif_should(IMAGE_AVIF_IROT_MISSING_ESSENTIAL, expected_msg);
+}
+
+#[test]
+fn public_avif_ipma_bad_version() {
+ let expected_msg = "The ipma version 0 should be used unless 32-bit \
+ item_ID values are needed \
+ per ISOBMFF (ISO 14496-12:2020 § 8.11.14.1";
+ assert_avif_should(IMAGE_AVIF_IPMA_BAD_VERSION, expected_msg);
+}
+
+#[test]
+fn public_avif_ipma_bad_flags() {
+ let expected_msg = "Unless there are more than 127 properties in the \
+ ItemPropertyContainerBox, flags should be equal to 0 \
+ per ISOBMFF (ISO 14496-12:2020 § 8.11.14.1";
+ assert_avif_should(IMAGE_AVIF_IPMA_BAD_FLAGS, expected_msg);
+}
+
+#[test]
+fn public_avif_ipma_duplicate_version_and_flags() {
+ let expected_msg = "There shall be at most one ItemPropertyAssociationbox \
+ with a given pair of values of version and flags \
+ per ISOBMFF (ISO 14496-12:2020 § 8.11.14.1";
+ assert_avif_shall(IMAGE_AVIF_IPMA_DUPLICATE_VERSION_AND_FLAGS, expected_msg);
+}
+
+#[test]
+// TODO: convert this to a `assert_avif_shall` test to cover all `ParseStrictness` modes
+// that would require crafting an input that validly uses multiple ipma boxes,
+// which is kind of annoying to make pass the "should" requirements on flags and version
+// as well as the "shall" requirement on duplicate version and flags
+fn public_avif_ipma_duplicate_item_id() {
+ let expected_msg = "There shall be at most one occurrence of a given item_ID, \
+ in the set of ItemPropertyAssociationBox boxes \
+ per ISOBMFF (ISO 14496-12:2020) § 8.11.14.1";
+ let input = &mut File::open(IMAGE_AVIF_IPMA_DUPLICATE_ITEM_ID).expect("Unknown file");
+ assert_invalid_data(
+ mp4::read_avif(input, ParseStrictness::Permissive),
+ expected_msg,
+ )
+}
+
+#[test]
+fn public_avif_ipma_invalid_property_index() {
+ let expected_msg = "Invalid property index in ipma";
+ assert_avif_shall(IMAGE_AVIF_IPMA_INVALID_PROPERTY_INDEX, expected_msg);
+}
+
+#[test]
+fn public_avif_hdlr_first_in_meta() {
+ let expected_msg = "The HandlerBox shall be the first contained box within \
+ the MetaBox \
+ per MIAF (ISO 23000-22:2019) § 7.2.1.5";
+ assert_avif_shall(IMAGE_AVIF_NO_HDLR, expected_msg);
+ assert_avif_shall(IMAGE_AVIF_HDLR_NOT_FIRST, expected_msg);
+}
+
+#[test]
+fn public_avif_hdlr_is_pict() {
+ let expected_msg = "The HandlerBox handler_type must be 'pict' \
+ per MIAF (ISO 23000-22:2019) § 7.2.1.5";
+ assert_avif_shall(IMAGE_AVIF_HDLR_NOT_PICT, expected_msg);
+}
+
+#[test]
+fn public_avif_hdlr_nonzero_reserved() {
+ let expected_msg = "The HandlerBox 'reserved' fields shall be 0 \
+ per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2";
+ // This is a "should" despite the spec indicating a (somewhat ambiguous)
+ // requirement that this field is set to zero.
+ assert_avif_should(IMAGE_AVIF_HDLR_NONZERO_RESERVED, expected_msg);
+}
+
+#[test]
+fn public_avif_no_mif1() {
+ let expected_msg = "The FileTypeBox should contain 'mif1' in the compatible_brands list \
+ per MIAF (ISO 23000-22:2019) § 7.2.1.2";
+ assert_avif_should(IMAGE_AVIF_NO_MIF1, expected_msg);
+}
+
+#[test]
+fn public_avif_pixi_present_for_displayable_images() {
+ let expected_msg = "The pixel information property shall be associated with every image \
+ that is displayable (not hidden) \
+ per MIAF (ISO/IEC 23000-22:2019) specification § 7.3.6.6";
+ let pixi_test = if cfg!(feature = "missing-pixi-permitted") {
+ assert_avif_should
+ } else {
+ assert_avif_shall
+ };
+
+ pixi_test(IMAGE_AVIF_NO_PIXI, expected_msg);
+ pixi_test(IMAGE_AVIF_NO_ALPHA_PIXI, expected_msg);
+}
+
+#[test]
+fn public_avif_av1c_present_for_av01() {
+ let expected_msg = "One AV1 Item Configuration Property (av1C) \
+ is mandatory for an image item of type 'av01' \
+ per AVIF specification § 2.2.1";
+ assert_avif_shall(IMAGE_AVIF_NO_AV1C, expected_msg);
+ assert_avif_shall(IMAGE_AVIF_NO_ALPHA_AV1C, expected_msg);
+}
+
+#[test]
+fn public_avif_ispe_present() {
+ let expected_msg = "Missing 'ispe' property for primary item, required \
+ per HEIF (ISO/IEC 23008-12:2017) § 6.5.3.1";
+ assert_avif_shall(IMAGE_AVIF_NO_ISPE, expected_msg);
+}
+
+fn assert_unsupported<T: std::fmt::Debug>(result: mp4::Result<T>, expected_status: mp4::Status) {
+ match result {
+ Err(Error::UnsupportedDetail(status, _msg)) if status == expected_status => {}
+ Err(Error::UnsupportedDetail(status, _msg)) if status != expected_status => {
+ panic!(
+ "Error message mismatch\nExpected: {:?}\nFound: {:?}",
+ expected_status, status
+ );
+ }
+ r => panic!(
+ "Expected Err({:?}), found {:?}",
+ Error::from(expected_status),
+ r
+ ),
+ }
+}
+
+#[test]
+fn public_avif_a1lx() {
+ let input = &mut File::open(AVIF_A1LX).expect("Unknown file");
+ assert_unsupported(
+ mp4::read_avif(input, ParseStrictness::Normal),
+ mp4::Status::UnsupportedA1lx,
+ );
+}
+
+#[test]
+fn public_avif_a1op() {
+ let input = &mut File::open(AVIF_A1OP).expect("Unknown file");
+ assert_unsupported(
+ mp4::read_avif(input, ParseStrictness::Normal),
+ mp4::Status::UnsupportedA1op,
+ );
+}
+
+#[test]
+fn public_avif_clap() {
+ let input = &mut File::open(AVIF_CLAP).expect("Unknown file");
+ assert_unsupported(
+ mp4::read_avif(input, ParseStrictness::Normal),
+ mp4::Status::UnsupportedClap,
+ );
+}
+
+#[test]
+fn public_avif_grid() {
+ let input = &mut File::open(AVIF_GRID).expect("Unknown file");
+ assert_unsupported(
+ mp4::read_avif(input, ParseStrictness::Normal),
+ mp4::Status::UnsupportedGrid,
+ );
+}
+
+#[test]
+fn public_avif_lsel() {
+ let input = &mut File::open(AVIF_LSEL).expect("Unknown file");
+ assert_unsupported(
+ mp4::read_avif(input, ParseStrictness::Normal),
+ mp4::Status::UnsupportedLsel,
+ );
+}
+
+#[test]
+fn public_avif_read_samples() {
+ public_avif_read_samples_impl(ParseStrictness::Normal);
+}
+
+#[test]
+#[ignore] // See https://github.com/AOMediaCodec/av1-avif/issues/146
+fn public_avif_read_samples_strict() {
+ public_avif_read_samples_impl(ParseStrictness::Strict);
+}
+
+fn to_canonical_paths(strs: &[&str]) -> Vec<std::path::PathBuf> {
+ strs.iter()
+ .map(std::fs::canonicalize)
+ .map(Result::unwrap)
+ .collect()
+}
+
+fn public_avif_read_samples_impl(strictness: ParseStrictness) {
+ let corrupt_images = to_canonical_paths(AV1_AVIF_CORRUPT_IMAGES);
+ let unsupported_images = to_canonical_paths(AVIF_UNSUPPORTED_IMAGES);
+ let legal_no_pixi_images = if cfg!(feature = "missing-pixi-permitted") {
+ to_canonical_paths(AVIF_NO_PIXI_IMAGES)
+ } else {
+ vec![]
+ };
+ for dir in AVIF_TEST_DIRS {
+ for entry in walkdir::WalkDir::new(dir) {
+ let entry = entry.expect("AVIF entry");
+ let path = entry.path();
+ if !path.is_file() || path.extension().unwrap_or_default() != "avif" {
+ eprintln!("Skipping {:?}", path);
+ continue; // Skip directories, ReadMe.txt, etc.
+ }
+ let corrupt = (path.canonicalize().unwrap().parent().unwrap()
+ == std::fs::canonicalize(AVIF_CORRUPT_IMAGES_DIR).unwrap()
+ || corrupt_images.contains(&path.canonicalize().unwrap()))
+ && !legal_no_pixi_images.contains(&path.canonicalize().unwrap());
+
+ let unsupported = unsupported_images.contains(&path.canonicalize().unwrap());
+ println!(
+ "parsing {}{}{:?}",
+ if corrupt { "(corrupt) " } else { "" },
+ if unsupported { "(unsupported) " } else { "" },
+ path,
+ );
+ let input = &mut File::open(path).expect("Unknow file");
+ match mp4::read_avif(input, strictness) {
+ Ok(_) if unsupported || corrupt => panic!("Expected error parsing {:?}", path),
+ Ok(_) => eprintln!("Successfully parsed {:?}", path),
+ Err(e @ Error::Unsupported(_)) | Err(e @ Error::UnsupportedDetail(..))
+ if unsupported =>
+ {
+ eprintln!(
+ "Expected error parsing unsupported input {:?}: {:?}",
+ path, e
+ )
+ }
+ Err(e) if corrupt => {
+ eprintln!("Expected error parsing corrupt input {:?}: {:?}", path, e)
+ }
+ Err(e) => panic!("Unexected error parsing {:?}: {:?}", path, e),
+ }
+ }
+ }
+}
+
+#[test]
+fn public_video_h263() {
+ let mut fd = File::open(VIDEO_H263_3GP).expect("Unknown file");
+ let mut buf = Vec::new();
+ fd.read_to_end(&mut buf).expect("File error");
+
+ let mut c = Cursor::new(&buf);
+ let context = mp4::read_mp4(&mut c).expect("read_mp4 failed");
+ for track in context.tracks {
+ let stsd = track.stsd.expect("expected an stsd");
+ let v = match stsd.descriptions.first().expect("expected a SampleEntry") {
+ mp4::SampleEntry::Video(ref v) => v,
+ _ => panic!("expected a VideoSampleEntry"),
+ };
+ assert_eq!(v.codec_type, mp4::CodecType::H263);
+ assert_eq!(v.width, 176);
+ assert_eq!(v.height, 144);
+ let _codec_specific = match v.codec_specific {
+ mp4::VideoCodecSpecific::H263Config(_) => true,
+ _ => {
+ panic!("expected a H263Config",);
+ }
+ };
+ }
+}
+
+#[test]
+#[cfg(feature = "3gpp")]
+fn public_audio_amrnb() {
+ let mut fd = File::open(AUDIO_AMRNB_3GP).expect("Unknown file");
+ let mut buf = Vec::new();
+ fd.read_to_end(&mut buf).expect("File error");
+
+ let mut c = Cursor::new(&buf);
+ let context = mp4::read_mp4(&mut c).expect("read_mp4 failed");
+ for track in context.tracks {
+ let stsd = track.stsd.expect("expected an stsd");
+ let a = match stsd.descriptions.first().expect("expected a SampleEntry") {
+ mp4::SampleEntry::Audio(ref v) => v,
+ _ => panic!("expected a AudioSampleEntry"),
+ };
+ assert!(a.codec_type == mp4::CodecType::AMRNB);
+ let _codec_specific = match a.codec_specific {
+ mp4::AudioCodecSpecific::AMRSpecificBox(_) => true,
+ _ => {
+ panic!("expected a AMRSpecificBox",);
+ }
+ };
+ }
+}
+
+#[test]
+#[cfg(feature = "3gpp")]
+fn public_audio_amrwb() {
+ let mut fd = File::open(AUDIO_AMRWB_3GP).expect("Unknown file");
+ let mut buf = Vec::new();
+ fd.read_to_end(&mut buf).expect("File error");
+
+ let mut c = Cursor::new(&buf);
+ let context = mp4::read_mp4(&mut c).expect("read_mp4 failed");
+ for track in context.tracks {
+ let stsd = track.stsd.expect("expected an stsd");
+ let a = match stsd.descriptions.first().expect("expected a SampleEntry") {
+ mp4::SampleEntry::Audio(ref v) => v,
+ _ => panic!("expected a AudioSampleEntry"),
+ };
+ assert!(a.codec_type == mp4::CodecType::AMRWB);
+ let _codec_specific = match a.codec_specific {
+ mp4::AudioCodecSpecific::AMRSpecificBox(_) => true,
+ _ => {
+ panic!("expected a AMRSpecificBox",);
+ }
+ };
+ }
+}
+
+#[test]
+#[cfg(feature = "mp4v")]
+fn public_video_mp4v() {
+ let mut fd = File::open(VIDEO_MP4V_MP4).expect("Unknown file");
+ let mut buf = Vec::new();
+ fd.read_to_end(&mut buf).expect("File error");
+
+ let mut c = Cursor::new(&buf);
+ let context = mp4::read_mp4(&mut c).expect("read_mp4 failed");
+ for track in context.tracks {
+ let stsd = track.stsd.expect("expected an stsd");
+ let v = match stsd.descriptions.first().expect("expected a SampleEntry") {
+ mp4::SampleEntry::Video(ref v) => v,
+ _ => panic!("expected a VideoSampleEntry"),
+ };
+ assert_eq!(v.codec_type, mp4::CodecType::MP4V);
+ assert_eq!(v.width, 176);
+ assert_eq!(v.height, 144);
+ let _codec_specific = match v.codec_specific {
+ mp4::VideoCodecSpecific::ESDSConfig(_) => true,
+ _ => {
+ panic!("expected a ESDSConfig",);
+ }
+ };
+ }
+}