//! 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] externcrate log;
externcrate bitreader; externcrate byteorder; externcrate fallible_collections; externcrate num_traits; use bitreader::{BitReader, ReadInto}; use byteorder::{ReadBytesExt, WriteBytesExt};
use fallible_collections::TryRead; use fallible_collections::TryReserveError;
use num_traits::Num; use std::collections::HashSet; use std::convert::{TryFrom, TryInto as _}; use std::fmt; use std::io::Cursor; use std::io::{Read, Take};
#[macro_use] mod macros;
mod boxes; pubusecrate::boxes::{BoxType, FourCC};
// Unit tests. #[cfg(test)] mod tests;
#[cfg(feature = "unstable-api")] pubmod unstable;
/// The HEIF image and image collection brand /// The 'mif1' brand indicates structural requirements on files /// See HEIF (ISO 23008-12:2017) § 10.2.1 pubconst MIF1_BRAND: FourCC = FourCC { value: *b"mif1" };
/// The HEIF image sequence brand /// The 'msf1' brand indicates structural requirements on files /// See HEIF (ISO 23008-12:2017) § 10.3.1 pubconst MSF1_BRAND: FourCC = FourCC { value: *b"msf1" };
/// 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 { 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. pubtrait 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"
))
}
}
};
}
/// A collection to indicate unsupported features that were encountered during /// parsing. Since the default behavior for many such features is to ignore /// them, this often not fatal and there may be several to report. #[derive(Debug, Default)] pubstruct UnsupportedFeatures(u32);
impl<T> From<Status> for Result<T> { /// A convenience method to enable shortcuts like /// ``` /// # use mp4parse::{Result,Status}; /// # let _: Result<()> = /// Status::MissingAvifOrAvisBrand.into(); /// ``` /// instead of /// ``` /// # use mp4parse::{Error,Result,Status}; /// # let _: Result<()> = /// Err(Error::from(Status::MissingAvifOrAvisBrand)); /// ``` /// Note that `Status::Ok` can't be supported this way and will panic. fn from(parse_status: Status) -> Self { match parse_status {
Status::Ok => panic!("Can't determine Ok(_) inner value from Status"),
err_status => Err(err_status.into()),
}
}
}
/// 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 { match parse_status {
Status::Ok
| Status::BadArg
| Status::Invalid
| Status::Unsupported
| Status::Eof
| Status::Io
| Status::Oom => {
panic!("Status -> Error is only for Status:InvalidData errors")
}
_ => Self::InvalidData(parse_status),
}
}
}
impl From<Status> for &str { fn from(status: Status) -> Self { match status {
Status::Ok
| Status::BadArg
| Status::Invalid
| Status::Unsupported
| Status::Eof
| Status::Io
| Status::Oom => {
panic!("Status -> Error is only for specific parsing errors")
}
Status::A1lxEssential => { "AV1LayeredImageIndexingProperty (a1lx) shall not be marked as essential \
per https://aomediacodec.github.io/av1-avif/#layered-image-indexing-property-description"
}
Status::A1opNoEssential => { "OperatingPointSelectorProperty (a1op) shall be marked as essential \
per https://aomediacodec.github.io/av1-avif/#operating-point-selector-property-description"
}
Status::AlacBadMagicCookieSize => { "ALACSpecificBox magic cookie is the wrong size"
}
Status::AlacFlagsNonzero => { "no-zero alac (ALAC) flags"
}
Status::Av1cMissing => { "One AV1 Item Configuration Property (av1C) is mandatory for an \
image item of type'av01' \
per AVIF specification § 2.2.1"
}
Status::BitReaderError => { "Bitwise read failed"
}
Status::BoxBadSize => { "malformed size"
}
Status::BoxBadWideSize => { "malformed wide size"
}
Status::CheckParserStateErr => { "unread box content or bad parser sync"
}
Status::ColrBadQuantity => { "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"
}
Status::ColrBadQuantityBMFF => { "Each sample entry should have at most one ColourInformationBox (colr) \ for a given value of colour_type per ISOBMFF (ISO 14496-12:2020) § 12.1.5"
}
Status::ColrBadSize => { "Unexpected size for colr box"
}
Status::ColrBadType => { "Unsupported colour_type for ColourInformationBox"
}
Status::ColrReservedNonzero => { "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"
}
Status::ConstructionMethod => { "construction_method shall be 0 (file) or 1 (idat) per MIAF (ISO 23000-22:2019) § 7.2.1.7"
}
Status::CttsBadSize => { "insufficient data in 'ctts' box"
}
Status::CttsBadVersion => { "unsupported version in 'ctts' box"
}
Status::DflaBadMetadataBlockSize => { "FLACMetadataBlock larger than parent box"
}
Status::DflaFlagsNonzero => { "no-zero dfLa (FLAC) flags"
}
Status::DflaMissingMetadata => { "FLACSpecificBox missing metadata"
}
Status::DflaStreamInfoBadSize => { "FLACSpecificBox STREAMINFO block is the wrong size"
}
Status::DflaStreamInfoNotFirst => { "FLACSpecificBox must have STREAMINFO metadata first"
}
Status::DopsChannelMappingWriteErr => { "Couldn't write channel mapping table data."
}
Status::DopsOpusHeadWriteErr => { "Couldn't write OpusHead tag."
}
Status::ElstBadVersion => { "unhandled elst version"
}
Status::EsdsBadAudioSampleEntry => { "malformed audio sample entry"
}
Status::EsdsBadDescriptor => { "Invalid descriptor."
}
Status::EsdsDecSpecificInfoTagQuantity => { "There can be only one DecSpecificInfoTag descriptor"
}
Status::FtypBadSize => { "invalid ftyp size"
}
Status::FtypNotFirst => { "The FileTypeBox shall be placed as early as possible in the file \
per ISOBMFF (ISO 14496-12:2020) § 4.3.1"
}
Status::HdlrNameNoNul => { "The HandlerBox 'name' field shall be null-terminated \
per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2"
}
Status::HdlrNameNotUtf8 => { "The HandlerBox 'name' field shall be valid utf8 \
per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2"
}
Status::HdlrNotFirst => { "The HandlerBox shall be the first contained box within the MetaBox \
per MIAF (ISO 23000-22:2019) § 7.2.1.5"
}
Status::HdlrPredefinedNonzero => { "The HandlerBox 'pre_defined' field shall be 0 \
per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2"
}
Status::HdlrReservedNonzero => { "The HandlerBox 'reserved' fields shall be 0 \
per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2"
}
Status::HdlrTypeNotPict => { "The HandlerBox handler_type must be 'pict' \
per MIAF (ISO 23000-22:2019) § 7.2.1.5"
}
Status::HdlrUnsupportedVersion => { "The HandlerBox version shall be 0 (zero) \
per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2"
}
Status::HdrlBadQuantity => { "There shall be exactly one hdlr box \
per ISOBMFF (ISO 14496-12:2020) § 8.4.3.1"
}
Status::IdatBadQuantity => { "There shall be zero or one idat boxes \
per ISOBMFF (ISO 14496-12:2020) § 8.11.11"
}
Status::IdatMissing => { "ItemLocationBox (iloc) construction_method indicates 1 (idat), \
but no idat box is present."
}
Status::IinfBadChild => { "iinf box shall contain only infe boxes \
per ISOBMFF (ISO 14496-12:2020) § 8.11.6.2"
}
Status::IinfBadQuantity => { "There shall be zero or one iinf boxes \
per ISOBMFF (ISO 14496-12:2020) § 8.11.6.1"
}
Status::IlocBadConstructionMethod => { "construction_method is taken from the set 0, 1 or 2 \
per ISOBMFF (ISO 14496-12:2020) § 8.11.3.3"
}
Status::IlocBadExtent => { "extent_count != 1 requires explicit offset and length \
per ISOBMFF (ISO 14496-12:2020) § 8.11.3.3"
}
Status::IlocBadExtentCount => { "extent_count must have a value 1 or greater \
per ISOBMFF (ISO 14496-12:2020) § 8.11.3.3"
}
Status::IlocBadFieldSize => { "value must be in the set {0, 4, 8}"
}
Status::IlocBadQuantity => { "There shall be zero or one iloc boxes \
per ISOBMFF (ISO 14496-12:2020) § 8.11.3.1"
}
Status::IlocBadSize => { "invalid iloc size"
}
Status::IlocDuplicateItemId => { "duplicate item_ID in iloc"
}
Status::IlocNotFound => { "ItemLocationBox (iloc) contains an extent not present in any mdat or idat box"
}
Status::IlocOffsetOverflow => { "offset calculation overflow"
}
Status::ImageItemType => { "Image item type is neither 'av01' nor 'grid'"
}
Status::InfeFlagsNonzero => { "'infe' flags field shall be 0 \
per ISOBMFF (ISO 14496-12:2020) § 8.11.6.2"
}
Status::InvalidUtf8 => { "invalid utf8"
}
Status::IpcoIndexOverflow => { "ipco index overflow"
}
Status::IpmaBadIndex => { "Invalid property index in ipma"
}
Status::IpmaBadItemOrder => { "Each ItemPropertyAssociation box shall be ordered by increasing item_ID"
}
Status::IpmaBadQuantity => { "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"
}
Status::IpmaBadVersion => { "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"
}
Status::IpmaDuplicateItemId => { "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"
}
Status::IpmaFlagsNonzero => { "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"
}
Status::IpmaIndexZeroNoEssential => { "the essential indicator shall be 0 for property index 0 \
per ISOBMFF (ISO 14496-12:2020) § 8.11.14.3"
}
Status::IpmaTooBig => { "ipma box exceeds maximum size for entry_count"
}
Status::IpmaTooSmall => { "ipma box below minimum size for entry_count"
}
Status::IprpBadChild => { "unexpected iprp child"
}
Status::IprpBadQuantity => { "There shall be zero or one iprp boxes \
per ISOBMFF (ISO 14496-12:2020) § 8.11.14.1"
}
Status::IprpConflict => { "conflicting item property values"
}
Status::IrefBadQuantity => { "There shall be zero or one iref boxes \
per ISOBMFF (ISO 14496-12:2020) § 8.11.12.1"
}
Status::IrefRecursion => { "from_item_id and to_item_id must be different"
}
Status::IspeMissing => { "Missing 'ispe' property for image item, required \
per HEIF (ISO/IEC 23008-12:2017) § 6.5.3.1"
}
Status::ItemTypeMissing => { "No ItemInfoEntry for item_ID"
}
Status::LselNoEssential => { "LayerSelectorProperty (lsel) shall be marked as essential \
per HEIF (ISO/IEC 23008-12:2017) § 6.5.11.1"
}
Status::MdhdBadTimescale => { "zero timescale in mdhd"
}
Status::MdhdBadVersion => { "unhandled mdhd version"
}
Status::MehdBadVersion => { "unhandled mehd version"
}
Status::MetaBadQuantity => { "There should be zero or one meta boxes \
per ISOBMFF (ISO 14496-12:2020) § 8.11.1.1"
}
Status::MissingAvifOrAvisBrand => { "The file shall list 'avif' or 'avis' in the compatible_brands field
of the FileTypeBox \
per https://aomediacodec.github.io/av1-avif/#file-constraints"
}
Status::MissingMif1Brand => { "The FileTypeBox should contain 'mif1' in the compatible_brands list \
per MIAF (ISO 23000-22:2019/Amd. 2:2021) § 7.2.1.2"
}
Status::MoovBadQuantity => { "Multiple moov boxes found; \
files with avis or msf1 brands shall contain exactly one moov box \
per ISOBMFF (ISO 14496-12:2020) § 8.2.1.1"
}
Status::MoovMissing => { "No moov box found; \
files with avis or msf1 brands shall contain exactly one moov box \
per ISOBMFF (ISO 14496-12:2020) § 8.2.1.1"
}
Status::MultipleAlpha => { "multiple alpha planes"
}
Status::MvhdBadTimescale => { "zero timescale in mvhd"
}
Status::MvhdBadVersion => { "unhandled mvhd version"
}
Status::NoImage => "No primary image or image sequence found",
Status::PitmBadQuantity => { "There shall be zero or one pitm boxes \
per ISOBMFF (ISO 14496-12:2020) § 8.11.4.1"
}
Status::PitmMissing => { "Missing required PrimaryItemBox (pitm), required \
per HEIF (ISO/IEC 23008-12:2017) § 10.2.1"
}
Status::PitmNotFound => { "PrimaryItemBox (pitm) referenced an item ID that was not present"
}
Status::PixiBadChannelCount => { "invalid num_channels"
}
Status::PixiMissing => { "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"
}
Status::PsshSizeOverflow => { "overflow in read_pssh"
}
Status::ReadBufErr => { "failed buffer read"
}
Status::SchiQuantity => { "tenc box should be only one at most in sinf box"
}
Status::StsdBadAudioSampleEntry => { "malformed audio sample entry"
}
Status::StsdBadVideoSampleEntry => { "malformed video sample entry"
}
Status::TkhdBadVersion => { "unhandled tkhd version"
}
Status::TxformBeforeIspe => { "Every image item shall be associated with one property of \ type ImageSpatialExtentsProperty (ispe), prior to the \
association of all transformative properties. \
per HEIF (ISO/IEC 23008-12:2017) § 6.5.3.1"
}
Status::TxformNoEssential => { "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"
}
Status::TxformOrder => { "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"
}
}
}
}
impl From<Error> for Status { fn from(error: Error) -> Self { match error {
Error::Unsupported(_) => Self::Unsupported,
Error::InvalidData(parse_status) => 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::MoovMissing => Self::MoovMissing,
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<T> From<Result<T>> for Status { fn from(result: Result<T>) -> Self { match result {
Ok(_) => Status::Ok,
Err(e) => Status::from(e),
}
}
}
impl From<fallible_collections::TryReserveError> for Status { fn from(_: fallible_collections::TryReserveError) -> Self {
Status::Oom
}
}
impl From<std::io::Error> for Status { fn from(_: std::io::Error) -> Self {
Status::Io
}
}
/// 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)] pubenum Error { /// Parse error caused by corrupt or malformed data. /// See the helper [`From<Status> for Error`](enum.Error.html#impl-From<Status>)
InvalidData(Status), /// Parse error caused by limited parser support rather than invalid data.
Unsupported(&'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.
MoovMissing, /// Out of memory
OutOfMemory,
}
/// Result shorthand using our Error enum. pubtype 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)] pubstruct BoxHeader { /// Box type. pub name: BoxType, /// Size of the box in bytes. pub size: u64, /// Offset to the start of the contained data (or header size). pub offset: u64, /// Uuid for extended type. #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 pub uuid: Option<[u8; 16]>,
}
/// Mastering display colour volume from an `mdcv` box (ISO 14496-12). /// Primary indices are R\[0\], G\[1\], B\[2\]. Divide chromaticity values by 50000 /// and luminance values by 10000 to obtain physical units. #[derive(Debug, Clone)] pubstruct MasteringDisplayColourVolume { pub display_primaries_x: [u16; 3], pub display_primaries_y: [u16; 3], pub white_point_x: u16, pub white_point_y: u16, /// In units of 0.0001 cd/m² pub max_display_mastering_luminance: u32, /// In units of 0.0001 cd/m² pub min_display_mastering_luminance: u32,
}
/// Content light level from a `clli` box (ISO 14496-12). #[derive(Debug, Clone)] pubstruct ContentLightLevel { /// Maximum content light level in cd/m² pub max_content_light_level: u16, /// Maximum picture average light level in cd/m² pub max_pic_average_light_level: u16,
}
#[derive(Debug)] pubstruct 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>, pub pixel_aspect_ratio: Option<f32>, /// Only `ColourInformation::Nclx` is currently surfaced through the C API; /// `ColourInformation::Icc` is stored but not exposed to C consumers. pub colour_info: Option<ColourInformation>, /// Mastering display colour volume from the `mdcv` box (ISO 14496-12). pub hdr_mastering_display: Option<MasteringDisplayColourVolume>, /// Content light level from the `clli` box (ISO 14496-12). pub hdr_content_light_level: Option<ContentLightLevel>,
}
/// 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)] pubstruct 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)] pubstruct 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>,
}
/// Represents a userdata box 'udta'. /// Currently, only the metadata atom 'meta' /// is parsed. #[derive(Debug, Default)] pubstruct UserdataBox { pub meta: Option<MetadataBox>,
}
/// Represents the contents of a 'stik' /// atom that indicates content types within /// iTunes. #[derive(Debug, Clone, Eq, PartialEq)] pubenum 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)] pubenum 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),
}
/// See ISOBMFF (ISO 14496-12:2020) § 8.11.2.1 #[cfg(feature = "meta-xml")] #[derive(Debug)] pubenum XmlBox { /// XML metadata
StringXmlBox(TryString), /// Binary XML metadata
BinaryXmlBox(TryVec<u8>),
}
/// Internal data structures. #[derive(Debug, Default)] pubstruct 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 `DataBox` 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 [`DataBox`] /// and we want to avoid the copy that splitting the storage would entail. enum IsobmffItem {
MdatLocation(Extent),
IdatLocation(Extent),
Data(TryVec<u8>),
}
impl fmt::Debug for IsobmffItem { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self {
IsobmffItem::MdatLocation(extent) | IsobmffItem::IdatLocation(extent) => f
.debug_struct("IsobmffItem::Location")
.field("0", &format_args!("{extent:?}"))
.finish(),
IsobmffItem::Data(data) => f
.debug_struct("IsobmffItem::Data")
.field("0", &format_args!("{} bytes", data.len()))
.finish(),
}
}
}
#[derive(Debug)] struct AvifItem { /// The `item_ID` from ISOBMFF (ISO 14496-12:2020) § 8.11.3 /// /// See [`read_iloc`]
id: ItemId,
#[derive(Default, Debug)] pubstruct AvifContext { /// Level of deviation from the specification before failing the parse
strictness: ParseStrictness, /// Storage elements which can be located anywhere within the "file" identified by /// [`BoxType::ItemLocationBox`]es using [`ConstructionMethod::File`]. /// Referred to by the [`IsobmffItem`]`::*Location` variants of the `AvifItem`s in this struct
media_storage: TryVec<DataBox>, /// Similar to `media_storage`, but for a single optional chunk of storage within the /// MetaBox itentified by [`BoxType::ItemLocationBox`]es using [`ConstructionMethod::Idat`].
item_data_box: Option<DataBox>, /// The item indicated by the `pitm` box, See ISOBMFF (ISO 14496-12:2020) § 8.11.4 /// May be `None` in the pure image sequence case.
primary_item: Option<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, /// Should probably only ever be [`AVIF_BRAND`] or [`AVIS_BRAND`], but other values /// are legal as long as one of the two is the `compatible_brand` list. pub major_brand: FourCC, /// Information on the sequence contained in the image, or None if not present pub sequence: Option<MediaContext>, /// A collection of unsupported features encountered during the parse pub unsupported_features: UnsupportedFeatures,
}
/// 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::MdatLocation(extent) => { for mdat in &self.media_storage { iflet Some(slice) = mdat.get(extent) { return slice;
}
}
unreachable!( "IsobmffItem::MdatLocation requires the location exists in AvifContext::media_storage"
);
}
IsobmffItem::IdatLocation(extent) => { self.item_data_box
.as_ref()
.and_then(|idat| idat.get(extent))
.unwrap_or_else(|| unreachable!("IsobmffItem::IdatLocation equires the location exists in AvifContext::item_data_box"))
}
IsobmffItem::Data(data) => data.as_slice(),
}
}
}
#[derive(Debug)] enum DataBoxMetadata {
Idat,
Mdat { /// 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,
},
}
/// Represents either an Item Data Box (ISOBMFF (ISO 14496-12:2020) § 8.11.11) /// Or a Media Data Box (ISOBMFF (ISO 14496-12:2020) § 8.1.1) struct DataBox {
metadata: DataBoxMetadata,
data: TryVec<u8>,
}
/// Convert an absolute offset to an offset relative to the beginning of the /// slice [`DataBox::data`] returns. Returns None if the offset would be /// negative or if the offset would overflow a `usize`. fn start(&self, offset: u64) -> Option<usize> { matchself.metadata {
DataBoxMetadata::Idat => u64_to_usize_logged(offset),
DataBoxMetadata::Mdat { file_offset } => { let start = offset.checked_sub(file_offset); if start.is_none() {
error!("Overflow subtracting {offset} + {file_offset}");
}
u64_to_usize_logged(start?)
}
}
}
/// Returns an appropriate variant of [`IsobmffItem`] to describe the extent /// referencing data within this type of box. fn location(&self, extent: &Extent) -> IsobmffItem { matchself.metadata {
DataBoxMetadata::Idat => IsobmffItem::IdatLocation(extent.clone()),
DataBoxMetadata::Mdat { .. } => IsobmffItem::MdatLocation(extent.clone()),
}
}
/// Return a slice from the DataBox specified by the provided `extent`. /// Returns `None` if the extent isn't fully contained by the DataBox or if /// either the offset or length (if the extent is bounded) of the slice /// would overflow a `usize`. fn get<'a>(&'a self, extent: &'a Extent) -> Option<&'a [u8]> { match extent {
Extent::WithLength { offset, len } => { let start = self.start(*offset)?; let end = start.checked_add(*len); if end.is_none() {
error!("Overflow adding {start} + {len}");
} self.data().get(start..end?)
}
Extent::ToEnd { offset } => { let start = self.start(*offset)?; self.data().get(start..)
}
}
}
}
/// 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, Eq)] enum IlocFieldSize {
Zero,
Four,
Eight,
}
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, Eq)] enum ConstructionMethod {
File = 0,
Idat = 1,
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 `DataBox` means this can still possibly succeed even in the case /// that the raw value exceeds usize::MAX on platforms where that type is smaller /// than u64. However, `len` is stored as a `usize` since no value larger than /// `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 },
}
// 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, Eq, Default)] pubenum ParseStrictness {
Permissive, // Error only on ambiguous inputs #[default]
Normal, // Error on "shall" directives, log warnings for "should"
Strict, // Error on "should" directives
}
/// The media's global (mvhd) timescale in units per second. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pubstruct MediaTimeScale(pub u64);
/// A time to be scaled by the media's global (mvhd) timescale. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pubstruct MediaScaledTime(pub u64);
/// The track's local (mdhd) timescale. /// Members are timescale units per second and the track id. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pubstruct 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, Eq)] pubstruct TrackScaledTime<T>(pub T, pub usize);
impl<T> std::ops::Add for TrackScaledTime<T> where
T: num_traits::CheckedAdd,
{ type Output = Option<Self>;
impl<T> Drop for BMFFBox<'_, T> { fn drop(&mutself) { ifself.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 pubfn 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 => { if name == BoxType::MediaDataBox { 0
} else { return Err(Error::Unsupported("unknown sized box"));
}
} 1 => be_u64(src)?,
_ => u64::from(size32),
};
trace!("read_box_header: name: {name:?}, size: {size}"); letmut offset = match size32 { 1 => BoxHeader::MIN_LARGE_SIZE,
_ => BoxHeader::MIN_SIZE,
}; let uuid = if name == BoxType::UuidBox { if size >= offset + 16 { letmut buffer = [0u8; 16]; let count = src.read(&mut buffer)?;
offset += count.to_u64(); if count == 16 {
Some(buffer)
} else {
debug!("malformed uuid (short read)"); return Err(Error::UnexpectedEOF);
}
} else {
None
}
} else {
None
}; match size32 { 0 => (), 1if offset > size => return Err(Error::from(Status::BoxBadWideSize)),
_ if offset > size => return Err(Error::from(Status::BoxBadSize)),
_ => (),
}
Ok(BoxHeader {
name,
size,
offset,
uuid,
})
}
/// Parse the extra header fields for a full box. pubfn 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. pubfn 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. pubfn 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!("{header:?} (skipped)");
header
.size
.checked_sub(header.offset)
.ok_or(Error::Unsupported("Skipping past unknown sized box"))?
};
assert_eq!(to_skip, src.bytes_left());
skip(src, to_skip)
}
/// Skip over the remain data of a box. pubfn skip_box_remain<T: Read>(src: &mut BMFFBox<T>) -> Result<()> { let remain = { let header = src.get_header(); let len = src.bytes_left();
debug!("remain {len} (skipped) in {header:?}");
len
};
skip(src, remain)
}
/// Read the contents of an AVIF file pubfn read_avif<T: Read>(f: &mut T, strictness: ParseStrictness) -> Result<AvifContext> {
debug!("read_avif(strictness: {strictness:?})");
letmut f = OffsetReader::new(f); letmut iter = BoxIter::new(&mut f); let expected_image_type; letmut unsupported_features = UnsupportedFeatures::new();
// 'ftyp' box must occur first; see ISOBMFF (ISO 14496-12:2020) § 4.3.1 let major_brand = iflet Some(mut b) = iter.next_box()? { if b.head.name == BoxType::FileTypeBox { let ftyp = read_ftyp(&mut b)?;
let has_avif_brand = ftyp.contains(&AVIF_BRAND); let has_avis_brand = ftyp.contains(&AVIS_BRAND); let has_mif1_brand = ftyp.contains(&MIF1_BRAND); let has_msf1_brand = ftyp.contains(&MSF1_BRAND);
let primary_image_expected = has_mif1_brand || has_avif_brand; let image_sequence_expected = has_msf1_brand || has_avis_brand;
// store data or record location of relevant items for (item_id, loc) in iloc_items { let item = if Some(item_id) == primary_item_id {
&mut primary_item
} elseif Some(item_id) == alpha_item_id {
&mut alpha_item
} else { continue;
};
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/idat later to avoid the copy. if loc.extents.len() > 1 {
*item = Some(AvifItem::with_inline_data(item_id))
}
// Generalize the process of connecting items to their data; returns // true if the extent is successfully added to the AvifItem letmut find_and_add_to_item = |extent: &Extent, dat: &DataBox| -> Result<bool> { iflet Some(extent_slice) = dat.get(extent) { match item {
None => {
trace!("Using IsobmffItem::Location");
*item = Some(AvifItem {
id: item_id,
image_data: dat.location(extent),
});
}
Some(AvifItem {
image_data: IsobmffItem::Data(bytes),
..
}) => {
trace!("Using IsobmffItem::Data"); // We could potentially optimize memory usage by trying to avoid reading // or storing dat boxes which aren't used by our API, but for now it seems // like unnecessary complexity
bytes.extend_from_slice(extent_slice)?;
}
_ => unreachable!(),
} return Ok(true);
}
Ok(false)
};
match loc.construction_method {
ConstructionMethod::File => { for extent in loc.extents { letmut found = false; // try to find an mdat which contains the extent for mdat in media_storage.iter() { if find_and_add_to_item(&extent, mdat)? {
found = true; break;
}
}
if !found { return Status::IlocNotFound.into();
}
}
}
ConstructionMethod::Idat => { iflet Some(idat) = &item_data_box { for extent in loc.extents { let found = find_and_add_to_item(&extent, idat)?; if !found { return Status::IlocNotFound.into();
}
}
} else { return Status::IdatMissing.into();
}
}
ConstructionMethod::Item => {
fail_with_status_if(
strictness != ParseStrictness::Permissive,
Status::ConstructionMethod,
)?;
}
}
// Lacking a brand that requires them, it's fine for moov boxes to exist in // BMFF files; they're simply ignored if expected_image_type.has_sequence() && image_sequence.is_none() {
fail_with_status_if(
strictness != ParseStrictness::Permissive,
Status::MoovMissing,
)?;
}
// Returns true iff `id` is `Some` and there is no corresponding property for it let missing_property_for = |id: Option<ItemId>, property: BoxType| -> bool {
id.is_some_and(|id| {
item_properties
.get(id, property)
.map_or(true, |opt| opt.is_none())
})
};
// Generalize the property checks so we can apply them to primary and alpha items letmut check_image_item = |item: &mut Option<AvifItem>| -> Result<()> { let item_id = item.as_ref().map(|item| item.id); let item_type = item_id.and_then(|item_id| {
item_infos
.iter()
.find(|item_info| item_id == item_info.item_id)
.map(|item_info| item_info.item_type)
});
match item_type.map(u32::to_be_bytes).as_ref() {
Some(b"av01") => { if missing_property_for(item_id, BoxType::AV1CodecConfigurationBox) {
fail_with_status_if(
strictness != ParseStrictness::Permissive,
Status::Av1cMissing,
)?;
}
if missing_property_for(item_id, BoxType::PixelInformationBox) { // 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_with_status_if( if cfg!(feature = "missing-pixi-permitted") {
strictness == ParseStrictness::Strict
} else {
strictness != ParseStrictness::Permissive
},
Status::PixiMissing,
)?;
}
iflet Some(AvifItem { id, .. }) = item { if item_properties.forbidden_items.contains(id) {
error!("Not processing item id {id:?} since it is associated with essential, but unsupported properties");
*item = None;
}
}
/// 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,
unsupported_features: &mut UnsupportedFeatures,
) -> Result<AvifMeta> { let version = read_fullbox_version_no_flags(src)?;
if version != 0 { return Err(Error::Unsupported("unsupported meta version"));
}
/// 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,
unsupported_features: &mut UnsupportedFeatures,
) -> 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()
}; letmut item_infos = TryVec::with_capacity(entry_count)?;
letmut iter = src.box_iter(); whilelet Some(mut b) = iter.next_box()? { if b.head.name != BoxType::ItemInfoEntry { return Status::IinfBadChild.into();
}
/// 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,
unsupported_features: &mut UnsupportedFeatures,
) -> Result<Option<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_with_status_if(
strictness == ParseStrictness::Strict,
Status::InfeFlagsNonzero,
)?;
}
// 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)?;
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)?;
/// 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>> { letmut item_references = TryVec::new(); let version = read_fullbox_version_no_flags(src)?; if version > 1 { return Err(Error::Unsupported("iref version"));
}
letmut iter = src.box_iter(); whilelet 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 _ in0..reference_count { let to_item_id = ItemId::read(&mut b, version)?; if from_item_id == to_item_id { return Status::IrefRecursion.into();
}
item_references.push(SingleItemTypeReferenceBox {
item_type: b.head.name.into(),
from_item_id,
to_item_id,
})?;
}
check_parser_state!(b.content);
}
trace!("read_iref -> {item_references:#?}");
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,
unsupported_features: &mut UnsupportedFeatures,
) -> Result<ItemPropertiesBox> { letmut 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(_) => Status::IprpBadChild.into(),
None => Err(Error::UnexpectedEOF),
}?;
whilelet Some(mut b) = iter.next_box()? { if b.head.name != BoxType::ItemPropertyAssociationBox { return Status::IprpBadChild.into();
}
let (version, flags) = read_fullbox_extra(&mut b)?; if ipma_version_and_flag_values_seen.contains(&(version, flags)) {
fail_with_status_if(
strictness != ParseStrictness::Permissive,
Status::IpmaBadQuantity,
)?;
} if flags != 0 && properties.len() <= 127 {
fail_with_status_if(
strictness == ParseStrictness::Strict,
Status::IpmaFlagsNonzero,
)?;
}
ipma_version_and_flag_values_seen.push((version, flags))?; for association_entry in read_ipma(&mut b, strictness, version, flags)? { if forbidden_items.contains(&association_entry.item_id) {
warn!( "Skipping {association_entry:?} since the item referenced shall not be processed"
);
}
iflet Some(previous_entry) = association_entries
.iter()
.find(|e| association_entry.item_id == e.item_id)
{
error!( "Duplicate ipma entries for item_id\n1: {previous_entry:?}\n2: {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 Status::IpmaDuplicateItemId.into();
}
const TRANSFORM_ORDER: &[BoxType] = &[
BoxType::ImageSpatialExtentsProperty,
BoxType::CleanApertureBox,
BoxType::ImageRotation,
BoxType::ImageMirror,
]; letmut prev_transform_index = None; // Realistically, there should only ever be 1 nclx and 1 icc letmut 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_with_status_if(
strictness != ParseStrictness::Permissive,
Status::IpmaIndexZeroNoEssential,
)?;
} continue;
}
// Check additional requirements on specific properties match property {
ItemProperty::AV1Config(_)
| ItemProperty::CleanAperture
| ItemProperty::Mirroring(_)
| ItemProperty::Rotation(_) => { if !a.essential {
warn!("{property:?} is missing required 'essential' bit"); // 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_with_status_if(
strictness == ParseStrictness::Strict,
Status::TxformNoEssential,
)?;
}
}
// NOTE: 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(); iflet 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_with_status_if(
strictness != ParseStrictness::Permissive,
Status::ColrBadQuantity,
)?;
} else {
colour_type_indexes.insert(colour_type, a.property_index)?;
}
}
// The following properties are unsupported, but we still enforce that // they've been correctly marked as essential or not.
ItemProperty::LayeredImageIndexing => {
assert!(feature.is_ok() && unsupported_features.contains(feature?)); if a.essential {
fail_with_status_if(
strictness != ParseStrictness::Permissive,
Status::A1lxEssential,
)?;
}
}
/// See ISOBMFF (ISO 14496-12:2020) § 8.11.14.1 /// Variants with no associated data are recognized but not necessarily supported. /// See [`Feature`] to determine support. #[derive(Debug)] pubenum ItemProperty {
AuxiliaryType(AuxiliaryTypeProperty),
AV1Config(AV1ConfigBox),
Channels(PixelInformation),
CleanAperture,
Colour(ColourInformation),
ImageSpatialExtents(ImageSpatialExtentsProperty),
LayeredImageIndexing,
LayerSelection,
Mirroring(ImageMirror),
OperatingPointSelector,
PixelAspectRatio(PixelAspectRatio),
Rotation(ImageRotation), /// Necessary to validate property indices in read_iprp
Unsupported(BoxType),
}
/// 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)] pubstruct ItemPropertiesBox { /// `ItemPropertyContainerBox property_container` in the spec
properties: TryHashMap<PropertyIndex, ItemProperty>, /// `ItemPropertyAssociationBox association[]` in the spec
association_entries: TryVec<ItemPropertyAssociationEntry>, /// Items that shall not be processed due to unsupported properties that /// have been marked essential. /// See HEIF (ISO/IEC 23008-12:2017) § 9.3.1
forbidden_items: TryVec<ItemId>,
}
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 get_multiple(
&self,
item_id: ItemId,
filter: implFn(&ItemProperty) -> bool,
) -> Result<TryVec<&ItemProperty>> { letmut values = TryVec::new(); for entry in &self.association_entries { for a in &entry.associations { if entry.item_id == item_id { matchself.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)] pubstruct $name($inner);
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)] pubstruct $name($inner);
impl UpperBounded for std::num::NonZeroU8 { const MAX: u64 = u8::MAX as u64;
}
}
usecrate::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 as UpperBounded>::MAX <= <$inner>::MAX as u64
);
static_assertions::const_assert!(
<$lhs as UpperBounded>::MAX * <$rhs as UpperBounded>::MAX
<= <$output as UpperBounded>::MAX
);
let lhs: $inner = self.get().into(); let rhs: $inner = rhs.get().into(); Self::Output::new(lhs.checked_mul(rhs).expect("infallible"))
}
}
};
}
/// 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 = iflet 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)
difference
} else { return Status::IpmaTooSmall.into();
};
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 Status::IpmaTooBig.into();
}
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. letmut entries = TryVec::<ItemPropertyAssociationEntry>::with_capacity(
total_associations / ItemPropertiesBox::MIN_PROPERTIES,
)?;
for _ in0..entry_count { let item_id = ItemId::read(src, version)?;
other_box_type => { // Even if we didn'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 = match other_box_type {
BoxType::AV1LayeredImageIndexingProperty => ItemProperty::LayeredImageIndexing,
BoxType::CleanApertureBox => ItemProperty::CleanAperture,
BoxType::LayerSelectorProperty => ItemProperty::LayerSelection,
BoxType::OperatingPointSelectorProperty => ItemProperty::OperatingPointSelector,
_ => {
warn!("No ItemProperty variant for {other_box_type:?}");
ItemProperty::Unsupported(other_box_type)
}
};
debug!("Storing empty record {item_property:?}");
item_property
}
};
properties.insert(index, property)?;
index = PropertyIndex(
index
.0
.checked_add(1) // must include ignored properties to have correct indexes
.ok_or_else(|| Error::from(Status::IpcoIndexOverflow))?,
);
/// 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 Status::PixiBadChannelCount.into();
}
/// 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 {
pub colour_primaries: u8,
pub transfer_characteristics: u8,
pub matrix_coefficients: u8,
pub full_range_flag: bool,
}
/// The raw bytes of the ICC profile
#[repr(C)]
pub struct IccColourInformation {
bytes: TryVec<u8>,
}
#[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();
}
/// 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 => ConstructionMethod::Item,
_ => return Status::IlocBadConstructionMethod.into(),
}
}
};
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 Status::IlocBadExtentCount.into();
}
// "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 Status::IlocBadExtent.into();
}
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_else(|| Error::from(Status::IlocOffsetOverflow))?;
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 Status::IlocDuplicateItemId.into();
}
}
/// Read the contents of a box, including sub boxes.
pub fn read_mp4<T: Read>(f: &mut T, strictness: ParseStrictness) -> 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
// 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, strictness)?);
}
#[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::MoovMissing)
}
/// 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 Status::MvhdBadTimescale.into();
}
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.
pub fn read_moov<T: Read>(
f: &mut BMFFBox<T>,
context: Option<MediaContext>,
strictness: ParseStrictness,
) -> 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, strictness)?;
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);
}
// ISO/IEC 14496-12 §6.1.4 (Track Identifiers) and §8.5.3 (tkhd semantics):
// track_ID values are unique within a file/presentation and must not be reused.
let mut track_ids = HashSet::new();
for track in &tracks {
if let Some(track_id) = track.track_id {
if !track_ids.insert(track_id) {
if strictness == ParseStrictness::Strict {
return Err(Error::from(Status::Invalid));
}
warn!(
"Duplicate track_id {track_id} found; track_id-based lookups will use first occurrence"
);
}
}
}
fn read_tref<T: Read>(f: &mut BMFFBox<T>) -> Result<TrackReferenceBox> {
// Will likely only see trefs with one auxl
let mut references = TryVec::with_capacity(1)?;
let mut iter = f.box_iter();
while let Some(mut b) = iter.next_box()? {
match b.head.name {
BoxType::AuxiliaryBox => {
references.push(TrackReferenceEntry::Auxiliary(read_tref_auxl(&mut b)?))?
}
_ => skip_box_content(&mut b)?,
};
check_parser_state!(b.content);
}
Ok(TrackReferenceBox { references })
}
fn read_tref_auxl<T: Read>(f: &mut BMFFBox<T>) -> Result<TrackReference> {
let num_track_ids = (f.bytes_left() / std::mem::size_of::<u32>().to_u64()).try_into()?;
let mut track_ids = TryVec::with_capacity(num_track_ids)?;
for _ in 0..num_track_ids {
track_ids.push(be_u32(f)?)?;
}
Ok(TrackReference { track_ids })
}
fn read_minf<T: Read>(
f: &mut BMFFBox<T>,
track: &mut Track,
strictness: ParseStrictness,
) -> 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, strictness)?,
_ => 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();
#[allow(clippy::manual_is_multiple_of)] // Allow until Gecko's MSRV is 1.87.
if bytes_left % 4 != 0 {
return Status::FtypBadSize.into();
}
// 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 Status::MvhdBadVersion.into(),
}
let timescale = be_u32(src)?;
let duration = match version { 1 => be_u64(src)?, 0 => {
let d = be_u32(src)?;
if d == u32::MAX {
u64::MAX
} else {
u64::from(d)
}
}
_ => unreachable!("Should have returned Status::MvhdBadVersion"),
};
// Skip remaining valid fields.
skip(src, 80)?;
// Padding could be added in some contents.
skip_box_remain(src)?;
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 Status::TkhdBadVersion.into(),
}
let track_id = be_u32(src)?;
skip(src, 4)?;
let duration = match version { 1 => be_u64(src)?, 0 => u64::from(be_u32(src)?),
_ => unreachable!("Should have returned Status::TkhdBadVersion"),
};
// Skip uninteresting fields.
skip(src, 16)?;
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, flags) = 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 Status::ElstBadVersion.into(),
};
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 {
looped: flags == 1,
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 == u32::MAX {
u64::MAX
} else {
u64::from(d)
}
};
(timescale, duration)
}
_ => return Status::MdhdBadVersion.into(),
};
// 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>,
strictness: ParseStrictness,
) -> Result<CompositionOffsetBox> {
let (version, _) = read_fullbox_extra(src)?;
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 Status::CttsBadVersion.into();
}
};
offsets.push(TimeOffset {
sample_count,
time_offset,
})?;
}
if strictness == ParseStrictness::Strict {
check_parser_state!(src.content);
} else {
// Padding may be present in some content.
skip_box_remain(src)?;
}
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)?;
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
};
// 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;
}
}
// 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,
strictness: ParseStrictness,
) -> Result<()> {
// Keep the first DecSpecificInfo and ignore subsequent entries in
// non-strict mode. Concatenating raw bytes and/or overwriting the parsed
// fields from a second DSI leaves the ES_Descriptor inconsistent (e.g.
// sample-rate/channel-count mismatched with the stored DSI bytes) and
// produces silent-audio playback. A single well-formed DSI is always
// sufficient.
if !esds.decoder_specific_data.is_empty() {
fail_with_status_if(
strictness == ParseStrictness::Strict,
Status::EsdsDecSpecificInfoTagQuantity,
)?;
return Ok(());
}
#[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 = [
(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 {
// Note: audio_object_type == 42 (xHE-AAC) uses UsacConfig rather than GASpecificConfig
// (similar to Chromium's AAC parsing). This may need different handling in the future
// if GASpecificConfig parsing is extended or made stricter, or if we need specific
// fields from the UsacConfig. 1..=4 | 6 | 7 | 17 | 19..=23 | 42 => {
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,
};
// Map channel_configuration to channel count. Zero indicates PCE
// (program_config_element) signalling, which requires parsing the
// trailing GASpecificConfig fields below and so can't be resolved
// yet.
let channel_count_from_config = match channel_configuration { 0 => None, 1..=7 => Some(channel_configuration), 11 => Some(7), // 6.1, AAC Amendment 4 (2013) 12 | 14 => Some(8), // 7.1 (a/d), ITU BS.2159
_ => return Err(Error::Unsupported("invalid channel configuration")),
};
// Record the essential audio fields now, before any further bit
// reads that may hit EOF on a truncated DSI. find_descriptor
// swallows BitReaderError in non-strict mode; without recording
// here we'd return a playable-looking track with no usable config
// (see band-orion.de, where HE-AAC with explicit SBR signalling
// omits the GASpecificConfig tail).
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);
if let Some(cc) = channel_count_from_config {
esds.audio_channel_count = Some(cc);
esds.decoder_specific_data.extend_from_slice(data)?;
}
if channel_count_from_config.is_none() {
// channel_configuration == 0: derive channel count from the
// program_config_element.
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
}
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,
strictness: ParseStrictness,
) -> Result<()> {
let des = &mut Cursor::new(data);
let object_profile = des.read_u8()?;
/// See MPEG-4 Systems (ISO 14496-1:2010) § 7.2.6.5
fn read_es_descriptor(
data: &[u8],
esds: &mut ES_Descriptor,
strictness: ParseStrictness,
) -> 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)?;
}
/// See MP4 (ISO 14496-14:2020) § 6.7.2
fn read_esds<T: Read>(src: &mut BMFFBox<T>, strictness: ParseStrictness) -> 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, strictness)?;
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 Status::DflaFlagsNonzero.into();
}
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 Status::DflaMissingMetadata.into();
} else if blocks[0].block_type != 0 {
return Status::DflaStreamInfoNotFirst.into();
} else if blocks[0].data.len() != 34 {
return Status::DflaStreamInfoBadSize.into();
}
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())?;
// 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 Status::DopsOpusHeadWriteErr.into();
}
}
}
// 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 Status::DopsChannelMappingWriteErr.into();
}
}
}
}
};
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 Status::AlacFlagsNonzero.into();
}
let length = match src.bytes_left() {
x @ 24 | x @ 48 => x,
_ => {
return Status::AlacBadMagicCookieSize.into();
}
};
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 Status::HdlrUnsupportedVersion.into();
}
let pre_defined = be_u32(src)?;
if pre_defined != 0 {
fail_with_status_if(
strictness == ParseStrictness::Strict,
Status::HdlrPredefinedNonzero,
)?;
}
let handler_type = FourCC::from(be_u32(src)?);
for _ in 1..=3 {
let reserved = be_u32(src)?;
if reserved != 0 {
fail_with_status_if(
strictness == ParseStrictness::Strict,
Status::HdlrReservedNonzero,
)?;
}
}
match std::str::from_utf8(src.read_into_try_vec()?.as_slice()) {
Ok(name) => {
// `name` must be nul-terminated and any trailing bytes after the first nul ignored.
// See https://github.com/MPEGGroup/FileFormat/issues/35
if !name.bytes().any(|b| b == b'\0') {
fail_with_status_if(
strictness != ParseStrictness::Permissive,
Status::HdlrNameNoNul,
)?;
}
}
Err(_) => fail_with_status_if(
strictness != ParseStrictness::Permissive,
Status::HdlrNameNotUtf8,
)?,
}
Ok(HandlerBox { handler_type })
}
/// Parse an video description inside an stsd box.
fn read_video_sample_entry<T: Read>(
src: &mut BMFFBox<T>,
strictness: ParseStrictness,
) -> 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,
BoxType::HEV1SampleEntry | BoxType::HVC1SampleEntry => CodecType::HEVC,
_ => {
debug!("Unsupported video codec, box {name:?} found");
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 pixel_aspect_ratio = None;
let mut colour_info = None;
let mut colr_types_seen = TryVec::<FourCC>::new();
let mut hdr_mastering_display = None;
let mut hdr_content_light_level = 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 Status::StsdBadVideoSampleEntry.into();
}
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 Status::StsdBadVideoSampleEntry.into();
}
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!("{h263_dec_spec_struc:?} (h263DecSpecStruc)");
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 Status::StsdBadVideoSampleEntry.into();
}
let vpcc = read_vpcc(&mut b)?;
codec_specific = Some(VideoCodecSpecific::VPxConfig(vpcc));
}
BoxType::AV1CodecConfigurationBox => {
if name != BoxType::AV1SampleEntry && name != BoxType::ProtectedVisualSampleEntry {
return Status::StsdBadVideoSampleEntry.into();
}
let av1c = read_av1c(&mut b)?;
codec_specific = Some(VideoCodecSpecific::AV1Config(av1c));
}
BoxType::ESDBox => {
if name != BoxType::MP4VideoSampleEntry || codec_specific.is_some() {
return Status::StsdBadVideoSampleEntry.into();
}
#[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, ParseStrictness::Normal)?;
codec_specific =
Some(VideoCodecSpecific::ESDSConfig(esds.decoder_specific_data));
}
}
BoxType::ProtectionSchemeInfoBox => {
if name != BoxType::ProtectedVisualSampleEntry {
return Status::StsdBadVideoSampleEntry.into();
}
let sinf = read_sinf(&mut b)?;
debug!("{sinf:?} (sinf)");
protection_info.push(sinf)?;
}
BoxType::HEVCConfigurationBox => {
if (name != BoxType::HEV1SampleEntry
&& name != BoxType::HVC1SampleEntry
&& name != BoxType::ProtectedVisualSampleEntry)
|| codec_specific.is_some()
{
return Status::StsdBadVideoSampleEntry.into();
}
let hvcc_size = b
.head
.size
.checked_sub(b.head.offset)
.expect("offset invalid");
let hvcc = read_buf(&mut b.content, hvcc_size)?;
debug!("{hvcc:?} (hvcc)");
codec_specific = Some(VideoCodecSpecific::HEVCConfig(hvcc));
}
BoxType::PixelAspectRatioBox => {
let pasp = read_pasp(&mut b)?;
let aspect_ratio = pasp.h_spacing as f32 / pasp.v_spacing as f32;
let is_valid_aspect_ratio = |value: f32| -> bool { value > 0.0 && value < 10.0 };
// Only set pixel_aspect_ratio if it is valid
if is_valid_aspect_ratio(aspect_ratio) {
pixel_aspect_ratio = Some(aspect_ratio);
}
debug!("Parsed pasp box: {pasp:?}, PAR {pixel_aspect_ratio:?}");
}
BoxType::ColourInformationBox => {
// ISO/IEC 14496-12:2015 § 12.1.5.1 permits one or more colr boxes
// in a VisualSampleEntry (at most one for a given colour_type) and
// assigns them no normative behaviour; a reader may keep the first
// (most accurate) and ignore the rest. Only reject a repeated
// colour_type, and only under Strict.
let colour_type = FourCC::from(be_u32(&mut b)?.to_be_bytes());
if colr_types_seen.contains(&colour_type) {
warn!(
"Multiple {colour_type:?} colr boxes in video sample entry, keeping first"
);
fail_with_status_if(
strictness == ParseStrictness::Strict,
Status::ColrBadQuantityBMFF,
)?;
skip_box_remain(&mut b)?;
} else {
colr_types_seen.push(colour_type.clone())?;
if colour_info.is_some() {
debug!("Already have colour info, skipping {colour_type:?} colr box");
skip_box_remain(&mut b)?;
} else if let ParsedColourInformation::Supported(colr) =
read_colr(&mut b, colour_type.value, strictness)?
{
debug!("Parsed colr box: {colr:?}");
colour_info = Some(colr);
}
}
}
BoxType::MasteringDisplayColourVolumeBox => {
let mdcv = read_mdcv(&mut b)?;
debug!("Parsed mdcv box: {mdcv:?}");
hdr_mastering_display = Some(mdcv);
}
BoxType::ContentLightLevelBox => {
let clli = read_clli(&mut b)?;
debug!("Parsed clli box: {clli:?}");
hdr_content_light_level = Some(clli);
}
_ => {
debug!("Unsupported video codec, box {:?} found", b.head.name);
skip_box_content(&mut b)?;
}
}
check_parser_state!(b.content);
}
/// 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>,
strictness: ParseStrictness,
) -> 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)?;
// 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,
};
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(())
}
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 Status::ReadBufErr.into();
}
// 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 = DataBox::at_offset(u64::MAX - 1, vec![1; 5]);
let extent = Extent::ToEnd { offset: u64::MAX };
¤ Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.0.217Bemerkung:
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.