Source code

Revision control

Copy as Markdown

Other Tools

//! Module for parsing ISO Base Media Format aka video/mp4 streams.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// `clippy::upper_case_acronyms` is a nightly-only lint as of 2021-03-15, so we
// allow `clippy::unknown_clippy_lints` to ignore it on stable - but
// `clippy::unknown_clippy_lints` has been renamed in nightly, so we need to
// allow `renamed_and_removed_lints` to ignore a warning for that.
#![allow(renamed_and_removed_lints)]
#![allow(clippy::unknown_clippy_lints)]
#![allow(clippy::upper_case_acronyms)]
#[macro_use]
extern crate log;
extern crate bitreader;
extern crate byteorder;
extern crate fallible_collections;
extern crate num_traits;
use bitreader::{BitReader, ReadInto};
use byteorder::{ReadBytesExt, WriteBytesExt};
use fallible_collections::TryRead;
use fallible_collections::TryReserveError;
use num_traits::Num;
use std::convert::{TryFrom, TryInto as _};
use std::fmt;
use std::io::Cursor;
use std::io::{Read, Take};
#[macro_use]
mod macros;
mod boxes;
use boxes::{BoxType, FourCC};
// Unit tests.
#[cfg(test)]
mod tests;
#[cfg(feature = "unstable-api")]
pub mod unstable;
/// The HEIF image and image collection brand
/// The 'mif1' brand indicates structural requirements on files
/// See HEIF (ISO 23008-12:2017) § 10.2.1
pub const 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
pub const MSF1_BRAND: FourCC = FourCC { value: *b"msf1" };
/// The brand to identify AV1 image items
/// The 'avif' brand indicates structural requirements on files
pub const AVIF_BRAND: FourCC = FourCC { value: *b"avif" };
/// The brand to identify AVIF image sequences
/// The 'avis' brand indicates structural requirements on files
pub const AVIS_BRAND: FourCC = FourCC { value: *b"avis" };
/// 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.
pub trait ToUsize {
fn to_usize(self) -> usize;
}
/// Statically verify that the given type can fit within a `usize`.
/// If the size won't fit on the given platform, this will fail at compile time, but if a type
/// which can fail TryInto<usize> is used, it may panic.
macro_rules! impl_to_usize_from {
( $from_type:ty ) => {
impl ToUsize for $from_type {
fn to_usize(self) -> usize {
static_assertions::const_assert!(
std::mem::size_of::<$from_type>() <= std::mem::size_of::<usize>()
);
self.try_into().expect(concat!(
stringify!($from_type),
" -> usize conversion failed"
))
}
}
};
}
impl_to_usize_from!(u8);
impl_to_usize_from!(u16);
impl_to_usize_from!(u32);
/// Indicate the current offset (i.e., bytes already read) in a reader
trait Offset {
fn offset(&self) -> u64;
}
/// Wraps a reader to track the current offset
struct OffsetReader<'a, T: 'a> {
reader: &'a mut T,
offset: u64,
}
impl<'a, T> OffsetReader<'a, T> {
fn new(reader: &'a mut T) -> Self {
Self { reader, offset: 0 }
}
}
impl<'a, T> Offset for OffsetReader<'a, T> {
fn offset(&self) -> u64 {
self.offset
}
}
impl<'a, T: Read> Read for OffsetReader<'a, T> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let bytes_read = self.reader.read(buf)?;
self.offset = self
.offset
.checked_add(bytes_read.to_u64())
.expect("total bytes read too large for offset type");
Ok(bytes_read)
}
}
pub type TryVec<T> = fallible_collections::TryVec<T>;
pub type TryString = fallible_collections::TryVec<u8>;
pub type TryHashMap<K, V> = fallible_collections::TryHashMap<K, V>;
pub type TryBox<T> = fallible_collections::TryBox<T>;
// To ensure we don't use stdlib allocating types by accident
#[allow(dead_code)]
struct Vec;
#[allow(dead_code)]
struct Box;
#[allow(dead_code)]
struct HashMap;
#[allow(dead_code)]
struct String;
/// The return value to the C API
/// Any detail that needs to be communicated to the caller must be encoded here
/// since the [`Error`] type's associated data is part of the FFI.
#[repr(C)]
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum Status {
Ok = 0,
BadArg = 1,
Invalid = 2,
Unsupported = 3,
Eof = 4,
Io = 5,
Oom = 6,
MissingBrand,
FtypNotFirst,
NoImage,
MultipleMoov,
NoMoov,
LselNoEssential,
A1opNoEssential,
A1lxEssential,
TxformNoEssential,
NoPrimaryItem,
ImageItemType,
ItemTypeMissing,
ConstructionMethod,
ItemLocNotFound,
NoItemDataBox,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Feature {
A1lx,
A1op,
Auxc,
Av1c,
Avis,
Clap,
Colr,
Grid,
Imir,
Ipro,
Irot,
Ispe,
Lsel,
Pasp,
Pixi,
}
impl Feature {
fn supported(self) -> bool {
match self {
Self::Auxc
| Self::Av1c
| Self::Colr
| Self::Imir
| Self::Irot
| Self::Ispe
| Self::Pasp
| Self::Pixi => true,
Self::A1lx
| Self::A1op
| Self::Clap
| Self::Grid
| Self::Ipro
| Self::Lsel
| Self::Avis => false,
}
}
}
impl TryFrom<&ItemProperty> for Feature {
type Error = Error;
fn try_from(item_property: &ItemProperty) -> Result<Self, Self::Error> {
Ok(match item_property {
ItemProperty::AuxiliaryType(_) => Self::Auxc,
ItemProperty::AV1Config(_) => Self::Av1c,
ItemProperty::Channels(_) => Self::Pixi,
ItemProperty::CleanAperture => Self::Clap,
ItemProperty::Colour(_) => Self::Colr,
ItemProperty::ImageSpatialExtents(_) => Self::Ispe,
ItemProperty::LayeredImageIndexing => Self::A1lx,
ItemProperty::LayerSelection => Self::Lsel,
ItemProperty::Mirroring(_) => Self::Imir,
ItemProperty::OperatingPointSelector => Self::A1op,
ItemProperty::PixelAspectRatio(_) => Self::Pasp,
ItemProperty::Rotation(_) => Self::Irot,
item_property => {
error!("No known Feature variant for {:?}", item_property);
return Err(Error::Unsupported("missing Feature fox ItemProperty"));
}
})
}
}
/// 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)]
pub struct UnsupportedFeatures(u32);
impl UnsupportedFeatures {
pub fn new() -> Self {
Self(0x0)
}
pub fn into_bitfield(&self) -> u32 {
self.0
}
fn feature_to_bitfield(feature: Feature) -> u32 {
let index = feature as usize;
assert!(
u8::BITS.to_usize() * std::mem::size_of::<Self>() > index,
"You're gonna need a bigger bitfield"
);
let bitfield = 1u32 << index;
assert_eq!(bitfield.count_ones(), 1);
bitfield
}
pub fn insert(&mut self, feature: Feature) {
warn!("Unsupported feature: {:?}", feature);
self.0 |= Self::feature_to_bitfield(feature);
}
pub fn contains(&self, feature: Feature) -> bool {
self.0 & Self::feature_to_bitfield(feature) != 0x0
}
pub fn is_empty(&self) -> bool {
self.0 == 0x0
}
}
impl<T> From<Status> for Result<T> {
/// A convenience method to enable shortcuts like
/// ```
/// # extern crate mp4parse;
/// # use mp4parse::{Result,Status};
/// # let _: Result<()> =
/// Status::MissingBrand.into();
/// ```
/// instead of
/// ```
/// # extern crate mp4parse;
/// # use mp4parse::{Error,Result,Status};
/// # let _: Result<()> =
/// Err(Error::from(Status::MissingBrand));
/// ```
/// 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:InvalidDataDetail errors")
}
Status::MissingBrand
| Status::FtypNotFirst
| Status::NoImage
| Status::MultipleMoov
| Status::NoMoov
| Status::LselNoEssential
| Status::A1opNoEssential
| Status::A1lxEssential
| Status::TxformNoEssential
| Status::NoPrimaryItem
| Status::ImageItemType
| Status::ItemTypeMissing
| Status::ConstructionMethod
| Status::ItemLocNotFound
| Status::NoItemDataBox => Self::InvalidDataDetail(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::MissingBrand => {
"The file shall list 'avif' or 'avis' in the compatible_brands field
of the FileTypeBox \
}
Status::FtypNotFirst => {
"The FileTypeBox shall be placed as early as possible in the file \
per ISOBMFF (ISO 14496-12:2020) § 4.3.1"
}
Status::NoImage => "No primary image or image sequence found",
Status::NoMoov => {
"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::MultipleMoov => {
"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::LselNoEssential => {
"LayerSelectorProperty (lsel) shall be marked as essential \
per HEIF (ISO/IEC 23008-12:2017) § 6.5.11.1"
}
Status::A1opNoEssential => {
"OperatingPointSelectorProperty (a1op) shall be marked as essential \
}
Status::A1lxEssential => {
"AV1LayeredImageIndexingProperty (a1lx) shall not be marked as essential \
}
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::NoPrimaryItem => {
"Missing required PrimaryItemBox (pitm), required \
per HEIF (ISO/IEC 23008-12:2017) § 10.2.1"
}
Status::ImageItemType => {
"Image item type is neither 'av01' nor 'grid'"
}
Status::ItemTypeMissing => {
"No ItemInfoEntry for item_ID"
}
Status::ConstructionMethod => {
"construction_method shall be 0 (file) or 1 (idat) per MIAF (ISO 23000-22:2019) § 7.2.1.7"
}
Status::ItemLocNotFound => {
"ItemLocationBox (iloc) contains an extent not present in any mdat or idat box"
}
Status::NoItemDataBox => {
"ItemLocationBox (iloc) construction_method indicates 1 (idat), \
but no idat box is present."
}
}
}
}
impl From<Error> for Status {
fn from(error: Error) -> Self {
match error {
Error::InvalidData(_) => Self::Invalid,
Error::Unsupported(_) => Self::Unsupported,
Error::InvalidDataDetail(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::NoMoov => Self::NoMoov,
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)]
pub enum Error {
/// Parse error caused by corrupt or malformed data.
InvalidData(&'static str),
/// Similar to [`Self::InvalidData`], but for errors that have a specific
/// [`Status`] variant for communicating the detail across FFI.
/// See the helper [`From<Status> for Error`](enum.Error.html#impl-From<Status>)
InvalidDataDetail(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.
NoMoov,
/// Out of memory
OutOfMemory,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl std::error::Error for Error {}
impl From<bitreader::BitReaderError> for Error {
fn from(_: bitreader::BitReaderError) -> Error {
Error::InvalidData("invalid data")
}
}
impl From<std::io::Error> for Error {
fn from(err: std::io::Error) -> Error {
match err.kind() {
std::io::ErrorKind::UnexpectedEof => Error::UnexpectedEOF,
_ => Error::Io(err),
}
}
}
impl From<std::string::FromUtf8Error> for Error {
fn from(_: std::string::FromUtf8Error) -> Error {
Error::InvalidData("invalid utf8")
}
}
impl From<std::str::Utf8Error> for Error {
fn from(_: std::str::Utf8Error) -> Error {
Error::InvalidData("invalid utf8")
}
}
impl From<std::num::TryFromIntError> for Error {
fn from(_: std::num::TryFromIntError) -> Error {
Error::Unsupported("integer conversion failed")
}
}
impl From<Error> for std::io::Error {
fn from(err: Error) -> Self {
let kind = match err {
Error::InvalidData(_) => std::io::ErrorKind::InvalidData,
Error::UnexpectedEOF => std::io::ErrorKind::UnexpectedEof,
Error::Io(io_err) => return io_err,
_ => std::io::ErrorKind::Other,
};
Self::new(kind, err)
}
}
impl From<TryReserveError> for Error {
fn from(_: TryReserveError) -> Error {
Error::OutOfMemory
}
}
/// Result shorthand using our Error enum.
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Basic ISO box structure.
///
/// mp4 files are a sequence of possibly-nested 'box' structures. Each box
/// begins with a header describing the length of the box's data and a
/// four-byte box type which identifies the type of the box. Together these
/// are enough to interpret the contents of that section of the file.
///
/// See ISOBMFF (ISO 14496-12:2020) § 4.2
#[derive(Debug, Clone, Copy)]
struct BoxHeader {
/// Box type.
name: BoxType,
/// Size of the box in bytes.
size: u64,
/// Offset to the start of the contained data (or header size).
offset: u64,
/// Uuid for extended type.
uuid: Option<[u8; 16]>,
}
impl BoxHeader {
const MIN_SIZE: u64 = 8; // 4-byte size + 4-byte type
const MIN_LARGE_SIZE: u64 = 16; // 4-byte size + 4-byte type + 16-byte size
}
/// File type box 'ftyp'.
#[derive(Debug)]
struct FileTypeBox {
major_brand: FourCC,
minor_version: u32,
compatible_brands: TryVec<FourCC>,
}
impl FileTypeBox {
fn contains(&self, brand: &FourCC) -> bool {
self.compatible_brands.contains(brand) || self.major_brand == *brand
}
}
/// Movie header box 'mvhd'.
#[derive(Debug)]
struct MovieHeaderBox {
pub timescale: u32,
duration: u64,
}
#[derive(Debug, Clone, Copy)]
pub struct Matrix {
pub a: i32, // 16.16 fix point
pub b: i32, // 16.16 fix point
pub u: i32, // 2.30 fix point
pub c: i32, // 16.16 fix point
pub d: i32, // 16.16 fix point
pub v: i32, // 2.30 fix point
pub x: i32, // 16.16 fix point
pub y: i32, // 16.16 fix point
pub w: i32, // 2.30 fix point
}
/// Track header box 'tkhd'
#[derive(Debug, Clone)]
pub struct TrackHeaderBox {
track_id: u32,
pub disabled: bool,
pub duration: u64,
pub width: u32,
pub height: u32,
pub matrix: Matrix,
}
/// Edit list box 'elst'
#[derive(Debug)]
struct EditListBox {
edits: TryVec<Edit>,
}
#[derive(Debug)]
struct Edit {
segment_duration: u64,
media_time: i64,
media_rate_integer: i16,
media_rate_fraction: i16,
}
/// Media header box 'mdhd'
#[derive(Debug)]
struct MediaHeaderBox {
timescale: u32,
duration: u64,
}
// Chunk offset box 'stco' or 'co64'
#[derive(Debug)]
pub struct ChunkOffsetBox {
pub offsets: TryVec<u64>,
}
// Sync sample box 'stss'
#[derive(Debug)]
pub struct SyncSampleBox {
pub samples: TryVec<u32>,
}
// Sample to chunk box 'stsc'
#[derive(Debug)]
pub struct SampleToChunkBox {
pub samples: TryVec<SampleToChunk>,
}
#[derive(Debug)]
pub struct SampleToChunk {
pub first_chunk: u32,
pub samples_per_chunk: u32,
pub sample_description_index: u32,
}
// Sample size box 'stsz'
#[derive(Debug)]
pub struct SampleSizeBox {
pub sample_size: u32,
pub sample_sizes: TryVec<u32>,
}
// Time to sample box 'stts'
#[derive(Debug)]
pub struct TimeToSampleBox {
pub samples: TryVec<Sample>,
}
#[repr(C)]
#[derive(Debug)]
pub struct Sample {
pub sample_count: u32,
pub sample_delta: u32,
}
#[derive(Debug, Clone, Copy)]
pub enum TimeOffsetVersion {
Version0(u32),
Version1(i32),
}
#[derive(Debug, Clone)]
pub struct TimeOffset {
pub sample_count: u32,
pub time_offset: TimeOffsetVersion,
}
#[derive(Debug)]
pub struct CompositionOffsetBox {
pub samples: TryVec<TimeOffset>,
}
// Handler reference box 'hdlr'
#[derive(Debug)]
struct HandlerBox {
handler_type: FourCC,
}
// Sample description box 'stsd'
#[derive(Debug)]
pub struct SampleDescriptionBox {
pub descriptions: TryVec<SampleEntry>,
}
#[derive(Debug)]
pub enum SampleEntry {
Audio(AudioSampleEntry),
Video(VideoSampleEntry),
Unknown,
}
/// An Elementary Stream Descriptor
/// See MPEG-4 Systems (ISO 14496-1:2010) § 7.2.6.5
#[allow(non_camel_case_types)]
#[derive(Debug, Default)]
pub struct ES_Descriptor {
pub audio_codec: CodecType,
pub audio_object_type: Option<u16>,
pub extended_audio_object_type: Option<u16>,
pub audio_sample_rate: Option<u32>,
pub audio_channel_count: Option<u16>,
#[cfg(feature = "mp4v")]
pub video_codec: CodecType,
pub codec_esds: TryVec<u8>,
pub decoder_specific_data: TryVec<u8>, // Data in DECODER_SPECIFIC_TAG
}
#[allow(non_camel_case_types)]
#[derive(Debug)]
pub enum AudioCodecSpecific {
ES_Descriptor(ES_Descriptor),
FLACSpecificBox(FLACSpecificBox),
OpusSpecificBox(OpusSpecificBox),
ALACSpecificBox(ALACSpecificBox),
MP3,
LPCM,
#[cfg(feature = "3gpp")]
AMRSpecificBox(TryVec<u8>),
}
#[derive(Debug)]
pub struct AudioSampleEntry {
pub codec_type: CodecType,
data_reference_index: u16,
pub channelcount: u32,
pub samplesize: u16,
pub samplerate: f64,
pub codec_specific: AudioCodecSpecific,
pub protection_info: TryVec<ProtectionSchemeInfoBox>,
}
#[derive(Debug)]
pub enum VideoCodecSpecific {
AVCConfig(TryVec<u8>),
VPxConfig(VPxConfigBox),
AV1Config(AV1ConfigBox),
ESDSConfig(TryVec<u8>),
H263Config(TryVec<u8>),
}
#[derive(Debug)]
pub struct VideoSampleEntry {
pub codec_type: CodecType,
data_reference_index: u16,
pub width: u16,
pub height: u16,
pub codec_specific: VideoCodecSpecific,
pub protection_info: TryVec<ProtectionSchemeInfoBox>,
}
/// Represent a Video Partition Codec Configuration 'vpcC' box (aka vp9). The meaning of each
/// field is covered in detail in "VP Codec ISO Media File Format Binding".
#[derive(Debug)]
pub struct VPxConfigBox {
/// An integer that specifies the VP codec profile.
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.
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.
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.
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).
video_full_range_flag: bool,
/// This is not used for VP8 and VP9 . Intended for binary codec initialization data.
pub codec_init: TryVec<u8>,
}
#[derive(Debug)]
pub struct AV1ConfigBox {
pub profile: u8,
pub level: u8,
pub tier: u8,
pub bit_depth: u8,
pub monochrome: bool,
pub chroma_subsampling_x: u8,
pub chroma_subsampling_y: u8,
pub chroma_sample_position: u8,
pub initial_presentation_delay_present: bool,
pub initial_presentation_delay_minus_one: u8,
// The raw config contained in the av1c box. Because some decoders accept this data as a binary
// blob, rather than as structured data, we store the blob here for convenience.
pub raw_config: TryVec<u8>,
}
impl AV1ConfigBox {
const CONFIG_OBUS_OFFSET: usize = 4;
pub fn config_obus(&self) -> &[u8] {
&self.raw_config[Self::CONFIG_OBUS_OFFSET..]
}
}
#[derive(Debug)]
pub struct FLACMetadataBlock {
pub block_type: u8,
pub data: TryVec<u8>,
}
/// Represents a FLACSpecificBox 'dfLa'
#[derive(Debug)]
pub struct FLACSpecificBox {
version: u8,
pub blocks: TryVec<FLACMetadataBlock>,
}
#[derive(Debug)]
struct ChannelMappingTable {
stream_count: u8,
coupled_count: u8,
channel_mapping: TryVec<u8>,
}
/// Represent an OpusSpecificBox 'dOps'
#[derive(Debug)]
pub struct OpusSpecificBox {
pub version: u8,
output_channel_count: u8,
pre_skip: u16,
input_sample_rate: u32,
output_gain: i16,
channel_mapping_family: u8,
channel_mapping_table: Option<ChannelMappingTable>,
}
/// Represent an ALACSpecificBox 'alac'
#[derive(Debug)]
pub struct ALACSpecificBox {
version: u8,
pub data: TryVec<u8>,
}
#[derive(Debug)]
pub struct MovieExtendsBox {
pub fragment_duration: Option<MediaScaledTime>,
}
pub type ByteData = TryVec<u8>;
#[derive(Debug, Default)]
pub struct ProtectionSystemSpecificHeaderBox {
pub system_id: ByteData,
pub kid: TryVec<ByteData>,
pub data: ByteData,
// The entire pssh box (include header) required by Gecko.
pub box_content: ByteData,
}
#[derive(Debug, Default, Clone)]
pub struct SchemeTypeBox {
pub scheme_type: FourCC,
pub scheme_version: u32,
}
#[derive(Debug, Default)]
pub struct TrackEncryptionBox {
pub is_encrypted: u8,
pub iv_size: u8,
pub kid: TryVec<u8>,
// Members for pattern encryption schemes
pub crypt_byte_block_count: Option<u8>,
pub skip_byte_block_count: Option<u8>,
pub constant_iv: Option<TryVec<u8>>,
// End pattern encryption scheme members
}
#[derive(Debug, Default)]
pub struct ProtectionSchemeInfoBox {
pub original_format: FourCC,
pub scheme_type: Option<SchemeTypeBox>,
pub tenc: Option<TrackEncryptionBox>,
}
/// Represents a userdata box 'udta'.
/// Currently, only the metadata atom 'meta'
/// is parsed.
#[derive(Debug, Default)]
pub struct UserdataBox {
pub meta: Option<MetadataBox>,
}
/// Represents possible contents of the
/// ©gen or gnre atoms within a metadata box.
/// 'udta.meta.ilst' may only have either a
/// standard genre box 'gnre' or a custom
/// genre box '©gen', but never both at once.
#[derive(Debug, PartialEq)]
pub enum Genre {
/// A standard ID3v1 numbered genre.
StandardGenre(u8),
/// Any custom genre string.
CustomGenre(TryString),
}
/// Represents the contents of a 'stik'
/// atom that indicates content types within
/// iTunes.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum MediaType {
/// Movie is stored as 0 in a 'stik' atom.
Movie, // 0
/// Normal is stored as 1 in a 'stik' atom.
Normal, // 1
/// AudioBook is stored as 2 in a 'stik' atom.
AudioBook, // 2
/// WhackedBookmark is stored as 5 in a 'stik' atom.
WhackedBookmark, // 5
/// MusicVideo is stored as 6 in a 'stik' atom.
MusicVideo, // 6
/// ShortFilm is stored as 9 in a 'stik' atom.
ShortFilm, // 9
/// TVShow is stored as 10 in a 'stik' atom.
TVShow, // 10
/// Booklet is stored as 11 in a 'stik' atom.
Booklet, // 11
/// An unknown 'stik' value.
Unknown(u8),
}
/// Represents the parental advisory rating on the track,
/// stored within the 'rtng' atom.
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum AdvisoryRating {
/// Clean is always stored as 2 in an 'rtng' atom.
Clean, // 2
/// A value of 0 in an 'rtng' atom indicates 'Inoffensive'
Inoffensive, // 0
/// Any non 2 or 0 value in 'rtng' indicates the track is explicit.
Explicit(u8),
}
/// Represents the contents of 'ilst' atoms within
/// a metadata box 'meta', parsed as iTunes metadata using
/// the conventional tags.
#[derive(Debug, Default)]
pub struct MetadataBox {
/// The album name, '©alb'
pub album: Option<TryString>,
/// The artist name '©art' or '©ART'
pub artist: Option<TryString>,
/// The album artist 'aART'
pub album_artist: Option<TryString>,
/// Track comments '©cmt'
pub comment: Option<TryString>,
/// The date or year field '©day'
///
/// This is stored as an arbitrary string,
/// and may not necessarily be in a valid date
/// format.
pub year: Option<TryString>,
/// The track title '©nam'
pub title: Option<TryString>,
/// The track genre '©gen' or 'gnre'.
pub genre: Option<Genre>,
/// The track number 'trkn'.
pub track_number: Option<u8>,
/// The disc number 'disk'
pub disc_number: Option<u8>,
/// The total number of tracks on the disc,
/// stored in 'trkn'
pub total_tracks: Option<u8>,
/// The total number of discs in the album,
/// stored in 'disk'
pub total_discs: Option<u8>,
/// The composer of the track '©wrt'
pub composer: Option<TryString>,
/// The encoder used to create this track '©too'
pub encoder: Option<TryString>,
/// The encoded-by settingo this track '©enc'
pub encoded_by: Option<TryString>,
/// The tempo or BPM of the track 'tmpo'
pub beats_per_minute: Option<u8>,
/// Copyright information of the track 'cprt'
pub copyright: Option<TryString>,
/// Whether or not this track is part of a compilation 'cpil'
pub compilation: Option<bool>,
/// The advisory rating of this track 'rtng'
pub advisory: Option<AdvisoryRating>,
/// The personal rating of this track, 'rate'.
///
/// This is stored in the box as string data, but
/// the format is an integer percentage from 0 - 100,
/// where 100 is displayed as 5 stars out of 5.
pub rating: Option<TryString>,
/// The grouping this track belongs to '©grp'
pub grouping: Option<TryString>,
/// The media type of this track 'stik'
pub media_type: Option<MediaType>, // stik
/// Whether or not this track is a podcast 'pcst'
pub podcast: Option<bool>,
/// The category of ths track 'catg'
pub category: Option<TryString>,
/// The podcast keyword 'keyw'
pub keyword: Option<TryString>,
/// The podcast url 'purl'
pub podcast_url: Option<TryString>,
/// The podcast episode GUID 'egid'
pub podcast_guid: Option<TryString>,
/// The description of the track 'desc'
pub description: Option<TryString>,
/// The long description of the track 'ldes'.
///
/// Unlike other string fields, the long description field
/// can be longer than 256 characters.
pub long_description: Option<TryString>,
/// The lyrics of the track '©lyr'.
///
/// Unlike other string fields, the lyrics field
/// can be longer than 256 characters.
pub lyrics: Option<TryString>,
/// The name of the TV network this track aired on 'tvnn'.
pub tv_network_name: Option<TryString>,
/// The name of the TV Show for this track 'tvsh'.
pub tv_show_name: Option<TryString>,
/// The name of the TV Episode for this track 'tven'.
pub tv_episode_name: Option<TryString>,
/// The number of the TV Episode for this track 'tves'.
pub tv_episode_number: Option<u8>,
/// The season of the TV Episode of this track 'tvsn'.
pub tv_season: Option<u8>,
/// The date this track was purchased 'purd'.
pub purchase_date: Option<TryString>,
/// Whether or not this track supports gapless playback 'pgap'
pub gapless_playback: Option<bool>,
/// Any cover artwork attached to this track 'covr'
///
/// 'covr' is unique in that it may contain multiple 'data' sub-entries,
/// each an image file. Here, each subentry's raw binary data is exposed,
/// which may contain image data in JPEG or PNG format.
pub cover_art: Option<TryVec<TryVec<u8>>>,
/// The owner of the track 'ownr'
pub owner: Option<TryString>,
/// Whether or not this track is HD Video 'hdvd'
pub hd_video: Option<bool>,
/// The name of the track to sort by 'sonm'
pub sort_name: Option<TryString>,
/// The name of the album to sort by 'soal'
pub sort_album: Option<TryString>,
/// The name of the artist to sort by 'soar'
pub sort_artist: Option<TryString>,
/// The name of the album artist to sort by 'soaa'
pub sort_album_artist: Option<TryString>,
/// The name of the composer to sort by 'soco'
pub sort_composer: Option<TryString>,
/// Metadata
#[cfg(feature = "meta-xml")]
pub xml: Option<XmlBox>,
}
/// See ISOBMFF (ISO 14496-12:2020) § 8.11.2.1
#[cfg(feature = "meta-xml")]
#[derive(Debug)]
pub enum XmlBox {
/// XML metadata
StringXmlBox(TryString),
/// Binary XML metadata
BinaryXmlBox(TryVec<u8>),
}
/// Internal data structures.
#[derive(Debug, Default)]
pub struct MediaContext {
pub timescale: Option<MediaTimeScale>,
/// Tracks found in the file.
pub tracks: TryVec<Track>,
pub mvex: Option<MovieExtendsBox>,
pub psshs: TryVec<ProtectionSystemSpecificHeaderBox>,
pub userdata: Option<Result<UserdataBox>>,
#[cfg(feature = "meta-xml")]
pub metadata: Option<Result<MetadataBox>>,
}
/// An ISOBMFF item as described by an iloc box. For the sake of avoiding copies,
/// this can either be represented by the `Location` variant, which indicates
/// where the data exists within a `MediaDataBox` stored separately, or the
/// `Data` variant which owns the data. Unfortunately, it's not simple to
/// represent this as a [`std::borrow::Cow`], or other reference-based type, because
/// multiple instances may references different parts of the same [`MediaDataBox`]
/// and we want to avoid the copy that splitting the storage would entail.
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,
image_data: IsobmffItem,
}
impl AvifItem {
fn with_inline_data(id: ItemId) -> Self {
Self {
id,
image_data: IsobmffItem::Data(TryVec::new()),
}
}
}
#[derive(Debug)]
pub struct AvifContext {
/// Level of deviation from the specification before failing the parse
strictness: ParseStrictness,
/// Referred to by the `IsobmffItem::*Location` variants of the `AvifItem`s in this struct
media_storage: TryVec<MediaDataBox>,
/// 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,
/// True if a `moov` box is present
pub has_sequence: bool,
/// A collection of unsupported features encountered during the parse
pub unsupported_features: UnsupportedFeatures,
}
impl AvifContext {
pub fn primary_item_coded_data(&self) -> Option<&[u8]> {
self.primary_item
.as_ref()
.map(|item| self.item_as_slice(item))
}
pub fn primary_item_bits_per_channel(&self) -> Option<Result<&[u8]>> {
self.primary_item
.as_ref()
.map(|item| self.image_bits_per_channel(item.id))
}
pub fn alpha_item_coded_data(&self) -> Option<&[u8]> {
self.alpha_item
.as_ref()
.map(|item| self.item_as_slice(item))
}
pub fn alpha_item_bits_per_channel(&self) -> Option<Result<&[u8]>> {
self.alpha_item
.as_ref()
.map(|item| self.image_bits_per_channel(item.id))
}
fn image_bits_per_channel(&self, item_id: ItemId) -> Result<&[u8]> {
match self
.item_properties
.get(item_id, BoxType::PixelInformationBox)?
{
Some(ItemProperty::Channels(pixi)) => Ok(pixi.bits_per_channel.as_slice()),
Some(other_property) => panic!("property key mismatch: {:?}", other_property),
None => Ok(&[]),
}
}
pub fn spatial_extents_ptr(&self) -> Result<*const ImageSpatialExtentsProperty> {
if let Some(primary_item) = &self.primary_item {
match self
.item_properties
.get(primary_item.id, BoxType::ImageSpatialExtentsProperty)?
{
Some(ItemProperty::ImageSpatialExtents(ispe)) => Ok(ispe),
Some(other_property) => panic!("property key mismatch: {:?}", other_property),
None => {
fail_if(
self.strictness != ParseStrictness::Permissive,
"ispe is a mandatory property",
)?;
Ok(std::ptr::null())
}
}
} else {
Ok(std::ptr::null())
}
}
/// Returns None if there is no primary item or it has no associated NCLX colour boxes.
pub fn nclx_colour_information_ptr(&self) -> Option<Result<*const NclxColourInformation>> {
if let Some(primary_item) = &self.primary_item {
match self.item_properties.get_multiple(primary_item.id, |prop| {
matches!(prop, ItemProperty::Colour(ColourInformation::Nclx(_)))
}) {
Ok(nclx_colr_boxes) => match *nclx_colr_boxes.as_slice() {
[] => None,
[ItemProperty::Colour(ColourInformation::Nclx(nclx)), ..] => {
if nclx_colr_boxes.len() > 1 {
warn!("Multiple nclx colr boxes, using first");
}
Some(Ok(nclx))
}
_ => unreachable!("Expect only ColourInformation::Nclx(_) matches"),
},
Err(e) => Some(Err(e)),
}
} else {
None
}
}
/// Returns None if there is no primary item or it has no associated ICC colour boxes.
pub fn icc_colour_information(&self) -> Option<Result<&[u8]>> {
if let Some(primary_item) = &self.primary_item {
match self.item_properties.get_multiple(primary_item.id, |prop| {
matches!(prop, ItemProperty::Colour(ColourInformation::Icc(_, _)))
}) {
Ok(icc_colr_boxes) => match *icc_colr_boxes.as_slice() {
[] => None,
[ItemProperty::Colour(ColourInformation::Icc(icc, _)), ..] => {
if icc_colr_boxes.len() > 1 {
warn!("Multiple ICC profiles in colr boxes, using first");
}
Some(Ok(icc.bytes.as_slice()))
}
_ => unreachable!("Expect only ColourInformation::Icc(_) matches"),
},
Err(e) => Some(Err(e)),
}
} else {
None
}
}
pub fn image_rotation(&self) -> Result<ImageRotation> {
if let Some(primary_item) = &self.primary_item {
match self
.item_properties
.get(primary_item.id, BoxType::ImageRotation)?
{
Some(ItemProperty::Rotation(irot)) => Ok(*irot),
Some(other_property) => panic!("property key mismatch: {:?}", other_property),
None => Ok(ImageRotation::D0),
}
} else {
Ok(ImageRotation::D0)
}
}
pub fn image_mirror_ptr(&self) -> Result<*const ImageMirror> {
if let Some(primary_item) = &self.primary_item {
match self
.item_properties
.get(primary_item.id, BoxType::ImageMirror)?
{
Some(ItemProperty::Mirroring(imir)) => Ok(imir),
Some(other_property) => panic!("property key mismatch: {:?}", other_property),
None => Ok(std::ptr::null()),
}
} else {
Ok(std::ptr::null())
}
}
pub fn pixel_aspect_ratio_ptr(&self) -> Result<*const PixelAspectRatio> {
if let Some(primary_item) = &self.primary_item {
match self
.item_properties
.get(primary_item.id, BoxType::PixelAspectRatioBox)?
{
Some(ItemProperty::PixelAspectRatio(pasp)) => Ok(pasp),
Some(other_property) => panic!("property key mismatch: {:?}", other_property),
None => Ok(std::ptr::null()),
}
} else {
Ok(std::ptr::null())
}
}
/// A helper for the various `AvifItem`s to expose a reference to the
/// underlying data while avoiding copies.
fn item_as_slice<'a>(&'a self, item: &'a AvifItem) -> &'a [u8] {
match &item.image_data {
IsobmffItem::MdatLocation(extent) => {
for mdat in &self.media_storage {
if let Some(slice) = mdat.get(extent) {
return slice;
}
}
unreachable!(
"IsobmffItem::Location requires the location exists in AvifContext::media_storage"
);
}
IsobmffItem::IdatLocation(_) => unimplemented!(),
IsobmffItem::Data(data) => data.as_slice(),
}
}
}
struct AvifMeta {
item_references: TryVec<SingleItemTypeReferenceBox>,
item_properties: ItemPropertiesBox,
/// Required for AvifImageType::Primary, but optional otherwise
/// See HEIF (ISO/IEC 23008-12:2017) § 7.1, 10.2.1
primary_item_id: Option<ItemId>,
item_infos: TryVec<ItemInfoEntry>,
iloc_items: TryHashMap<ItemId, ItemLocationBoxItem>,
item_data_box: Option<ItemDataBox>,
}
/// An Item Data Box
/// See ISOBMFF (ISO 14496-12:2020) § 8.11.11
struct ItemDataBox {
data: TryVec<u8>,
}
/// A Media Data Box
/// See ISOBMFF (ISO 14496-12:2020) § 8.1.1
struct MediaDataBox {
/// Offset of `data` from the beginning of the "file". See ConstructionMethod::File.
/// Note: the file may not be an actual file, read_avif supports any `&mut impl Read`
/// source for input. However we try to match the terminology used in the spec.
file_offset: u64,
data: TryVec<u8>,
}
impl fmt::Debug for MediaDataBox {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("MediaDataBox")
.field("file_offset", &self.file_offset)
.field("data", &format_args!("{} bytes", self.data.len()))
.finish()
}
}
fn u64_to_usize_logged(x: u64) -> Option<usize> {
match x.try_into() {
Ok(x) => Some(x),
Err(e) => {
error!("{:?} converting {:?}", e, x);
None
}
}
}
/// Generalizes the different data boxes a [`ItemLocationBoxItem`] can refer to
trait DataBox {
fn data(&self) -> &[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>;
/// Returns an appropriate variant of [`IsobmffItem`] to describe the extent
/// referencing data within this type of box.
fn location(&self, extent: &Extent) -> IsobmffItem;
/// 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..)
}
}
}
}
impl DataBox for ItemDataBox {
fn data(&self) -> &[u8] {
&self.data
}
fn start(&self, offset: u64) -> Option<usize> {
u64_to_usize_logged(offset)
}
fn location(&self, extent: &Extent) -> IsobmffItem {
IsobmffItem::IdatLocation(extent.clone())
}
}
impl DataBox for MediaDataBox {
fn data(&self) -> &[u8] {
&self.data
}
fn start(&self, offset: u64) -> Option<usize> {
let start = offset.checked_sub(self.file_offset);
if start.is_none() {
error!("Overflow subtracting {} + {}", offset, self.file_offset);
}
u64_to_usize_logged(start?)
}
fn location(&self, extent: &Extent) -> IsobmffItem {
IsobmffItem::MdatLocation(extent.clone())
}
}
#[cfg(test)]
mod media_data_box_tests {
use super::*;
impl MediaDataBox {
fn at_offset(file_offset: u64, data: std::vec::Vec<u8>) -> Self {
MediaDataBox {
file_offset,
data: data.into(),
}
}
}
#[test]
fn extent_with_length_before_mdat_returns_none() {
let mdat = MediaDataBox::at_offset(100, vec![1; 5]);
let extent = Extent::WithLength { offset: 0, len: 2 };
assert!(mdat.get(&extent).is_none());
}
#[test]
fn extent_to_end_before_mdat_returns_none() {
let mdat = MediaDataBox::at_offset(100, vec![1; 5]);
let extent = Extent::ToEnd { offset: 0 };
assert!(mdat.get(&extent).is_none());
}
#[test]
fn extent_with_length_crossing_front_mdat_boundary_returns_none() {
let mdat = MediaDataBox::at_offset(100, vec![1; 5]);
let extent = Extent::WithLength { offset: 99, len: 3 };
assert!(mdat.get(&extent).is_none());
}
#[test]
fn extent_with_length_which_is_subset_of_mdat() {
let mdat = MediaDataBox::at_offset(100, vec![1; 5]);
let extent = Extent::WithLength {
offset: 101,
len: 2,
};
assert_eq!(mdat.get(&extent), Some(&[1, 1][..]));