diff options
Diffstat (limited to 'third_party/rust/mp4parse_capi')
-rw-r--r-- | third_party/rust/mp4parse_capi/.cargo-checksum.json | 1 | ||||
-rw-r--r-- | third_party/rust/mp4parse_capi/Cargo.toml | 33 | ||||
-rw-r--r-- | third_party/rust/mp4parse_capi/cbindgen.toml | 17 | ||||
-rw-r--r-- | third_party/rust/mp4parse_capi/examples/dump.rs | 166 | ||||
-rw-r--r-- | third_party/rust/mp4parse_capi/src/lib.rs | 2111 | ||||
-rw-r--r-- | third_party/rust/mp4parse_capi/tests/test_chunk_out_of_range.rs | 45 | ||||
-rw-r--r-- | third_party/rust/mp4parse_capi/tests/test_encryption.rs | 213 | ||||
-rw-r--r-- | third_party/rust/mp4parse_capi/tests/test_fragment.rs | 117 | ||||
-rw-r--r-- | third_party/rust/mp4parse_capi/tests/test_rotation.rs | 41 | ||||
-rw-r--r-- | third_party/rust/mp4parse_capi/tests/test_sample_table.rs | 278 | ||||
-rw-r--r-- | third_party/rust/mp4parse_capi/tests/test_workaround_stsc.rs | 46 |
11 files changed, 3068 insertions, 0 deletions
diff --git a/third_party/rust/mp4parse_capi/.cargo-checksum.json b/third_party/rust/mp4parse_capi/.cargo-checksum.json new file mode 100644 index 0000000000..e59d87b754 --- /dev/null +++ b/third_party/rust/mp4parse_capi/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"ba144343a4d38fe6918dd61bb6723e46ac297f506a27370c1d10b49bd8123572","cbindgen.toml":"5c9429f271d6e914d81b63e6509c04ffe84cab11ed3a53a2ed4715e5d5ace80e","examples/dump.rs":"83462422315c22e496960bae922edb23105c0aa272d2b106edd7574ff068513a","src/lib.rs":"d25c2e010186ada3524801e8f1ecdbe42cc4f961880ccca11ba9bdddd42363f7","tests/test_chunk_out_of_range.rs":"b5da583218d98027ed973a29c67434a91a1306f2d2fb39ec4d640d4824c308ce","tests/test_encryption.rs":"ca98516ff423c03b5fcc17b05f993f13b32485e4cf3ba86faf1bea72681d75ce","tests/test_fragment.rs":"e90eb5a4418d30002655466c0c4b3125c7fd70a74b6871471eaa172f1def9db8","tests/test_rotation.rs":"fb43c2f2dfa496d151c33bdd46c0fd3252387c23cc71e2cac9ed0234de715a81","tests/test_sample_table.rs":"185755909b2f4e0ea99604bb423a07623d614a180accdaebd1c98aef2c2e3ae6","tests/test_workaround_stsc.rs":"7dd419f3d55b9a3a039cac57e58a9240a9c8166bcd4356c24f69f731c3ced83b"},"package":null}
\ No newline at end of file diff --git a/third_party/rust/mp4parse_capi/Cargo.toml b/third_party/rust/mp4parse_capi/Cargo.toml new file mode 100644 index 0000000000..b4ede98c93 --- /dev/null +++ b/third_party/rust/mp4parse_capi/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "mp4parse_capi" +version = "0.11.4" +authors = [ + "Ralph Giles <giles@mozilla.com>", + "Matthew Gregan <kinetik@flim.org>", + "Alfredo Yang <ayang@mozilla.com>", + "Jon Bauman <jbauman@mozilla.com>", +] + +description = "Parser for ISO base media file format (mp4)" +documentation = "https://docs.rs/mp4parse_capi/" +license = "MPL-2.0" + +repository = "https://github.com/mozilla/mp4parse-rust" + +# Avoid complaints about trying to package test files. +exclude = [ + "*.mp4", +] + +[badges] +travis-ci = { repository = "https://github.com/mozilla/mp4parse-rust" } + +[dependencies] +byteorder = "1.2.1" +fallible_collections = { version = "0.3", features = ["std_io"] } +log = "0.4" +mp4parse = {version = "0.11.2", path = "../mp4parse"} +num-traits = "=0.2.10" + +[dev-dependencies] +env_logger = "0.7.1" diff --git a/third_party/rust/mp4parse_capi/cbindgen.toml b/third_party/rust/mp4parse_capi/cbindgen.toml new file mode 100644 index 0000000000..bbaf50b078 --- /dev/null +++ b/third_party/rust/mp4parse_capi/cbindgen.toml @@ -0,0 +1,17 @@ +header = """/* 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 http://mozilla.org/MPL/2.0/. */""" +autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. */ +#ifndef mp4parse_rust_mp4parse_h +#error "Don't include this file directly, instead include mp4parse.h" +#endif +""" +include_version = true +braces = "SameLine" +line_length = 100 +tab_width = 2 +language = "C" +cpp_compat = true + +[enum] +rename_variants = "QualifiedScreamingSnakeCase" diff --git a/third_party/rust/mp4parse_capi/examples/dump.rs b/third_party/rust/mp4parse_capi/examples/dump.rs new file mode 100644 index 0000000000..d00fdf5e93 --- /dev/null +++ b/third_party/rust/mp4parse_capi/examples/dump.rs @@ -0,0 +1,166 @@ +extern crate mp4parse; +extern crate mp4parse_capi; + +#[macro_use] +extern crate log; + +extern crate env_logger; + +use mp4parse_capi::*; +use std::env; +use std::fs::File; +use std::io::Read; + +extern "C" fn buf_read(buf: *mut u8, size: usize, userdata: *mut std::os::raw::c_void) -> isize { + let input: &mut std::fs::File = unsafe { &mut *(userdata as *mut _) }; + let mut buf = unsafe { std::slice::from_raw_parts_mut(buf, size) }; + match input.read(&mut buf) { + Ok(n) => n as isize, + Err(_) => -1, + } +} + +fn dump_file(filename: &str) { + let mut file = File::open(filename).expect("Unknown file"); + let io = Mp4parseIo { + read: Some(buf_read), + userdata: &mut file as *mut _ as *mut std::os::raw::c_void, + }; + + unsafe { + let mut parser = std::ptr::null_mut(); + let rv = mp4parse_new(&io, &mut parser); + + match rv { + Mp4parseStatus::Ok => (), + _ => { + println!("-- fail to parse: {:?}, '-v' for more info", rv); + return; + } + } + + let mut frag_info = Mp4parseFragmentInfo::default(); + match mp4parse_get_fragment_info(parser, &mut frag_info) { + Mp4parseStatus::Ok => { + println!("-- mp4parse_fragment_info {:?}", frag_info); + } + rv => { + println!("-- mp4parse_fragment_info failed with {:?}", rv); + return; + } + } + + let mut counts: u32 = 0; + match mp4parse_get_track_count(parser, &mut counts) { + Mp4parseStatus::Ok => (), + _ => { + println!("-- mp4parse_get_track_count failed"); + return; + } + } + + for i in 0..counts { + let mut track_info = Mp4parseTrackInfo { + track_type: Mp4parseTrackType::Audio, + ..Default::default() + }; + match mp4parse_get_track_info(parser, i, &mut track_info) { + Mp4parseStatus::Ok => { + println!("-- mp4parse_get_track_info {:?}", track_info); + } + _ => { + println!("-- mp4parse_get_track_info failed, track id: {}", i); + return; + } + } + + match track_info.track_type { + Mp4parseTrackType::Audio => { + let mut audio_info = Mp4parseTrackAudioInfo::default(); + match mp4parse_get_track_audio_info(parser, i, &mut audio_info) { + Mp4parseStatus::Ok => { + println!("-- mp4parse_get_track_audio_info {:?}", audio_info); + for i in 0..audio_info.sample_info_count as isize { + let sample_info = audio_info.sample_info.offset(i); + println!( + " -- mp4parse_get_track_audio_info sample_info[{:?}] {:?}", + i, *sample_info + ); + } + } + _ => { + println!("-- mp4parse_get_track_audio_info failed, track id: {}", i); + return; + } + } + } + Mp4parseTrackType::Video => { + let mut video_info = Mp4parseTrackVideoInfo::default(); + match mp4parse_get_track_video_info(parser, i, &mut video_info) { + Mp4parseStatus::Ok => { + println!("-- mp4parse_get_track_video_info {:?}", video_info); + for i in 0..video_info.sample_info_count as isize { + let sample_info = video_info.sample_info.offset(i); + println!( + " -- mp4parse_get_track_video_info sample_info[{:?}] {:?}", + i, *sample_info + ); + } + } + _ => { + println!("-- mp4parse_get_track_video_info failed, track id: {}", i); + return; + } + } + } + Mp4parseTrackType::Metadata => { + println!("TODO metadata track"); + } + } + + let mut indices = Mp4parseByteData::default(); + match mp4parse_get_indice_table(parser, track_info.track_id, &mut indices) { + Mp4parseStatus::Ok => { + println!( + "-- mp4parse_get_indice_table track_id {} indices {:?}", + track_info.track_id, indices + ); + } + _ => { + println!( + "-- mp4parse_get_indice_table failed, track_info.track_id: {}", + track_info.track_id + ); + return; + } + } + } + mp4parse_free(parser); + } +} + +fn main() { + let args: Vec<String> = env::args().collect(); + if args.len() < 2 { + return; + } + + // Initialize logging, setting the log level if requested. + let (skip, verbose) = if args[1] == "-v" { + (2, true) + } else { + (1, false) + }; + let env = env_logger::Env::default(); + let mut logger = env_logger::Builder::from_env(env); + if verbose { + logger.filter(None, log::LevelFilter::Debug); + } + logger.init(); + + for filename in args.iter().skip(skip) { + info!("-- dump of '{}' --", filename); + dump_file(filename); + info!("-- end of '{}' --", filename); + } +} diff --git a/third_party/rust/mp4parse_capi/src/lib.rs b/third_party/rust/mp4parse_capi/src/lib.rs new file mode 100644 index 0000000000..becb5dec1d --- /dev/null +++ b/third_party/rust/mp4parse_capi/src/lib.rs @@ -0,0 +1,2111 @@ +//! C API for mp4parse module. +//! +//! Parses ISO Base Media Format aka video/mp4 streams. +//! +//! # Examples +//! +//! ```rust +//! extern crate mp4parse_capi; +//! use std::io::Read; +//! +//! extern fn buf_read(buf: *mut u8, size: usize, userdata: *mut std::os::raw::c_void) -> isize { +//! let mut input: &mut std::fs::File = unsafe { &mut *(userdata as *mut _) }; +//! let mut buf = unsafe { std::slice::from_raw_parts_mut(buf, size) }; +//! match input.read(&mut buf) { +//! Ok(n) => n as isize, +//! Err(_) => -1, +//! } +//! } +//! let capi_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); +//! let mut file = std::fs::File::open(capi_dir + "/../mp4parse/tests/minimal.mp4").unwrap(); +//! let io = mp4parse_capi::Mp4parseIo { +//! read: Some(buf_read), +//! userdata: &mut file as *mut _ as *mut std::os::raw::c_void +//! }; +//! let mut parser = std::ptr::null_mut(); +//! unsafe { +//! let rv = mp4parse_capi::mp4parse_new(&io, &mut parser); +//! assert_eq!(rv, mp4parse_capi::Mp4parseStatus::Ok); +//! assert!(!parser.is_null()); +//! mp4parse_capi::mp4parse_free(parser); +//! } +//! ``` + +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +extern crate byteorder; +extern crate log; +extern crate mp4parse; +extern crate num_traits; + +use byteorder::WriteBytesExt; +use num_traits::{CheckedAdd, CheckedSub}; +use num_traits::{PrimInt, Zero}; +use std::convert::TryFrom; +use std::convert::TryInto; + +use std::io::Read; +use std::ops::Neg; +use std::ops::{Add, Sub}; + +// Symbols we need from our rust api. +use mp4parse::read_avif; +use mp4parse::read_mp4; +use mp4parse::serialize_opus_header; +use mp4parse::AudioCodecSpecific; +use mp4parse::AvifContext; +use mp4parse::CodecType; +use mp4parse::Error; +use mp4parse::MediaContext; +use mp4parse::MediaScaledTime; +use mp4parse::MediaTimeScale; +use mp4parse::SampleEntry; +use mp4parse::ToUsize; +use mp4parse::Track; +use mp4parse::TrackScaledTime; +use mp4parse::TrackTimeScale; +use mp4parse::TrackType; +use mp4parse::TryBox; +use mp4parse::TryHashMap; +use mp4parse::TryVec; +use mp4parse::VideoCodecSpecific; + +// 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; + +#[repr(C)] +#[derive(PartialEq, Debug)] +pub enum Mp4parseStatus { + Ok = 0, + BadArg = 1, + Invalid = 2, + Unsupported = 3, + Eof = 4, + Io = 5, + Oom = 6, +} + +#[repr(C)] +#[derive(PartialEq, Debug)] +pub enum Mp4parseTrackType { + Video = 0, + Audio = 1, + Metadata = 2, +} + +impl Default for Mp4parseTrackType { + fn default() -> Self { + Mp4parseTrackType::Video + } +} + +#[allow(non_camel_case_types)] +#[repr(C)] +#[derive(PartialEq, Debug)] +pub enum Mp4parseCodec { + Unknown, + Aac, + Flac, + Opus, + Avc, + Vp9, + Av1, + Mp3, + Mp4v, + Jpeg, // for QT JPEG atom in video track + Ac3, + Ec3, + Alac, +} + +impl Default for Mp4parseCodec { + fn default() -> Self { + Mp4parseCodec::Unknown + } +} + +#[repr(C)] +#[derive(PartialEq, Debug)] +pub enum Mp4ParseEncryptionSchemeType { + None, + Cenc, + Cbc1, + Cens, + Cbcs, + // Schemes also have a version component. At the time of writing, this does + // not impact handling, so we do not expose it. Note that this may need to + // be exposed in future, should the spec change. +} + +impl Default for Mp4ParseEncryptionSchemeType { + fn default() -> Self { + Mp4ParseEncryptionSchemeType::None + } +} + +/// A zero-overhead wrapper around integer types for the sake of always +/// requiring checked arithmetic +#[repr(transparent)] +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct CheckedInteger<T>(pub T); + +impl<T> From<T> for CheckedInteger<T> { + fn from(i: T) -> Self { + Self(i) + } +} + +// Orphan rules prevent a more general implementation, but this suffices +impl From<CheckedInteger<i64>> for i64 { + fn from(checked: CheckedInteger<i64>) -> i64 { + checked.0 + } +} + +impl<T, U: Into<T>> Add<U> for CheckedInteger<T> +where + T: CheckedAdd, +{ + type Output = Option<Self>; + + fn add(self, other: U) -> Self::Output { + self.0.checked_add(&other.into()).map(Into::into) + } +} + +impl<T, U: Into<T>> Sub<U> for CheckedInteger<T> +where + T: CheckedSub, +{ + type Output = Option<Self>; + + fn sub(self, other: U) -> Self::Output { + self.0.checked_sub(&other.into()).map(Into::into) + } +} + +/// Implement subtraction of checked `u64`s returning i64 +// This is necessary for handling Mp4parseTrackInfo::media_time gracefully +impl Sub for CheckedInteger<u64> { + type Output = Option<CheckedInteger<i64>>; + + fn sub(self, other: Self) -> Self::Output { + if self >= other { + self.0 + .checked_sub(other.0) + .and_then(|u| i64::try_from(u).ok()) + .map(CheckedInteger) + } else { + other + .0 + .checked_sub(self.0) + .and_then(|u| i64::try_from(u).ok()) + .map(i64::neg) + .map(CheckedInteger) + } + } +} + +#[test] +fn u64_subtraction_returning_i64() { + // self > other + assert_eq!( + CheckedInteger(2u64) - CheckedInteger(1u64), + Some(CheckedInteger(1i64)) + ); + + // self == other + assert_eq!( + CheckedInteger(1u64) - CheckedInteger(1u64), + Some(CheckedInteger(0i64)) + ); + + // difference too large to store in i64 + assert_eq!(CheckedInteger(u64::MAX) - CheckedInteger(1u64), None); + + // self < other + assert_eq!( + CheckedInteger(1u64) - CheckedInteger(2u64), + Some(CheckedInteger(-1i64)) + ); + + // difference not representable due to overflow + assert_eq!(CheckedInteger(1u64) - CheckedInteger(u64::MAX), None); +} + +impl<T: std::cmp::PartialEq> PartialEq<T> for CheckedInteger<T> { + fn eq(&self, other: &T) -> bool { + self.0 == *other + } +} + +#[repr(C)] +#[derive(Default, Debug)] +pub struct Mp4parseTrackInfo { + pub track_type: Mp4parseTrackType, + pub track_id: u32, + pub duration: u64, + pub media_time: CheckedInteger<i64>, // wants to be u64? understand how elst adjustment works + // TODO(kinetik): include crypto guff + // If this changes to u64, we can get rid of the strange + // impl Sub for CheckedInteger<u64> +} + +#[repr(C)] +#[derive(Default, Debug, PartialEq)] +pub struct Mp4parseIndice { + /// The byte offset in the file where the indexed sample begins. + pub start_offset: CheckedInteger<u64>, + /// The byte offset in the file where the indexed sample ends. This is + /// equivalent to `start_offset` + the length in bytes of the indexed + /// sample. Typically this will be the `start_offset` of the next sample + /// in the file. + pub end_offset: CheckedInteger<u64>, + /// The time in microseconds when the indexed sample should be displayed. + /// Analogous to the concept of presentation time stamp (pts). + pub start_composition: CheckedInteger<i64>, + /// The time in microseconds when the indexed sample should stop being + /// displayed. Typically this would be the `start_composition` time of the + /// next sample if samples were ordered by composition time. + pub end_composition: CheckedInteger<i64>, + /// The time in microseconds that the indexed sample should be decoded at. + /// Analogous to the concept of decode time stamp (dts). + pub start_decode: CheckedInteger<i64>, + /// Set if the indexed sample is a sync sample. The meaning of sync is + /// somewhat codec specific, but essentially amounts to if the sample is a + /// key frame. + pub sync: bool, +} + +#[repr(C)] +#[derive(Debug)] +pub struct Mp4parseByteData { + pub length: u32, + // cheddar can't handle generic type, so it needs to be multiple data types here. + pub data: *const u8, + pub indices: *const Mp4parseIndice, +} + +impl Default for Mp4parseByteData { + fn default() -> Self { + Self { + length: 0, + data: std::ptr::null(), + indices: std::ptr::null(), + } + } +} + +impl Mp4parseByteData { + fn set_data(&mut self, data: &[u8]) { + self.length = data.len() as u32; + self.data = data.as_ptr(); + } + + fn set_indices(&mut self, data: &[Mp4parseIndice]) { + self.length = data.len() as u32; + self.indices = data.as_ptr(); + } +} + +#[repr(C)] +#[derive(Default)] +pub struct Mp4parsePsshInfo { + pub data: Mp4parseByteData, +} + +#[repr(u8)] +#[derive(Debug, PartialEq)] +pub enum OptionalFourCC { + None, + Some([u8; 4]), +} + +impl Default for OptionalFourCC { + fn default() -> Self { + Self::None + } +} + +#[repr(C)] +#[derive(Default, Debug)] +pub struct Mp4parseSinfInfo { + pub original_format: OptionalFourCC, + pub scheme_type: Mp4ParseEncryptionSchemeType, + pub is_encrypted: u8, + pub iv_size: u8, + pub kid: Mp4parseByteData, + // Members for pattern encryption schemes, may be 0 (u8) or empty + // (Mp4parseByteData) if pattern encryption is not in use + pub crypt_byte_block: u8, + pub skip_byte_block: u8, + pub constant_iv: Mp4parseByteData, + // End pattern encryption scheme members +} + +#[repr(C)] +#[derive(Default, Debug)] +pub struct Mp4parseTrackAudioSampleInfo { + pub codec_type: Mp4parseCodec, + pub channels: u16, + pub bit_depth: u16, + pub sample_rate: u32, + pub profile: u16, + pub extended_profile: u16, + pub codec_specific_config: Mp4parseByteData, + pub extra_data: Mp4parseByteData, + pub protected_data: Mp4parseSinfInfo, +} + +#[repr(C)] +#[derive(Debug)] +pub struct Mp4parseTrackAudioInfo { + pub sample_info_count: u32, + pub sample_info: *const Mp4parseTrackAudioSampleInfo, +} + +impl Default for Mp4parseTrackAudioInfo { + fn default() -> Self { + Self { + sample_info_count: 0, + sample_info: std::ptr::null(), + } + } +} + +#[repr(C)] +#[derive(Default, Debug)] +pub struct Mp4parseTrackVideoSampleInfo { + pub codec_type: Mp4parseCodec, + pub image_width: u16, + pub image_height: u16, + pub extra_data: Mp4parseByteData, + pub protected_data: Mp4parseSinfInfo, +} + +#[repr(C)] +#[derive(Debug)] +pub struct Mp4parseTrackVideoInfo { + pub display_width: u32, + pub display_height: u32, + pub rotation: u16, + pub sample_info_count: u32, + pub sample_info: *const Mp4parseTrackVideoSampleInfo, +} + +impl Default for Mp4parseTrackVideoInfo { + fn default() -> Self { + Self { + display_width: 0, + display_height: 0, + rotation: 0, + sample_info_count: 0, + sample_info: std::ptr::null(), + } + } +} + +#[repr(C)] +#[derive(Default, Debug)] +pub struct Mp4parseFragmentInfo { + pub fragment_duration: u64, + // TODO: + // info in trex box. +} + +#[derive(Default)] +pub struct Mp4parseParser { + context: MediaContext, + opus_header: TryHashMap<u32, TryVec<u8>>, + pssh_data: TryVec<u8>, + sample_table: TryHashMap<u32, TryVec<Mp4parseIndice>>, + // Store a mapping from track index (not id) to associated sample + // descriptions. Because each track has a variable number of sample + // descriptions, and because we need the data to live long enough to be + // copied out by callers, we store these on the parser struct. + audio_track_sample_descriptions: TryHashMap<u32, TryVec<Mp4parseTrackAudioSampleInfo>>, + video_track_sample_descriptions: TryHashMap<u32, TryVec<Mp4parseTrackVideoSampleInfo>>, +} + +#[repr(C)] +#[derive(Default)] +pub struct AvifImage { + pub primary_item: Mp4parseByteData, + pub alpha_item: Mp4parseByteData, + pub premultiplied_alpha: bool, +} + +/// A unified interface for the parsers which have different contexts, but +/// share the same pattern of construction. This allows unification of +/// argument validation from C and minimizes the surface of unsafe code. +trait ContextParser +where + Self: Sized, +{ + type Context; + + fn with_context(context: Self::Context) -> Self; + + fn read<T: Read>(io: &mut T) -> mp4parse::Result<Self::Context>; +} + +impl Mp4parseParser { + fn context(&self) -> &MediaContext { + &self.context + } + + fn context_mut(&mut self) -> &mut MediaContext { + &mut self.context + } +} + +impl ContextParser for Mp4parseParser { + type Context = MediaContext; + + fn with_context(context: Self::Context) -> Self { + Self { + context, + ..Default::default() + } + } + + fn read<T: Read>(io: &mut T) -> mp4parse::Result<Self::Context> { + read_mp4(io) + } +} + +pub struct Mp4parseAvifParser { + context: AvifContext, +} + +impl Mp4parseAvifParser { + fn context(&self) -> &AvifContext { + &self.context + } +} + +impl ContextParser for Mp4parseAvifParser { + type Context = AvifContext; + + fn with_context(context: Self::Context) -> Self { + Self { context } + } + + fn read<T: Read>(io: &mut T) -> mp4parse::Result<Self::Context> { + read_avif(io) + } +} + +#[repr(C)] +#[derive(Clone)] +pub struct Mp4parseIo { + pub read: Option< + extern "C" fn(buffer: *mut u8, size: usize, userdata: *mut std::os::raw::c_void) -> isize, + >, + pub userdata: *mut std::os::raw::c_void, +} + +impl Read for Mp4parseIo { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { + if buf.len() > isize::max_value() as usize { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "buf length overflow in Mp4parseIo Read impl", + )); + } + let rv = self.read.unwrap()(buf.as_mut_ptr(), buf.len(), self.userdata); + if rv >= 0 { + Ok(rv as usize) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + "I/O error in Mp4parseIo Read impl", + )) + } + } +} + +// C API wrapper functions. + +/// Allocate an `Mp4parseParser*` to read from the supplied `Mp4parseIo` and +/// parse the content from the `Mp4parseIo` argument until EOF or error. +/// +/// # Safety +/// +/// This function is unsafe because it dereferences the `io` and `parser_out` +/// pointers given to it. The caller should ensure that the `Mp4ParseIo` +/// struct passed in is a valid pointer. The caller should also ensure the +/// members of io are valid: the `read` function should be sanely implemented, +/// and the `userdata` pointer should be valid. The `parser_out` should be a +/// valid pointer to a location containing a null pointer. Upon successful +/// return (`Mp4parseStatus::Ok`), that location will contain the address of +/// an `Mp4parseParser` allocated by this function. +/// +/// To avoid leaking memory, any successful return of this function must be +/// paired with a call to `mp4parse_free`. In the event of error, no memory +/// will be allocated and `mp4parse_free` must *not* be called. +#[no_mangle] +pub unsafe extern "C" fn mp4parse_new( + io: *const Mp4parseIo, + parser_out: *mut *mut Mp4parseParser, +) -> Mp4parseStatus { + mp4parse_new_common(io, parser_out) +} + +/// Allocate an `Mp4parseAvifParser*` to read from the supplied `Mp4parseIo`. +/// +/// See mp4parse_new; this function is identical except that it allocates an +/// `Mp4parseAvifParser`, which (when successful) must be paired with a call +/// to mp4parse_avif_free. +/// +/// # Safety +/// +/// Same as mp4parse_new. +#[no_mangle] +pub unsafe extern "C" fn mp4parse_avif_new( + io: *const Mp4parseIo, + parser_out: *mut *mut Mp4parseAvifParser, +) -> Mp4parseStatus { + mp4parse_new_common(io, parser_out) +} + +unsafe fn mp4parse_new_common<P: ContextParser>( + io: *const Mp4parseIo, + parser_out: *mut *mut P, +) -> Mp4parseStatus { + // Validate arguments from C. + if io.is_null() + || (*io).userdata.is_null() + || (*io).read.is_none() + || parser_out.is_null() + || !(*parser_out).is_null() + { + Mp4parseStatus::BadArg + } else { + match mp4parse_new_common_safe(&mut (*io).clone()) { + Ok(parser) => { + *parser_out = parser; + Mp4parseStatus::Ok + } + Err(status) => status, + } + } +} + +fn mp4parse_new_common_safe<T: Read, P: ContextParser>( + io: &mut T, +) -> Result<*mut P, Mp4parseStatus> { + P::read(io) + .map(P::with_context) + .and_then(|x| TryBox::try_new(x).map_err(mp4parse::Error::from)) + .map(TryBox::into_raw) + .map_err(Mp4parseStatus::from) +} + +impl From<mp4parse::Error> for Mp4parseStatus { + fn from(error: mp4parse::Error) -> Self { + match error { + Error::NoMoov | Error::InvalidData(_) => Mp4parseStatus::Invalid, + Error::Unsupported(_) => Mp4parseStatus::Unsupported, + Error::UnexpectedEOF => Mp4parseStatus::Eof, + Error::Io(_) => { + // Getting std::io::ErrorKind::UnexpectedEof is normal + // but our From trait implementation should have converted + // those to our Error::UnexpectedEOF variant. + Mp4parseStatus::Io + } + Error::OutOfMemory => Mp4parseStatus::Oom, + } + } +} + +impl From<Result<(), Mp4parseStatus>> for Mp4parseStatus { + fn from(result: Result<(), Mp4parseStatus>) -> Self { + match result { + Ok(()) => Mp4parseStatus::Ok, + Err(Mp4parseStatus::Ok) => unreachable!(), + Err(e) => e, + } + } +} + +impl From<fallible_collections::TryReserveError> for Mp4parseStatus { + fn from(_: fallible_collections::TryReserveError) -> Self { + Mp4parseStatus::Oom + } +} + +/// Free an `Mp4parseParser*` allocated by `mp4parse_new()`. +/// +/// # Safety +/// +/// This function is unsafe because it creates a box from a raw pointer. +/// Callers should ensure that the parser pointer points to a valid +/// `Mp4parseParser` created by `mp4parse_new`. +#[no_mangle] +pub unsafe extern "C" fn mp4parse_free(parser: *mut Mp4parseParser) { + assert!(!parser.is_null()); + let _ = TryBox::from_raw(parser); +} + +/// Free an `Mp4parseAvifParser*` allocated by `mp4parse_avif_new()`. +/// +/// # Safety +/// +/// This function is unsafe because it creates a box from a raw pointer. +/// Callers should ensure that the parser pointer points to a valid +/// `Mp4parseAvifParser` created by `mp4parse_avif_new`. +#[no_mangle] +pub unsafe extern "C" fn mp4parse_avif_free(parser: *mut Mp4parseAvifParser) { + assert!(!parser.is_null()); + let _ = TryBox::from_raw(parser); +} + +/// Return the number of tracks parsed by previous `mp4parse_read()` call. +/// +/// # Safety +/// +/// This function is unsafe because it dereferences both the parser and count +/// raw pointers passed into it. Callers should ensure the parser pointer +/// points to a valid `Mp4parseParser`, and that the count pointer points an +/// appropriate memory location to have a `u32` written to. +#[no_mangle] +pub unsafe extern "C" fn mp4parse_get_track_count( + parser: *const Mp4parseParser, + count: *mut u32, +) -> Mp4parseStatus { + // Validate arguments from C. + if parser.is_null() || count.is_null() { + return Mp4parseStatus::BadArg; + } + let context = (*parser).context(); + + // Make sure the track count fits in a u32. + if context.tracks.len() > u32::max_value() as usize { + return Mp4parseStatus::Invalid; + } + *count = context.tracks.len() as u32; + Mp4parseStatus::Ok +} + +/// Calculate numerator * scale / denominator, if possible. +/// +/// Applying the associativity of integer arithmetic, we divide first +/// and add the remainder after multiplying each term separately +/// to preserve precision while leaving more headroom. That is, +/// (n * s) / d is split into floor(n / d) * s + (n % d) * s / d. +/// +/// Return None on overflow or if the denominator is zero. +fn rational_scale<T, S>(numerator: T, denominator: T, scale2: S) -> Option<T> +where + T: PrimInt + Zero, + S: PrimInt, +{ + if denominator.is_zero() { + return None; + } + + let integer = numerator / denominator; + let remainder = numerator % denominator; + num_traits::cast(scale2).and_then(|s| match integer.checked_mul(&s) { + Some(integer) => remainder + .checked_mul(&s) + .and_then(|remainder| (remainder / denominator).checked_add(&integer)), + None => None, + }) +} + +fn media_time_to_us(time: MediaScaledTime, scale: MediaTimeScale) -> Option<u64> { + let microseconds_per_second = 1_000_000; + rational_scale::<u64, u64>(time.0, scale.0, microseconds_per_second) +} + +fn track_time_to_us<T>(time: TrackScaledTime<T>, scale: TrackTimeScale<T>) -> Option<T> +where + T: PrimInt + Zero, +{ + assert_eq!(time.1, scale.1); + let microseconds_per_second = 1_000_000; + rational_scale::<T, u64>(time.0, scale.0, microseconds_per_second) +} + +/// Fill the supplied `Mp4parseTrackInfo` with metadata for `track`. +/// +/// # Safety +/// +/// This function is unsafe because it dereferences the the parser and info raw +/// pointers passed to it. Callers should ensure the parser pointer points to a +/// valid `Mp4parseParser` and that the info pointer points to a valid +/// `Mp4parseTrackInfo`. +#[no_mangle] +pub unsafe extern "C" fn mp4parse_get_track_info( + parser: *mut Mp4parseParser, + track_index: u32, + info: *mut Mp4parseTrackInfo, +) -> Mp4parseStatus { + if parser.is_null() || info.is_null() { + return Mp4parseStatus::BadArg; + } + + // Initialize fields to default values to ensure all fields are always valid. + *info = Default::default(); + + let context = (*parser).context_mut(); + let track_index: usize = track_index as usize; + let info: &mut Mp4parseTrackInfo = &mut *info; + + if track_index >= context.tracks.len() { + return Mp4parseStatus::BadArg; + } + + info.track_type = match context.tracks[track_index].track_type { + TrackType::Video => Mp4parseTrackType::Video, + TrackType::Audio => Mp4parseTrackType::Audio, + TrackType::Metadata => Mp4parseTrackType::Metadata, + TrackType::Unknown => return Mp4parseStatus::Unsupported, + }; + + let track = &context.tracks[track_index]; + + if let (Some(track_timescale), Some(context_timescale)) = (track.timescale, context.timescale) { + let media_time: CheckedInteger<_> = match track.media_time.map_or(Some(0), |media_time| { + track_time_to_us(media_time, track_timescale) + }) { + Some(time) => time.into(), + None => return Mp4parseStatus::Invalid, + }; + let empty_duration: CheckedInteger<_> = + match track.empty_duration.map_or(Some(0), |empty_duration| { + media_time_to_us(empty_duration, context_timescale) + }) { + Some(time) => time.into(), + None => return Mp4parseStatus::Invalid, + }; + info.media_time = match media_time - empty_duration { + Some(difference) => difference, + None => return Mp4parseStatus::Invalid, + }; + + if let Some(track_duration) = track.duration { + match track_time_to_us(track_duration, track_timescale) { + Some(duration) => info.duration = duration, + None => return Mp4parseStatus::Invalid, + } + } else { + // Duration unknown; stagefright returns 0 for this. + info.duration = 0 + } + } else { + return Mp4parseStatus::Invalid; + } + + info.track_id = match track.track_id { + Some(track_id) => track_id, + None => return Mp4parseStatus::Invalid, + }; + + Mp4parseStatus::Ok +} + +/// Fill the supplied `Mp4parseTrackAudioInfo` with metadata for `track`. +/// +/// # Safety +/// +/// This function is unsafe because it dereferences the the parser and info raw +/// pointers passed to it. Callers should ensure the parser pointer points to a +/// valid `Mp4parseParser` and that the info pointer points to a valid +/// `Mp4parseTrackAudioInfo`. +#[no_mangle] +pub unsafe extern "C" fn mp4parse_get_track_audio_info( + parser: *mut Mp4parseParser, + track_index: u32, + info: *mut Mp4parseTrackAudioInfo, +) -> Mp4parseStatus { + if parser.is_null() || info.is_null() { + return Mp4parseStatus::BadArg; + } + + // Initialize fields to default values to ensure all fields are always valid. + *info = Default::default(); + + get_track_audio_info(&mut *parser, track_index, &mut *info).into() +} + +fn get_track_audio_info( + parser: &mut Mp4parseParser, + track_index: u32, + info: &mut Mp4parseTrackAudioInfo, +) -> Result<(), Mp4parseStatus> { + let Mp4parseParser { + context, + opus_header, + .. + } = parser; + + if track_index as usize >= context.tracks.len() { + return Err(Mp4parseStatus::BadArg); + } + + let track = &context.tracks[track_index as usize]; + + if track.track_type != TrackType::Audio { + return Err(Mp4parseStatus::Invalid); + } + + // Handle track.stsd + let stsd = match track.stsd { + Some(ref stsd) => stsd, + None => return Err(Mp4parseStatus::Invalid), // Stsd should be present + }; + + if stsd.descriptions.is_empty() { + return Err(Mp4parseStatus::Invalid); // Should have at least 1 description + } + + let mut audio_sample_infos = TryVec::with_capacity(stsd.descriptions.len())?; + for description in stsd.descriptions.iter() { + let mut sample_info = Mp4parseTrackAudioSampleInfo::default(); + let audio = match description { + SampleEntry::Audio(a) => a, + _ => return Err(Mp4parseStatus::Invalid), + }; + + // UNKNOWN for unsupported format. + sample_info.codec_type = match audio.codec_specific { + AudioCodecSpecific::OpusSpecificBox(_) => Mp4parseCodec::Opus, + AudioCodecSpecific::FLACSpecificBox(_) => Mp4parseCodec::Flac, + AudioCodecSpecific::ES_Descriptor(ref esds) if esds.audio_codec == CodecType::AAC => { + Mp4parseCodec::Aac + } + AudioCodecSpecific::ES_Descriptor(ref esds) if esds.audio_codec == CodecType::MP3 => { + Mp4parseCodec::Mp3 + } + AudioCodecSpecific::ES_Descriptor(_) | AudioCodecSpecific::LPCM => { + Mp4parseCodec::Unknown + } + AudioCodecSpecific::MP3 => Mp4parseCodec::Mp3, + AudioCodecSpecific::ALACSpecificBox(_) => Mp4parseCodec::Alac, + }; + sample_info.channels = audio.channelcount as u16; + sample_info.bit_depth = audio.samplesize; + sample_info.sample_rate = audio.samplerate as u32; + // sample_info.profile is handled below on a per case basis + + match audio.codec_specific { + AudioCodecSpecific::ES_Descriptor(ref esds) => { + if esds.codec_esds.len() > std::u32::MAX as usize { + return Err(Mp4parseStatus::Invalid); + } + sample_info.extra_data.length = esds.codec_esds.len() as u32; + sample_info.extra_data.data = esds.codec_esds.as_ptr(); + sample_info.codec_specific_config.length = esds.decoder_specific_data.len() as u32; + sample_info.codec_specific_config.data = esds.decoder_specific_data.as_ptr(); + if let Some(rate) = esds.audio_sample_rate { + sample_info.sample_rate = rate; + } + if let Some(channels) = esds.audio_channel_count { + sample_info.channels = channels; + } + if let Some(profile) = esds.audio_object_type { + sample_info.profile = profile; + } + sample_info.extended_profile = match esds.extended_audio_object_type { + Some(extended_profile) => extended_profile, + _ => sample_info.profile, + }; + } + AudioCodecSpecific::FLACSpecificBox(ref flac) => { + // Return the STREAMINFO metadata block in the codec_specific. + let streaminfo = &flac.blocks[0]; + if streaminfo.block_type != 0 || streaminfo.data.len() != 34 { + return Err(Mp4parseStatus::Invalid); + } + sample_info.codec_specific_config.length = streaminfo.data.len() as u32; + sample_info.codec_specific_config.data = streaminfo.data.as_ptr(); + } + AudioCodecSpecific::OpusSpecificBox(ref opus) => { + let mut v = TryVec::new(); + match serialize_opus_header(opus, &mut v) { + Err(_) => { + return Err(Mp4parseStatus::Invalid); + } + Ok(_) => { + opus_header.insert(track_index, v)?; + if let Some(v) = opus_header.get(&track_index) { + if v.len() > std::u32::MAX as usize { + return Err(Mp4parseStatus::Invalid); + } + sample_info.codec_specific_config.length = v.len() as u32; + sample_info.codec_specific_config.data = v.as_ptr(); + } + } + } + } + AudioCodecSpecific::ALACSpecificBox(ref alac) => { + sample_info.codec_specific_config.length = alac.data.len() as u32; + sample_info.codec_specific_config.data = alac.data.as_ptr(); + } + AudioCodecSpecific::MP3 | AudioCodecSpecific::LPCM => (), + } + + if let Some(p) = audio + .protection_info + .iter() + .find(|sinf| sinf.tenc.is_some()) + { + sample_info.protected_data.original_format = + OptionalFourCC::Some(p.original_format.value); + sample_info.protected_data.scheme_type = match p.scheme_type { + Some(ref scheme_type_box) => { + match scheme_type_box.scheme_type.value.as_ref() { + b"cenc" => Mp4ParseEncryptionSchemeType::Cenc, + b"cbcs" => Mp4ParseEncryptionSchemeType::Cbcs, + // We don't support other schemes, and shouldn't reach + // this case. Try to gracefully handle by treating as + // no encryption case. + _ => Mp4ParseEncryptionSchemeType::None, + } + } + None => Mp4ParseEncryptionSchemeType::None, + }; + if let Some(ref tenc) = p.tenc { + sample_info.protected_data.is_encrypted = tenc.is_encrypted; + sample_info.protected_data.iv_size = tenc.iv_size; + sample_info.protected_data.kid.set_data(&(tenc.kid)); + sample_info.protected_data.crypt_byte_block = match tenc.crypt_byte_block_count { + Some(n) => n, + None => 0, + }; + sample_info.protected_data.skip_byte_block = match tenc.skip_byte_block_count { + Some(n) => n, + None => 0, + }; + if let Some(ref iv_vec) = tenc.constant_iv { + if iv_vec.len() > std::u32::MAX as usize { + return Err(Mp4parseStatus::Invalid); + } + sample_info.protected_data.constant_iv.set_data(iv_vec); + }; + } + } + audio_sample_infos.push(sample_info)?; + } + + parser + .audio_track_sample_descriptions + .insert(track_index, audio_sample_infos)?; + match parser.audio_track_sample_descriptions.get(&track_index) { + Some(sample_info) => { + if sample_info.len() > std::u32::MAX as usize { + // Should never happen due to upper limits on number of sample + // descriptions a track can have, but lets be safe. + return Err(Mp4parseStatus::Invalid); + } + info.sample_info_count = sample_info.len() as u32; + info.sample_info = sample_info.as_ptr(); + } + None => return Err(Mp4parseStatus::Invalid), // Shouldn't happen, we just inserted the info! + } + + Ok(()) +} + +/// Fill the supplied `Mp4parseTrackVideoInfo` with metadata for `track`. +/// +/// # Safety +/// +/// This function is unsafe because it dereferences the the parser and info raw +/// pointers passed to it. Callers should ensure the parser pointer points to a +/// valid `Mp4parseParser` and that the info pointer points to a valid +/// `Mp4parseTrackVideoInfo`. +#[no_mangle] +pub unsafe extern "C" fn mp4parse_get_track_video_info( + parser: *mut Mp4parseParser, + track_index: u32, + info: *mut Mp4parseTrackVideoInfo, +) -> Mp4parseStatus { + if parser.is_null() || info.is_null() { + return Mp4parseStatus::BadArg; + } + + // Initialize fields to default values to ensure all fields are always valid. + *info = Default::default(); + + mp4parse_get_track_video_info_safe(&mut *parser, track_index, &mut *info).into() +} + +fn mp4parse_get_track_video_info_safe( + parser: &mut Mp4parseParser, + track_index: u32, + info: &mut Mp4parseTrackVideoInfo, +) -> Result<(), Mp4parseStatus> { + let context = parser.context(); + + if track_index as usize >= context.tracks.len() { + return Err(Mp4parseStatus::BadArg); + } + + let track = &context.tracks[track_index as usize]; + + if track.track_type != TrackType::Video { + return Err(Mp4parseStatus::Invalid); + } + + // Handle track.tkhd + if let Some(ref tkhd) = track.tkhd { + info.display_width = tkhd.width >> 16; // 16.16 fixed point + info.display_height = tkhd.height >> 16; // 16.16 fixed point + let matrix = ( + tkhd.matrix.a >> 16, + tkhd.matrix.b >> 16, + tkhd.matrix.c >> 16, + tkhd.matrix.d >> 16, + ); + info.rotation = match matrix { + (0, 1, -1, 0) => 90, // rotate 90 degrees + (-1, 0, 0, -1) => 180, // rotate 180 degrees + (0, -1, 1, 0) => 270, // rotate 270 degrees + _ => 0, + }; + } else { + return Err(Mp4parseStatus::Invalid); + } + + // Handle track.stsd + let stsd = match track.stsd { + Some(ref stsd) => stsd, + None => return Err(Mp4parseStatus::Invalid), // Stsd should be present + }; + + if stsd.descriptions.is_empty() { + return Err(Mp4parseStatus::Invalid); // Should have at least 1 description + } + + let mut video_sample_infos = TryVec::with_capacity(stsd.descriptions.len())?; + for description in stsd.descriptions.iter() { + let mut sample_info = Mp4parseTrackVideoSampleInfo::default(); + let video = match description { + SampleEntry::Video(v) => v, + _ => return Err(Mp4parseStatus::Invalid), + }; + + // UNKNOWN for unsupported format. + sample_info.codec_type = match video.codec_specific { + VideoCodecSpecific::VPxConfig(_) => Mp4parseCodec::Vp9, + VideoCodecSpecific::AV1Config(_) => Mp4parseCodec::Av1, + VideoCodecSpecific::AVCConfig(_) => Mp4parseCodec::Avc, + VideoCodecSpecific::ESDSConfig(_) => + // MP4V (14496-2) video is unsupported. + { + Mp4parseCodec::Unknown + } + }; + sample_info.image_width = video.width; + sample_info.image_height = video.height; + + match video.codec_specific { + VideoCodecSpecific::AVCConfig(ref data) | VideoCodecSpecific::ESDSConfig(ref data) => { + sample_info.extra_data.set_data(data); + } + _ => {} + } + + if let Some(p) = video + .protection_info + .iter() + .find(|sinf| sinf.tenc.is_some()) + { + sample_info.protected_data.original_format = + OptionalFourCC::Some(p.original_format.value); + sample_info.protected_data.scheme_type = match p.scheme_type { + Some(ref scheme_type_box) => { + match scheme_type_box.scheme_type.value.as_ref() { + b"cenc" => Mp4ParseEncryptionSchemeType::Cenc, + b"cbcs" => Mp4ParseEncryptionSchemeType::Cbcs, + // We don't support other schemes, and shouldn't reach + // this case. Try to gracefully handle by treating as + // no encryption case. + _ => Mp4ParseEncryptionSchemeType::None, + } + } + None => Mp4ParseEncryptionSchemeType::None, + }; + if let Some(ref tenc) = p.tenc { + sample_info.protected_data.is_encrypted = tenc.is_encrypted; + sample_info.protected_data.iv_size = tenc.iv_size; + sample_info.protected_data.kid.set_data(&(tenc.kid)); + sample_info.protected_data.crypt_byte_block = match tenc.crypt_byte_block_count { + Some(n) => n, + None => 0, + }; + sample_info.protected_data.skip_byte_block = match tenc.skip_byte_block_count { + Some(n) => n, + None => 0, + }; + if let Some(ref iv_vec) = tenc.constant_iv { + if iv_vec.len() > std::u32::MAX as usize { + return Err(Mp4parseStatus::Invalid); + } + sample_info.protected_data.constant_iv.set_data(iv_vec); + }; + } + } + video_sample_infos.push(sample_info)?; + } + + parser + .video_track_sample_descriptions + .insert(track_index, video_sample_infos)?; + match parser.video_track_sample_descriptions.get(&track_index) { + Some(sample_info) => { + if sample_info.len() > std::u32::MAX as usize { + // Should never happen due to upper limits on number of sample + // descriptions a track can have, but lets be safe. + return Err(Mp4parseStatus::Invalid); + } + info.sample_info_count = sample_info.len() as u32; + info.sample_info = sample_info.as_ptr(); + } + None => return Err(Mp4parseStatus::Invalid), // Shouldn't happen, we just inserted the info! + } + Ok(()) +} + +/// Return a pointer to the primary item parsed by previous `mp4parse_avif_new()` call. +/// +/// # Safety +/// +/// This function is unsafe because it dereferences both the parser and +/// avif_image raw pointers passed into it. Callers should ensure the parser +/// pointer points to a valid `Mp4parseAvifParser`, and that the avif_image +/// pointer points to a valid `AvifImage`. If there was not a previous +/// successful call to `mp4parse_avif_read()`, no guarantees are made as to +/// the state of `avif_image`. If `avif_image.alpha_item` is set to a +/// positive `length` and non-null `data`, then the `avif_image` contains an +/// valid alpha channel data. Otherwise, the image is opaque. +#[no_mangle] +pub unsafe extern "C" fn mp4parse_avif_get_image( + parser: *mut Mp4parseAvifParser, + avif_image: *mut AvifImage, +) -> Mp4parseStatus { + if parser.is_null() || avif_image.is_null() { + return Mp4parseStatus::BadArg; + } + + // Initialize fields to default values to ensure all fields are always valid. + *avif_image = Default::default(); + let context = (*parser).context(); + + (*avif_image).primary_item.set_data(context.primary_item()); + if let Some(context_alpha_item) = context.alpha_item() { + (*avif_image).alpha_item.set_data(context_alpha_item); + (*avif_image).premultiplied_alpha = context.premultiplied_alpha; + } + + Mp4parseStatus::Ok +} + +/// Fill the supplied `Mp4parseByteData` with index information from `track`. +/// +/// # Safety +/// +/// This function is unsafe because it dereferences the the parser and indices +/// raw pointers passed to it. Callers should ensure the parser pointer points +/// to a valid `Mp4parseParser` and that the indices pointer points to a valid +/// `Mp4parseByteData`. +#[no_mangle] +pub unsafe extern "C" fn mp4parse_get_indice_table( + parser: *mut Mp4parseParser, + track_id: u32, + indices: *mut Mp4parseByteData, +) -> Mp4parseStatus { + if parser.is_null() { + return Mp4parseStatus::BadArg; + } + + // Initialize fields to default values to ensure all fields are always valid. + *indices = Default::default(); + + get_indice_table(&mut *parser, track_id, &mut *indices).into() +} + +fn get_indice_table( + parser: &mut Mp4parseParser, + track_id: u32, + indices: &mut Mp4parseByteData, +) -> Result<(), Mp4parseStatus> { + let Mp4parseParser { + context, + sample_table: index_table, + .. + } = parser; + let tracks = &context.tracks; + let track = match tracks.iter().find(|track| track.track_id == Some(track_id)) { + Some(t) => t, + _ => return Err(Mp4parseStatus::Invalid), + }; + + if let Some(v) = index_table.get(&track_id) { + indices.set_indices(v); + return Ok(()); + } + + let media_time = match (&track.media_time, &track.timescale) { + (&Some(t), &Some(s)) => track_time_to_us(t, s) + .and_then(|v| i64::try_from(v).ok()) + .map(Into::into), + _ => None, + }; + + let empty_duration: Option<CheckedInteger<_>> = + match (&track.empty_duration, &context.timescale) { + (&Some(e), &Some(s)) => media_time_to_us(e, s) + .and_then(|v| i64::try_from(v).ok()) + .map(Into::into), + _ => None, + }; + + // Find the track start offset time from 'elst'. + // 'media_time' maps start time onward, 'empty_duration' adds time offset + // before first frame is displayed. + let offset_time = match (empty_duration, media_time) { + (Some(e), Some(m)) => (e - m).ok_or(Err(Mp4parseStatus::Invalid))?, + (Some(e), None) => e, + (None, Some(m)) => m, + _ => 0.into(), + }; + + if let Some(v) = create_sample_table(track, offset_time) { + indices.set_indices(&v); + index_table.insert(track_id, v)?; + return Ok(()); + } + + Err(Mp4parseStatus::Invalid) +} + +// Convert a 'ctts' compact table to full table by iterator, +// (sample_with_the_same_offset_count, offset) => (offset), (offset), (offset) ... +// +// For example: +// (2, 10), (4, 9) into (10, 10, 9, 9, 9, 9) by calling next_offset_time(). +struct TimeOffsetIterator<'a> { + cur_sample_range: std::ops::Range<u32>, + cur_offset: i64, + ctts_iter: Option<std::slice::Iter<'a, mp4parse::TimeOffset>>, + track_id: usize, +} + +impl<'a> Iterator for TimeOffsetIterator<'a> { + type Item = i64; + + #[allow(clippy::reversed_empty_ranges)] + fn next(&mut self) -> Option<i64> { + let has_sample = self.cur_sample_range.next().or_else(|| { + // At end of current TimeOffset, find the next TimeOffset. + let iter = match self.ctts_iter { + Some(ref mut v) => v, + _ => return None, + }; + let offset_version; + self.cur_sample_range = match iter.next() { + Some(v) => { + offset_version = v.time_offset; + 0..v.sample_count + } + _ => { + offset_version = mp4parse::TimeOffsetVersion::Version0(0); + 0..0 + } + }; + + self.cur_offset = match offset_version { + mp4parse::TimeOffsetVersion::Version0(i) => i64::from(i), + mp4parse::TimeOffsetVersion::Version1(i) => i64::from(i), + }; + + self.cur_sample_range.next() + }); + + has_sample.and(Some(self.cur_offset)) + } +} + +impl<'a> TimeOffsetIterator<'a> { + fn next_offset_time(&mut self) -> TrackScaledTime<i64> { + match self.next() { + Some(v) => TrackScaledTime::<i64>(v as i64, self.track_id), + _ => TrackScaledTime::<i64>(0, self.track_id), + } + } +} + +// Convert 'stts' compact table to full table by iterator, +// (sample_count_with_the_same_time, time) => (time, time, time) ... repeats +// sample_count_with_the_same_time. +// +// For example: +// (2, 3000), (1, 2999) to (3000, 3000, 2999). +struct TimeToSampleIterator<'a> { + cur_sample_count: std::ops::Range<u32>, + cur_sample_delta: u32, + stts_iter: std::slice::Iter<'a, mp4parse::Sample>, + track_id: usize, +} + +impl<'a> Iterator for TimeToSampleIterator<'a> { + type Item = u32; + + #[allow(clippy::reversed_empty_ranges)] + fn next(&mut self) -> Option<u32> { + let has_sample = self.cur_sample_count.next().or_else(|| { + self.cur_sample_count = match self.stts_iter.next() { + Some(v) => { + self.cur_sample_delta = v.sample_delta; + 0..v.sample_count + } + _ => 0..0, + }; + + self.cur_sample_count.next() + }); + + has_sample.and(Some(self.cur_sample_delta)) + } +} + +impl<'a> TimeToSampleIterator<'a> { + fn next_delta(&mut self) -> TrackScaledTime<i64> { + match self.next() { + Some(v) => TrackScaledTime::<i64>(i64::from(v), self.track_id), + _ => TrackScaledTime::<i64>(0, self.track_id), + } + } +} + +// Convert 'stco' compact table to full table by iterator. +// (start_chunk_num, sample_number) => (start_chunk_num, sample_number), +// (start_chunk_num + 1, sample_number), +// (start_chunk_num + 2, sample_number), +// ... +// (next start_chunk_num, next sample_number), +// ... +// +// For example: +// (1, 5), (5, 10), (9, 2) => (1, 5), (2, 5), (3, 5), (4, 5), (5, 10), (6, 10), +// (7, 10), (8, 10), (9, 2) +fn sample_to_chunk_iter<'a>( + stsc_samples: &'a TryVec<mp4parse::SampleToChunk>, + stco_offsets: &'a TryVec<u64>, +) -> SampleToChunkIterator<'a> { + SampleToChunkIterator { + chunks: (0..0), + sample_count: 0, + stsc_peek_iter: stsc_samples.as_slice().iter().peekable(), + remain_chunk_count: stco_offsets + .len() + .try_into() + .expect("stco.entry_count is u32"), + } +} + +struct SampleToChunkIterator<'a> { + chunks: std::ops::Range<u32>, + sample_count: u32, + stsc_peek_iter: std::iter::Peekable<std::slice::Iter<'a, mp4parse::SampleToChunk>>, + remain_chunk_count: u32, // total chunk number from 'stco'. +} + +impl<'a> Iterator for SampleToChunkIterator<'a> { + type Item = (u32, u32); + + fn next(&mut self) -> Option<(u32, u32)> { + let has_chunk = self.chunks.next().or_else(|| { + self.chunks = self.locate(); + self.remain_chunk_count + .checked_sub( + self.chunks + .len() + .try_into() + .expect("len() of a Range<u32> must fit in u32"), + ) + .and_then(|res| { + self.remain_chunk_count = res; + self.chunks.next() + }) + }); + + has_chunk.map(|id| (id, self.sample_count)) + } +} + +impl<'a> SampleToChunkIterator<'a> { + #[allow(clippy::reversed_empty_ranges)] + fn locate(&mut self) -> std::ops::Range<u32> { + loop { + return match (self.stsc_peek_iter.next(), self.stsc_peek_iter.peek()) { + (Some(next), Some(peek)) if next.first_chunk == peek.first_chunk => { + // Invalid entry, skip it and will continue searching at + // next loop iteration. + continue; + } + (Some(next), Some(peek)) if next.first_chunk > 0 && peek.first_chunk > 0 => { + self.sample_count = next.samples_per_chunk; + (next.first_chunk - 1)..(peek.first_chunk - 1) + } + (Some(next), None) if next.first_chunk > 0 => { + self.sample_count = next.samples_per_chunk; + // Total chunk number in 'stsc' could be different to 'stco', + // there could be more chunks at the last 'stsc' record. + match next.first_chunk.checked_add(self.remain_chunk_count) { + Some(r) => (next.first_chunk - 1)..r - 1, + _ => 0..0, + } + } + _ => 0..0, + }; + } + } +} + +#[allow(clippy::reversed_empty_ranges)] +fn create_sample_table( + track: &Track, + track_offset_time: CheckedInteger<i64>, +) -> Option<TryVec<Mp4parseIndice>> { + let timescale = match track.timescale { + Some(ref t) => TrackTimeScale::<i64>(t.0 as i64, t.1), + _ => return None, + }; + + let (stsc, stco, stsz, stts) = match (&track.stsc, &track.stco, &track.stsz, &track.stts) { + (&Some(ref a), &Some(ref b), &Some(ref c), &Some(ref d)) => (a, b, c, d), + _ => return None, + }; + + // According to spec, no sync table means every sample is sync sample. + let has_sync_table = match track.stss { + Some(_) => true, + _ => false, + }; + + let mut sample_size_iter = stsz.sample_sizes.iter(); + + // Get 'stsc' iterator for (chunk_id, chunk_sample_count) and calculate the sample + // offset address. + + // With large numbers of samples, the cost of many allocations dominates, + // so it's worth iterating twice to allocate sample_table just once. + let total_sample_count = sample_to_chunk_iter(&stsc.samples, &stco.offsets) + .by_ref() + .map(|(_, sample_counts)| sample_counts.to_usize()) + .sum(); + let mut sample_table = TryVec::with_capacity(total_sample_count).ok()?; + + for i in sample_to_chunk_iter(&stsc.samples, &stco.offsets) { + let chunk_id = i.0 as usize; + let sample_counts = i.1; + let mut cur_position = match stco.offsets.get(chunk_id) { + Some(&i) => i.into(), + _ => return None, + }; + for _ in 0..sample_counts { + let start_offset = cur_position; + let end_offset = match (stsz.sample_size, sample_size_iter.next()) { + (_, Some(t)) => (start_offset + *t)?, + (t, _) if t > 0 => (start_offset + t)?, + _ => 0.into(), + }; + if end_offset == 0 { + return None; + } + cur_position = end_offset; + + sample_table + .push(Mp4parseIndice { + start_offset, + end_offset, + sync: !has_sync_table, + ..Default::default() + }) + .ok()?; + } + } + + // Mark the sync sample in sample_table according to 'stss'. + if let Some(ref v) = track.stss { + for iter in &v.samples { + match iter + .checked_sub(&1) + .and_then(|idx| sample_table.get_mut(idx as usize)) + { + Some(elem) => elem.sync = true, + _ => return None, + } + } + } + + let ctts_iter = match track.ctts { + Some(ref v) => Some(v.samples.as_slice().iter()), + _ => None, + }; + + let mut ctts_offset_iter = TimeOffsetIterator { + cur_sample_range: (0..0), + cur_offset: 0, + ctts_iter, + track_id: track.id, + }; + + let mut stts_iter = TimeToSampleIterator { + cur_sample_count: (0..0), + cur_sample_delta: 0, + stts_iter: stts.samples.as_slice().iter(), + track_id: track.id, + }; + + // sum_delta is the sum of stts_iter delta. + // According to sepc: + // decode time => DT(n) = DT(n-1) + STTS(n) + // composition time => CT(n) = DT(n) + CTTS(n) + // Note: + // composition time needs to add the track offset time from 'elst' table. + let mut sum_delta = TrackScaledTime::<i64>(0, track.id); + for sample in sample_table.as_mut_slice() { + let decode_time = sum_delta; + sum_delta = (sum_delta + stts_iter.next_delta())?; + + // ctts_offset is the current sample offset time. + let ctts_offset = ctts_offset_iter.next_offset_time(); + + let start_composition = track_time_to_us((decode_time + ctts_offset)?, timescale)?; + + let end_composition = track_time_to_us((sum_delta + ctts_offset)?, timescale)?; + + let start_decode = track_time_to_us(decode_time, timescale)?; + + sample.start_composition = (track_offset_time + start_composition)?; + sample.end_composition = (track_offset_time + end_composition)?; + sample.start_decode = start_decode.into(); + } + + // Correct composition end time due to 'ctts' causes composition time re-ordering. + // + // Composition end time is not in specification. However, gecko needs it, so we need to + // calculate to correct the composition end time. + if !sample_table.is_empty() { + // Create an index table refers to sample_table and sorted by start_composisiton time. + let mut sort_table = TryVec::with_capacity(sample_table.len()).ok()?; + + for i in 0..sample_table.len() { + sort_table.push(i).ok()?; + } + + sort_table.sort_by_key(|i| match sample_table.get(*i) { + Some(v) => v.start_composition, + _ => 0.into(), + }); + + for indices in sort_table.windows(2) { + if let [current_index, peek_index] = *indices { + let next_start_composition_time = sample_table[peek_index].start_composition; + let sample = &mut sample_table[current_index]; + sample.end_composition = next_start_composition_time; + } + } + } + + Some(sample_table) +} + +/// Fill the supplied `Mp4parseFragmentInfo` with metadata from fragmented file. +/// +/// # Safety +/// +/// This function is unsafe because it dereferences the the parser and +/// info raw pointers passed to it. Callers should ensure the parser +/// pointer points to a valid `Mp4parseParser` and that the info pointer points +/// to a valid `Mp4parseFragmentInfo`. + +#[no_mangle] +pub unsafe extern "C" fn mp4parse_get_fragment_info( + parser: *mut Mp4parseParser, + info: *mut Mp4parseFragmentInfo, +) -> Mp4parseStatus { + if parser.is_null() || info.is_null() { + return Mp4parseStatus::BadArg; + } + + // Initialize fields to default values to ensure all fields are always valid. + *info = Default::default(); + + let context = (*parser).context(); + let info: &mut Mp4parseFragmentInfo = &mut *info; + + info.fragment_duration = 0; + + let duration = match context.mvex { + Some(ref mvex) => mvex.fragment_duration, + None => return Mp4parseStatus::Invalid, + }; + + if let (Some(time), Some(scale)) = (duration, context.timescale) { + info.fragment_duration = match media_time_to_us(time, scale) { + Some(time_us) => time_us as u64, + None => return Mp4parseStatus::Invalid, + } + } + + Mp4parseStatus::Ok +} + +/// Determine if an mp4 file is fragmented. A fragmented file needs mvex table +/// and contains no data in stts, stsc, and stco boxes. +/// +/// # Safety +/// +/// This function is unsafe because it dereferences the the parser and +/// fragmented raw pointers passed to it. Callers should ensure the parser +/// pointer points to a valid `Mp4parseParser` and that the fragmented pointer +/// points to an appropriate memory location to have a `u8` written to. +#[no_mangle] +pub unsafe extern "C" fn mp4parse_is_fragmented( + parser: *mut Mp4parseParser, + track_id: u32, + fragmented: *mut u8, +) -> Mp4parseStatus { + if parser.is_null() { + return Mp4parseStatus::BadArg; + } + + let context = (*parser).context_mut(); + let tracks = &context.tracks; + (*fragmented) = false as u8; + + if context.mvex.is_none() { + return Mp4parseStatus::Ok; + } + + // check sample tables. + let mut iter = tracks.iter(); + iter.find(|track| track.track_id == Some(track_id)) + .map_or(Mp4parseStatus::BadArg, |track| { + match (&track.stsc, &track.stco, &track.stts) { + (&Some(ref stsc), &Some(ref stco), &Some(ref stts)) + if stsc.samples.is_empty() + && stco.offsets.is_empty() + && stts.samples.is_empty() => + { + (*fragmented) = true as u8 + } + _ => {} + }; + Mp4parseStatus::Ok + }) +} + +/// Get 'pssh' system id and 'pssh' box content for eme playback. +/// +/// The data format of the `info` struct passed to gecko is: +/// +/// - system id (16 byte uuid) +/// - pssh box size (32-bit native endian) +/// - pssh box content (including header) +/// +/// # Safety +/// +/// This function is unsafe because it dereferences the the parser and +/// info raw pointers passed to it. Callers should ensure the parser +/// pointer points to a valid `Mp4parseParser` and that the fragmented pointer +/// points to a valid `Mp4parsePsshInfo`. +#[no_mangle] +pub unsafe extern "C" fn mp4parse_get_pssh_info( + parser: *mut Mp4parseParser, + info: *mut Mp4parsePsshInfo, +) -> Mp4parseStatus { + if parser.is_null() || info.is_null() { + return Mp4parseStatus::BadArg; + } + + // Initialize fields to default values to ensure all fields are always valid. + *info = Default::default(); + + get_pssh_info(&mut *parser, &mut *info).into() +} + +fn get_pssh_info( + parser: &mut Mp4parseParser, + info: &mut Mp4parsePsshInfo, +) -> Result<(), Mp4parseStatus> { + let Mp4parseParser { + context, pssh_data, .. + } = parser; + + pssh_data.clear(); + for pssh in &context.psshs { + let content_len = pssh + .box_content + .len() + .try_into() + .map_err(|_| Mp4parseStatus::Invalid)?; + let mut data_len = TryVec::new(); + if data_len + .write_u32::<byteorder::NativeEndian>(content_len) + .is_err() + { + return Err(Mp4parseStatus::Io); + } + pssh_data.extend_from_slice(pssh.system_id.as_slice())?; + pssh_data.extend_from_slice(data_len.as_slice())?; + pssh_data.extend_from_slice(pssh.box_content.as_slice())?; + } + + info.data.set_data(pssh_data); + + Ok(()) +} + +#[cfg(test)] +extern "C" fn error_read(_: *mut u8, _: usize, _: *mut std::os::raw::c_void) -> isize { + -1 +} + +#[cfg(test)] +extern "C" fn valid_read(buf: *mut u8, size: usize, userdata: *mut std::os::raw::c_void) -> isize { + let input: &mut std::fs::File = unsafe { &mut *(userdata as *mut _) }; + + let mut buf = unsafe { std::slice::from_raw_parts_mut(buf, size) }; + match input.read(&mut buf) { + Ok(n) => n as isize, + Err(_) => -1, + } +} + +#[test] +fn get_track_count_null_parser() { + unsafe { + let mut count: u32 = 0; + let rv = mp4parse_get_track_count(std::ptr::null(), std::ptr::null_mut()); + assert_eq!(rv, Mp4parseStatus::BadArg); + let rv = mp4parse_get_track_count(std::ptr::null(), &mut count); + assert_eq!(rv, Mp4parseStatus::BadArg); + } +} + +#[test] +fn arg_validation() { + unsafe { + let rv = mp4parse_new(std::ptr::null(), std::ptr::null_mut()); + assert_eq!(rv, Mp4parseStatus::BadArg); + + // Passing a null Mp4parseIo is an error. + let mut parser = std::ptr::null_mut(); + let rv = mp4parse_new(std::ptr::null(), &mut parser); + assert_eq!(rv, Mp4parseStatus::BadArg); + assert!(parser.is_null()); + + let null_mut: *mut std::os::raw::c_void = std::ptr::null_mut(); + + // Passing an Mp4parseIo with null members is an error. + let io = Mp4parseIo { + read: None, + userdata: null_mut, + }; + let mut parser = std::ptr::null_mut(); + let rv = mp4parse_new(&io, &mut parser); + assert_eq!(rv, Mp4parseStatus::BadArg); + assert!(parser.is_null()); + + let mut dummy_value = 42; + let io = Mp4parseIo { + read: None, + userdata: &mut dummy_value as *mut _ as *mut std::os::raw::c_void, + }; + let mut parser = std::ptr::null_mut(); + let rv = mp4parse_new(&io, &mut parser); + assert_eq!(rv, Mp4parseStatus::BadArg); + assert!(parser.is_null()); + + let mut dummy_info = Mp4parseTrackInfo { + track_type: Mp4parseTrackType::Video, + ..Default::default() + }; + assert_eq!( + Mp4parseStatus::BadArg, + mp4parse_get_track_info(std::ptr::null_mut(), 0, &mut dummy_info) + ); + + let mut dummy_video = Mp4parseTrackVideoInfo { + display_width: 0, + display_height: 0, + rotation: 0, + sample_info_count: 0, + sample_info: std::ptr::null(), + }; + assert_eq!( + Mp4parseStatus::BadArg, + mp4parse_get_track_video_info(std::ptr::null_mut(), 0, &mut dummy_video) + ); + + let mut dummy_audio = Default::default(); + assert_eq!( + Mp4parseStatus::BadArg, + mp4parse_get_track_audio_info(std::ptr::null_mut(), 0, &mut dummy_audio) + ); + } +} + +#[test] +fn parser_input_must_be_null() { + let mut dummy_value = 42; + let io = Mp4parseIo { + read: Some(error_read), + userdata: &mut dummy_value as *mut _ as *mut std::os::raw::c_void, + }; + let mut parser = 0xDEAD_BEEF as *mut _; + let rv = unsafe { mp4parse_new(&io, &mut parser) }; + assert_eq!(rv, Mp4parseStatus::BadArg); +} + +#[test] +fn arg_validation_with_parser() { + unsafe { + let mut dummy_value = 42; + let io = Mp4parseIo { + read: Some(error_read), + userdata: &mut dummy_value as *mut _ as *mut std::os::raw::c_void, + }; + let mut parser = std::ptr::null_mut(); + let rv = mp4parse_new(&io, &mut parser); + assert_eq!(rv, Mp4parseStatus::Io); + assert!(parser.is_null()); + + // Null info pointers are an error. + assert_eq!( + Mp4parseStatus::BadArg, + mp4parse_get_track_info(parser, 0, std::ptr::null_mut()) + ); + assert_eq!( + Mp4parseStatus::BadArg, + mp4parse_get_track_video_info(parser, 0, std::ptr::null_mut()) + ); + assert_eq!( + Mp4parseStatus::BadArg, + mp4parse_get_track_audio_info(parser, 0, std::ptr::null_mut()) + ); + + let mut dummy_info = Mp4parseTrackInfo { + track_type: Mp4parseTrackType::Video, + ..Default::default() + }; + assert_eq!( + Mp4parseStatus::BadArg, + mp4parse_get_track_info(parser, 0, &mut dummy_info) + ); + + let mut dummy_video = Mp4parseTrackVideoInfo { + display_width: 0, + display_height: 0, + rotation: 0, + sample_info_count: 0, + sample_info: std::ptr::null(), + }; + assert_eq!( + Mp4parseStatus::BadArg, + mp4parse_get_track_video_info(parser, 0, &mut dummy_video) + ); + + let mut dummy_audio = Default::default(); + assert_eq!( + Mp4parseStatus::BadArg, + mp4parse_get_track_audio_info(parser, 0, &mut dummy_audio) + ); + } +} + +#[cfg(test)] +fn parse_minimal_mp4() -> *mut Mp4parseParser { + let mut file = std::fs::File::open("../mp4parse/tests/minimal.mp4").unwrap(); + let io = Mp4parseIo { + read: Some(valid_read), + userdata: &mut file as *mut _ as *mut std::os::raw::c_void, + }; + let mut parser = std::ptr::null_mut(); + let rv = unsafe { mp4parse_new(&io, &mut parser) }; + assert_eq!(Mp4parseStatus::Ok, rv); + parser +} + +#[test] +fn minimal_mp4_parse_ok() { + let parser = parse_minimal_mp4(); + + assert!(!parser.is_null()); + + unsafe { + mp4parse_free(parser); + } +} + +#[test] +fn minimal_mp4_get_track_cout() { + let parser = parse_minimal_mp4(); + + let mut count: u32 = 0; + assert_eq!(Mp4parseStatus::Ok, unsafe { + mp4parse_get_track_count(parser, &mut count) + }); + assert_eq!(2, count); + + unsafe { + mp4parse_free(parser); + } +} + +#[test] +fn minimal_mp4_get_track_info() { + let parser = parse_minimal_mp4(); + + let mut info = Mp4parseTrackInfo { + track_type: Mp4parseTrackType::Video, + ..Default::default() + }; + assert_eq!(Mp4parseStatus::Ok, unsafe { + mp4parse_get_track_info(parser, 0, &mut info) + }); + assert_eq!(info.track_type, Mp4parseTrackType::Video); + assert_eq!(info.track_id, 1); + assert_eq!(info.duration, 40000); + assert_eq!(info.media_time, 0); + + assert_eq!(Mp4parseStatus::Ok, unsafe { + mp4parse_get_track_info(parser, 1, &mut info) + }); + assert_eq!(info.track_type, Mp4parseTrackType::Audio); + assert_eq!(info.track_id, 2); + assert_eq!(info.duration, 61333); + assert_eq!(info.media_time, 21333); + + unsafe { + mp4parse_free(parser); + } +} + +#[test] +fn minimal_mp4_get_track_video_info() { + let parser = parse_minimal_mp4(); + + let mut video = Mp4parseTrackVideoInfo::default(); + assert_eq!(Mp4parseStatus::Ok, unsafe { + mp4parse_get_track_video_info(parser, 0, &mut video) + }); + assert_eq!(video.display_width, 320); + assert_eq!(video.display_height, 240); + assert_eq!(video.sample_info_count, 1); + + unsafe { + assert_eq!((*video.sample_info).image_width, 320); + assert_eq!((*video.sample_info).image_height, 240); + } + + unsafe { + mp4parse_free(parser); + } +} + +#[test] +fn minimal_mp4_get_track_audio_info() { + let parser = parse_minimal_mp4(); + + let mut audio = Mp4parseTrackAudioInfo::default(); + assert_eq!(Mp4parseStatus::Ok, unsafe { + mp4parse_get_track_audio_info(parser, 1, &mut audio) + }); + assert_eq!(audio.sample_info_count, 1); + + unsafe { + assert_eq!((*audio.sample_info).channels, 1); + assert_eq!((*audio.sample_info).bit_depth, 16); + assert_eq!((*audio.sample_info).sample_rate, 48000); + } + + unsafe { + mp4parse_free(parser); + } +} + +#[test] +fn minimal_mp4_get_track_info_invalid_track_number() { + let parser = parse_minimal_mp4(); + + let mut info = Mp4parseTrackInfo { + track_type: Mp4parseTrackType::Video, + ..Default::default() + }; + assert_eq!(Mp4parseStatus::BadArg, unsafe { + mp4parse_get_track_info(parser, 3, &mut info) + }); + assert_eq!(info.track_type, Mp4parseTrackType::Video); + assert_eq!(info.track_id, 0); + assert_eq!(info.duration, 0); + assert_eq!(info.media_time, 0); + + let mut video = Mp4parseTrackVideoInfo::default(); + assert_eq!(Mp4parseStatus::BadArg, unsafe { + mp4parse_get_track_video_info(parser, 3, &mut video) + }); + assert_eq!(video.display_width, 0); + assert_eq!(video.display_height, 0); + assert_eq!(video.sample_info_count, 0); + + let mut audio = Default::default(); + assert_eq!(Mp4parseStatus::BadArg, unsafe { + mp4parse_get_track_audio_info(parser, 3, &mut audio) + }); + assert_eq!(audio.sample_info_count, 0); + + unsafe { + mp4parse_free(parser); + } +} + +#[test] +fn rational_scale_overflow() { + assert_eq!(rational_scale::<u64, u64>(17, 3, 1000), Some(5666)); + let large = 0x4000_0000_0000_0000; + assert_eq!(rational_scale::<u64, u64>(large, 2, 2), Some(large)); + assert_eq!(rational_scale::<u64, u64>(large, 4, 4), Some(large)); + assert_eq!(rational_scale::<u64, u64>(large, 2, 8), None); + assert_eq!(rational_scale::<u64, u64>(large, 8, 4), Some(large / 2)); + assert_eq!(rational_scale::<u64, u64>(large + 1, 4, 4), Some(large + 1)); + assert_eq!(rational_scale::<u64, u64>(large, 40, 1000), None); +} + +#[test] +fn media_time_overflow() { + let scale = MediaTimeScale(90000); + let duration = MediaScaledTime(9_007_199_254_710_000); + assert_eq!( + media_time_to_us(duration, scale), + Some(100_079_991_719_000_000) + ); +} + +#[test] +fn track_time_overflow() { + let scale = TrackTimeScale(44100u64, 0); + let duration = TrackScaledTime(4_413_527_634_807_900u64, 0); + assert_eq!( + track_time_to_us(duration, scale), + Some(100_079_991_719_000_000) + ); +} diff --git a/third_party/rust/mp4parse_capi/tests/test_chunk_out_of_range.rs b/third_party/rust/mp4parse_capi/tests/test_chunk_out_of_range.rs new file mode 100644 index 0000000000..5529c92182 --- /dev/null +++ b/third_party/rust/mp4parse_capi/tests/test_chunk_out_of_range.rs @@ -0,0 +1,45 @@ +extern crate mp4parse_capi; +use mp4parse_capi::*; +use std::io::Read; + +extern "C" fn buf_read(buf: *mut u8, size: usize, userdata: *mut std::os::raw::c_void) -> isize { + let input: &mut std::fs::File = unsafe { &mut *(userdata as *mut _) }; + let mut buf = unsafe { std::slice::from_raw_parts_mut(buf, size) }; + match input.read(&mut buf) { + Ok(n) => n as isize, + Err(_) => -1, + } +} + +#[test] +fn parse_out_of_chunk_range() { + let mut file = std::fs::File::open("tests/chunk_out_of_range.mp4").expect("Unknown file"); + let io = Mp4parseIo { + read: Some(buf_read), + userdata: &mut file as *mut _ as *mut std::os::raw::c_void, + }; + + unsafe { + let mut parser = std::ptr::null_mut(); + let mut rv = mp4parse_new(&io, &mut parser); + assert_eq!(rv, Mp4parseStatus::Ok); + assert!(!parser.is_null()); + + let mut counts: u32 = 0; + rv = mp4parse_get_track_count(parser, &mut counts); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(counts, 1); + + // its first chunk is out of range. + // <SampleToChunkBox EntryCount="1"> + // <BoxInfo Size="28" Type="stsc"/> + // <FullBoxInfo Version="0" Flags="0x0"/> + // <SampleToChunkEntry FirstChunk="16777217" SamplesPerChunk="17" SampleDescriptionIndex="1"/> + // + let mut indice = Mp4parseByteData::default(); + let rv = mp4parse_get_indice_table(parser, 1, &mut indice); + assert_eq!(rv, Mp4parseStatus::Invalid); + + mp4parse_free(parser); + } +} diff --git a/third_party/rust/mp4parse_capi/tests/test_encryption.rs b/third_party/rust/mp4parse_capi/tests/test_encryption.rs new file mode 100644 index 0000000000..33c9c6d9f9 --- /dev/null +++ b/third_party/rust/mp4parse_capi/tests/test_encryption.rs @@ -0,0 +1,213 @@ +extern crate mp4parse_capi; +use mp4parse_capi::*; +use std::io::Read; + +extern "C" fn buf_read(buf: *mut u8, size: usize, userdata: *mut std::os::raw::c_void) -> isize { + let input: &mut std::fs::File = unsafe { &mut *(userdata as *mut _) }; + let mut buf = unsafe { std::slice::from_raw_parts_mut(buf, size) }; + match input.read(&mut buf) { + Ok(n) => n as isize, + Err(_) => -1, + } +} + +#[test] +#[allow(clippy::cognitive_complexity)] // TODO: Consider simplifying this +fn parse_cenc() { + let mut file = std::fs::File::open("tests/short-cenc.mp4").expect("Unknown file"); + let io = Mp4parseIo { + read: Some(buf_read), + userdata: &mut file as *mut _ as *mut std::os::raw::c_void, + }; + + unsafe { + let mut parser = std::ptr::null_mut(); + let mut rv = mp4parse_new(&io, &mut parser); + assert_eq!(rv, Mp4parseStatus::Ok); + assert!(!parser.is_null()); + let mut counts: u32 = 0; + rv = mp4parse_get_track_count(parser, &mut counts); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(counts, 2); + + // Make sure we have a video track and it's at index 0 + let mut video_track_info = Mp4parseTrackInfo::default(); + rv = mp4parse_get_track_info(parser, 0, &mut video_track_info); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(video_track_info.track_type, Mp4parseTrackType::Video); + + // Make sure we have a audio track and it's at index 1 + let mut audio_track_info = Mp4parseTrackInfo::default(); + rv = mp4parse_get_track_info(parser, 1, &mut audio_track_info); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(audio_track_info.track_type, Mp4parseTrackType::Audio); + + // Verify video track and crypto information + let mut video = Mp4parseTrackVideoInfo::default(); + rv = mp4parse_get_track_video_info(parser, 0, &mut video); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(video.sample_info_count, 1); + assert_eq!((*video.sample_info).codec_type, Mp4parseCodec::Avc); + assert_eq!((*video.sample_info).image_width, 320); + assert_eq!((*video.sample_info).image_height, 240); + let protected_data = &(*video.sample_info).protected_data; + assert_eq!( + protected_data.original_format, + OptionalFourCC::Some(*b"avc1") + ); + assert_eq!( + protected_data.scheme_type, + Mp4ParseEncryptionSchemeType::Cenc + ); + assert_eq!(protected_data.is_encrypted, 0x01); + assert_eq!(protected_data.iv_size, 16); + assert_eq!(protected_data.kid.length, 16); + let expected_kid = [ + 0x7e, 0x57, 0x1d, 0x01, 0x7e, 0x57, 0x1d, 0x01, 0x7e, 0x57, 0x1d, 0x01, 0x7e, 0x57, + 0x1d, 0x01, + ]; + for (i, expected_byte) in expected_kid.iter().enumerate() { + assert_eq!(&(*protected_data.kid.data.add(i)), expected_byte); + } + assert_eq!(protected_data.crypt_byte_block, 0); + assert_eq!(protected_data.skip_byte_block, 0); + assert_eq!(protected_data.constant_iv.length, 0); + + // Verify audio track and crypto information + let mut audio = Mp4parseTrackAudioInfo::default(); + rv = mp4parse_get_track_audio_info(parser, 1, &mut audio); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(audio.sample_info_count, 1); + assert_eq!((*audio.sample_info).codec_type, Mp4parseCodec::Aac); + assert_eq!((*audio.sample_info).channels, 2); + assert_eq!((*audio.sample_info).bit_depth, 16); + assert_eq!((*audio.sample_info).sample_rate, 44100); + let protected_data = &(*audio.sample_info).protected_data; + assert_eq!( + protected_data.original_format, + OptionalFourCC::Some(*b"mp4a") + ); + assert_eq!(protected_data.is_encrypted, 0x01); + assert_eq!(protected_data.iv_size, 16); + assert_eq!(protected_data.kid.length, 16); + let expected_kid = [ + 0x7e, 0x57, 0x1d, 0x02, 0x7e, 0x57, 0x1d, 0x02, 0x7e, 0x57, 0x1d, 0x02, 0x7e, 0x57, + 0x1d, 0x02, + ]; + for (i, expected_byte) in expected_kid.iter().enumerate() { + assert_eq!(&(*protected_data.kid.data.add(i)), expected_byte); + } + assert_eq!(protected_data.crypt_byte_block, 0); + assert_eq!(protected_data.skip_byte_block, 0); + assert_eq!(protected_data.constant_iv.length, 0); + } +} + +#[test] +fn parse_cbcs() { + let mut file = std::fs::File::open("tests/bipbop_cbcs_video_init.mp4").expect("Unknown file"); + let io = Mp4parseIo { + read: Some(buf_read), + userdata: &mut file as *mut _ as *mut std::os::raw::c_void, + }; + + unsafe { + let mut parser = std::ptr::null_mut(); + let mut rv = mp4parse_new(&io, &mut parser); + assert_eq!(rv, Mp4parseStatus::Ok); + assert!(!parser.is_null()); + let mut counts: u32 = 0; + rv = mp4parse_get_track_count(parser, &mut counts); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(counts, 1); + + // Make sure we have a video track + let mut video_track_info = Mp4parseTrackInfo::default(); + rv = mp4parse_get_track_info(parser, 0, &mut video_track_info); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(video_track_info.track_type, Mp4parseTrackType::Video); + + // Verify video track and crypto information + let mut video = Mp4parseTrackVideoInfo::default(); + rv = mp4parse_get_track_video_info(parser, 0, &mut video); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(video.sample_info_count, 2); + assert_eq!((*video.sample_info).codec_type, Mp4parseCodec::Avc); + assert_eq!((*video.sample_info).image_width, 400); + assert_eq!((*video.sample_info).image_height, 300); + let protected_data = &(*video.sample_info).protected_data; + assert_eq!( + protected_data.original_format, + OptionalFourCC::Some(*b"avc1") + ); + assert_eq!( + protected_data.scheme_type, + Mp4ParseEncryptionSchemeType::Cbcs + ); + assert_eq!(protected_data.is_encrypted, 0x01); + assert_eq!(protected_data.iv_size, 0); + assert_eq!(protected_data.kid.length, 16); + let expected_kid = [ + 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, + 0x1d, 0x21, + ]; + for (i, expected_byte) in expected_kid.iter().enumerate() { + assert_eq!(&(*protected_data.kid.data.add(i)), expected_byte); + } + assert_eq!(protected_data.crypt_byte_block, 1); + assert_eq!(protected_data.skip_byte_block, 9); + assert_eq!(protected_data.constant_iv.length, 16); + let expected_iv = [ + 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0x11, 0x22, 0x33, 0x44, + 0x55, 0x66, + ]; + for (i, expected_byte) in expected_iv.iter().enumerate() { + assert_eq!(&(*protected_data.constant_iv.data.add(i)), expected_byte); + } + } +} + +#[test] +fn parse_unencrypted() { + // Ensure the encryption related data is not populated for files without + // encryption metadata. + let mut file = std::fs::File::open("tests/opus_audioinit.mp4").expect("Unknown file"); + let io = Mp4parseIo { + read: Some(buf_read), + userdata: &mut file as *mut _ as *mut std::os::raw::c_void, + }; + + unsafe { + let mut parser = std::ptr::null_mut(); + let mut rv = mp4parse_new(&io, &mut parser); + assert_eq!(rv, Mp4parseStatus::Ok); + assert!(!parser.is_null()); + + let mut counts: u32 = 0; + rv = mp4parse_get_track_count(parser, &mut counts); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(counts, 1); + + let mut track_info = Mp4parseTrackInfo::default(); + rv = mp4parse_get_track_info(parser, 0, &mut track_info); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(track_info.track_type, Mp4parseTrackType::Audio); + + let mut audio = Mp4parseTrackAudioInfo::default(); + rv = mp4parse_get_track_audio_info(parser, 0, &mut audio); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(audio.sample_info_count, 1); + let protected_data = &(*audio.sample_info).protected_data; + assert_eq!(protected_data.original_format, OptionalFourCC::None); + assert_eq!( + protected_data.scheme_type, + Mp4ParseEncryptionSchemeType::None + ); + assert_eq!(protected_data.is_encrypted, 0x00); + assert_eq!(protected_data.iv_size, 0); + assert_eq!(protected_data.kid.length, 0); + assert_eq!(protected_data.crypt_byte_block, 0); + assert_eq!(protected_data.skip_byte_block, 0); + assert_eq!(protected_data.constant_iv.length, 0); + } +} diff --git a/third_party/rust/mp4parse_capi/tests/test_fragment.rs b/third_party/rust/mp4parse_capi/tests/test_fragment.rs new file mode 100644 index 0000000000..5ded45fd3d --- /dev/null +++ b/third_party/rust/mp4parse_capi/tests/test_fragment.rs @@ -0,0 +1,117 @@ +extern crate mp4parse_capi; +use mp4parse_capi::*; +use std::io::Read; + +extern "C" fn buf_read(buf: *mut u8, size: usize, userdata: *mut std::os::raw::c_void) -> isize { + let input: &mut std::fs::File = unsafe { &mut *(userdata as *mut _) }; + let mut buf = unsafe { std::slice::from_raw_parts_mut(buf, size) }; + match input.read(&mut buf) { + Ok(n) => n as isize, + Err(_) => -1, + } +} + +#[test] +fn parse_fragment() { + let mut file = std::fs::File::open("tests/bipbop_audioinit.mp4").expect("Unknown file"); + let io = Mp4parseIo { + read: Some(buf_read), + userdata: &mut file as *mut _ as *mut std::os::raw::c_void, + }; + + unsafe { + let mut parser = std::ptr::null_mut(); + let mut rv = mp4parse_new(&io, &mut parser); + assert_eq!(rv, Mp4parseStatus::Ok); + assert!(!parser.is_null()); + + let mut counts: u32 = 0; + rv = mp4parse_get_track_count(parser, &mut counts); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(counts, 1); + + let mut track_info = Mp4parseTrackInfo::default(); + rv = mp4parse_get_track_info(parser, 0, &mut track_info); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(track_info.track_type, Mp4parseTrackType::Audio); + assert_eq!(track_info.track_id, 1); + assert_eq!(track_info.duration, 0); + assert_eq!(track_info.media_time, 0); + + let mut audio = Default::default(); + rv = mp4parse_get_track_audio_info(parser, 0, &mut audio); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(audio.sample_info_count, 1); + + assert_eq!((*audio.sample_info).codec_type, Mp4parseCodec::Aac); + assert_eq!((*audio.sample_info).channels, 2); + assert_eq!((*audio.sample_info).bit_depth, 16); + assert_eq!((*audio.sample_info).sample_rate, 22050); + assert_eq!((*audio.sample_info).extra_data.length, 27); + assert_eq!((*audio.sample_info).codec_specific_config.length, 2); + + let mut is_fragmented_file: u8 = 0; + rv = mp4parse_is_fragmented(parser, track_info.track_id, &mut is_fragmented_file); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(is_fragmented_file, 1); + + let mut fragment_info = Mp4parseFragmentInfo::default(); + rv = mp4parse_get_fragment_info(parser, &mut fragment_info); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(fragment_info.fragment_duration, 10_032_000); + + mp4parse_free(parser); + } +} + +#[test] +fn parse_opus_fragment() { + let mut file = std::fs::File::open("tests/opus_audioinit.mp4").expect("Unknown file"); + let io = Mp4parseIo { + read: Some(buf_read), + userdata: &mut file as *mut _ as *mut std::os::raw::c_void, + }; + + unsafe { + let mut parser = std::ptr::null_mut(); + let mut rv = mp4parse_new(&io, &mut parser); + assert_eq!(rv, Mp4parseStatus::Ok); + assert!(!parser.is_null()); + + let mut counts: u32 = 0; + rv = mp4parse_get_track_count(parser, &mut counts); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(counts, 1); + + let mut track_info = Mp4parseTrackInfo::default(); + rv = mp4parse_get_track_info(parser, 0, &mut track_info); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(track_info.track_type, Mp4parseTrackType::Audio); + assert_eq!(track_info.track_id, 1); + assert_eq!(track_info.duration, 0); + assert_eq!(track_info.media_time, 0); + + let mut audio = Default::default(); + rv = mp4parse_get_track_audio_info(parser, 0, &mut audio); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(audio.sample_info_count, 1); + + assert_eq!((*audio.sample_info).codec_type, Mp4parseCodec::Opus); + assert_eq!((*audio.sample_info).channels, 1); + assert_eq!((*audio.sample_info).bit_depth, 16); + assert_eq!((*audio.sample_info).sample_rate, 48000); + assert_eq!((*audio.sample_info).extra_data.length, 0); + assert_eq!((*audio.sample_info).codec_specific_config.length, 19); + + let mut is_fragmented_file: u8 = 0; + rv = mp4parse_is_fragmented(parser, track_info.track_id, &mut is_fragmented_file); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(is_fragmented_file, 1); + + let mut fragment_info = Mp4parseFragmentInfo::default(); + rv = mp4parse_get_fragment_info(parser, &mut fragment_info); + assert_eq!(rv, Mp4parseStatus::Ok); + + mp4parse_free(parser); + } +} diff --git a/third_party/rust/mp4parse_capi/tests/test_rotation.rs b/third_party/rust/mp4parse_capi/tests/test_rotation.rs new file mode 100644 index 0000000000..45991c6ada --- /dev/null +++ b/third_party/rust/mp4parse_capi/tests/test_rotation.rs @@ -0,0 +1,41 @@ +extern crate mp4parse_capi; +use mp4parse_capi::*; +use std::io::Read; + +extern "C" fn buf_read(buf: *mut u8, size: usize, userdata: *mut std::os::raw::c_void) -> isize { + let input: &mut std::fs::File = unsafe { &mut *(userdata as *mut _) }; + let mut buf = unsafe { std::slice::from_raw_parts_mut(buf, size) }; + match input.read(&mut buf) { + Ok(n) => n as isize, + Err(_) => -1, + } +} + +#[test] +fn parse_rotation() { + let mut file = std::fs::File::open("tests/video_rotation_90.mp4").expect("Unknown file"); + let io = Mp4parseIo { + read: Some(buf_read), + userdata: &mut file as *mut _ as *mut std::os::raw::c_void, + }; + + unsafe { + let mut parser = std::ptr::null_mut(); + let mut rv = mp4parse_new(&io, &mut parser); + assert_eq!(rv, Mp4parseStatus::Ok); + assert!(!parser.is_null()); + + let mut counts: u32 = 0; + rv = mp4parse_get_track_count(parser, &mut counts); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(counts, 1); + + let mut video = Mp4parseTrackVideoInfo::default(); + + let rv = mp4parse_get_track_video_info(parser, 0, &mut video); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(video.rotation, 90); + + mp4parse_free(parser); + } +} diff --git a/third_party/rust/mp4parse_capi/tests/test_sample_table.rs b/third_party/rust/mp4parse_capi/tests/test_sample_table.rs new file mode 100644 index 0000000000..018bfc8e9b --- /dev/null +++ b/third_party/rust/mp4parse_capi/tests/test_sample_table.rs @@ -0,0 +1,278 @@ +extern crate mp4parse_capi; +use mp4parse_capi::*; +use std::io::Read; + +extern "C" fn buf_read(buf: *mut u8, size: usize, userdata: *mut std::os::raw::c_void) -> isize { + let input: &mut std::fs::File = unsafe { &mut *(userdata as *mut _) }; + let mut buf = unsafe { std::slice::from_raw_parts_mut(buf, size) }; + match input.read(&mut buf) { + Ok(n) => n as isize, + Err(_) => -1, + } +} + +#[test] +fn parse_sample_table() { + let mut file = + std::fs::File::open("tests/bipbop_nonfragment_header.mp4").expect("Unknown file"); + let io = Mp4parseIo { + read: Some(buf_read), + userdata: &mut file as *mut _ as *mut std::os::raw::c_void, + }; + + unsafe { + let mut parser = std::ptr::null_mut(); + let mut rv = mp4parse_new(&io, &mut parser); + assert_eq!(rv, Mp4parseStatus::Ok); + assert!(!parser.is_null()); + + let mut counts: u32 = 0; + rv = mp4parse_get_track_count(parser, &mut counts); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(counts, 2); + + let mut track_info = Mp4parseTrackInfo::default(); + rv = mp4parse_get_track_info(parser, 1, &mut track_info); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(track_info.track_type, Mp4parseTrackType::Audio); + + // Check audio smaple table + let mut is_fragmented_file: u8 = 0; + rv = mp4parse_is_fragmented(parser, track_info.track_id, &mut is_fragmented_file); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(is_fragmented_file, 0); + + let mut indice = Mp4parseByteData::default(); + rv = mp4parse_get_indice_table(parser, track_info.track_id, &mut indice); + assert_eq!(rv, Mp4parseStatus::Ok); + + // Compare the value from stagefright. + let audio_indice_0 = Mp4parseIndice { + start_offset: 27_046.into(), + end_offset: 27_052.into(), + start_composition: 0.into(), + end_composition: 46_439.into(), + start_decode: 0.into(), + sync: true, + }; + let audio_indice_215 = Mp4parseIndice { + start_offset: 283_550.into(), + end_offset: 283_556.into(), + start_composition: 9_984_580.into(), + end_composition: 10_031_020.into(), + start_decode: 9_984_580.into(), + sync: true, + }; + assert_eq!(indice.length, 216); + assert_eq!(*indice.indices.offset(0), audio_indice_0); + assert_eq!(*indice.indices.offset(215), audio_indice_215); + + // Check video smaple table + rv = mp4parse_get_track_info(parser, 0, &mut track_info); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(track_info.track_type, Mp4parseTrackType::Video); + + let mut is_fragmented_file: u8 = 0; + rv = mp4parse_is_fragmented(parser, track_info.track_id, &mut is_fragmented_file); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(is_fragmented_file, 0); + + let mut indice = Mp4parseByteData::default(); + rv = mp4parse_get_indice_table(parser, track_info.track_id, &mut indice); + assert_eq!(rv, Mp4parseStatus::Ok); + + // Compare the last few data from stagefright. + let video_indice_291 = Mp4parseIndice { + start_offset: 280_226.into(), + end_offset: 280_855.into(), + start_composition: 9_838_333.into(), + end_composition: 9_871_677.into(), + start_decode: 9_710_000.into(), + sync: false, + }; + let video_indice_292 = Mp4parseIndice { + start_offset: 280_855.into(), + end_offset: 281_297.into(), + start_composition: 9_805_011.into(), + end_composition: 9_838_333.into(), + start_decode: 9_710_011.into(), + sync: false, + }; + // TODO: start_composition time in stagefright is 9905000, but it is 9904999 in parser, it + // could be rounding error. + //let video_indice_293 = Mp4parseIndice { start_offset: 281_297, end_offset: 281_919, start_composition: 9_905_000, end_composition: 9_938_344, start_decode: 9_776_666, sync: false }; + //let video_indice_294 = Mp4parseIndice { start_offset: 281_919, end_offset: 282_391, start_composition: 9_871_677, end_composition: 9_905_000, start_decode: 9_776_677, sync: false }; + let video_indice_295 = Mp4parseIndice { + start_offset: 282_391.into(), + end_offset: 283_032.into(), + start_composition: 9_971_666.into(), + end_composition: 9_971_677.into(), + start_decode: 9_843_333.into(), + sync: false, + }; + let video_indice_296 = Mp4parseIndice { + start_offset: 283_092.into(), + end_offset: 283_526.into(), + start_composition: 9_938_344.into(), + end_composition: 9_971_666.into(), + start_decode: 9_843_344.into(), + sync: false, + }; + + assert_eq!(indice.length, 297); + assert_eq!(*indice.indices.offset(291), video_indice_291); + assert_eq!(*indice.indices.offset(292), video_indice_292); + //assert_eq!(*indice.indices.offset(293), video_indice_293); + //assert_eq!(*indice.indices.offset(294), video_indice_294); + assert_eq!(*indice.indices.offset(295), video_indice_295); + assert_eq!(*indice.indices.offset(296), video_indice_296); + + mp4parse_free(parser); + } +} + +#[test] +fn parse_sample_table_with_elst() { + let mut file = std::fs::File::open("tests/short-cenc.mp4").expect("Unknown file"); + let io = Mp4parseIo { + read: Some(buf_read), + userdata: &mut file as *mut _ as *mut std::os::raw::c_void, + }; + + unsafe { + let mut parser = std::ptr::null_mut(); + let mut rv = mp4parse_new(&io, &mut parser); + assert_eq!(rv, Mp4parseStatus::Ok); + assert!(!parser.is_null()); + + let mut counts: u32 = 0; + rv = mp4parse_get_track_count(parser, &mut counts); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(counts, 2); + + let mut track_info = Mp4parseTrackInfo::default(); + rv = mp4parse_get_track_info(parser, 1, &mut track_info); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(track_info.track_type, Mp4parseTrackType::Audio); + + // Check audio sample table + let mut is_fragmented_file: u8 = std::u8::MAX; + rv = mp4parse_is_fragmented(parser, track_info.track_id, &mut is_fragmented_file); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(is_fragmented_file, 0); + + let mut indice = Mp4parseByteData::default(); + rv = mp4parse_get_indice_table(parser, track_info.track_id, &mut indice); + assert_eq!(rv, Mp4parseStatus::Ok); + + // Compare the value from stagefright. + // Due to 'elst', the start_composition and end_composition are negative + // at first two samples. + let audio_indice_0 = Mp4parseIndice { + start_offset: 6992.into(), + end_offset: 7363.into(), + start_composition: (-36281).into(), + end_composition: (-13062).into(), + start_decode: 0.into(), + sync: true, + }; + let audio_indice_1 = Mp4parseIndice { + start_offset: 7363.into(), + end_offset: 7735.into(), + start_composition: (-13062).into(), + end_composition: 10158.into(), + start_decode: 23219.into(), + sync: true, + }; + let audio_indice_2 = Mp4parseIndice { + start_offset: 7735.into(), + end_offset: 8106.into(), + start_composition: 10158.into(), + end_composition: 33378.into(), + start_decode: 46439.into(), + sync: true, + }; + assert_eq!(indice.length, 21); + assert_eq!(*indice.indices.offset(0), audio_indice_0); + assert_eq!(*indice.indices.offset(1), audio_indice_1); + assert_eq!(*indice.indices.offset(2), audio_indice_2); + + mp4parse_free(parser); + } +} + +#[test] +fn parse_sample_table_with_negative_ctts() { + let mut file = std::fs::File::open("tests/white.mp4").expect("Unknown file"); + let io = Mp4parseIo { + read: Some(buf_read), + userdata: &mut file as *mut _ as *mut std::os::raw::c_void, + }; + + unsafe { + let mut parser = std::ptr::null_mut(); + let mut rv = mp4parse_new(&io, &mut parser); + assert_eq!(rv, Mp4parseStatus::Ok); + assert!(!parser.is_null()); + + let mut counts: u32 = 0; + rv = mp4parse_get_track_count(parser, &mut counts); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(counts, 1); + + let mut track_info = Mp4parseTrackInfo::default(); + rv = mp4parse_get_track_info(parser, 0, &mut track_info); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(track_info.track_type, Mp4parseTrackType::Video); + + let mut is_fragmented_file: u8 = std::u8::MAX; + rv = mp4parse_is_fragmented(parser, track_info.track_id, &mut is_fragmented_file); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(is_fragmented_file, 0); + + let mut indice = Mp4parseByteData::default(); + rv = mp4parse_get_indice_table(parser, track_info.track_id, &mut indice); + assert_eq!(rv, Mp4parseStatus::Ok); + + // There are negative value in 'ctts' table. + let video_indice_0 = Mp4parseIndice { + start_offset: 48.into(), + end_offset: 890.into(), + start_composition: 0.into(), + end_composition: 33_333.into(), + start_decode: 0.into(), + sync: true, + }; + let video_indice_1 = Mp4parseIndice { + start_offset: 890.into(), + end_offset: 913.into(), + start_composition: 133_333.into(), + end_composition: 166_666.into(), + start_decode: 33_333.into(), + sync: false, + }; + let video_indice_2 = Mp4parseIndice { + start_offset: 913.into(), + end_offset: 934.into(), + start_composition: 66_666.into(), + end_composition: 100_000.into(), + start_decode: 66_666.into(), + sync: false, + }; + let video_indice_3 = Mp4parseIndice { + start_offset: 934.into(), + end_offset: 955.into(), + start_composition: 33_333.into(), + end_composition: 66_666.into(), + start_decode: 100_000.into(), + sync: false, + }; + assert_eq!(indice.length, 300); + assert_eq!(*indice.indices.offset(0), video_indice_0); + assert_eq!(*indice.indices.offset(1), video_indice_1); + assert_eq!(*indice.indices.offset(2), video_indice_2); + assert_eq!(*indice.indices.offset(3), video_indice_3); + + mp4parse_free(parser); + } +} diff --git a/third_party/rust/mp4parse_capi/tests/test_workaround_stsc.rs b/third_party/rust/mp4parse_capi/tests/test_workaround_stsc.rs new file mode 100644 index 0000000000..b77da23b6d --- /dev/null +++ b/third_party/rust/mp4parse_capi/tests/test_workaround_stsc.rs @@ -0,0 +1,46 @@ +extern crate mp4parse_capi; +use mp4parse_capi::*; +use std::io::Read; + +extern "C" fn buf_read(buf: *mut u8, size: usize, userdata: *mut std::os::raw::c_void) -> isize { + let input: &mut std::fs::File = unsafe { &mut *(userdata as *mut _) }; + let mut buf = unsafe { std::slice::from_raw_parts_mut(buf, size) }; + match input.read(&mut buf) { + Ok(n) => n as isize, + Err(_) => -1, + } +} + +#[test] +fn parse_invalid_stsc_table() { + let mut file = std::fs::File::open("tests/zero_empty_stsc.mp4").expect("Unknown file"); + let io = Mp4parseIo { + read: Some(buf_read), + userdata: &mut file as *mut _ as *mut std::os::raw::c_void, + }; + + unsafe { + let mut parser = std::ptr::null_mut(); + let rv = mp4parse_new(&io, &mut parser); + + assert_eq!(rv, Mp4parseStatus::Ok); + assert!(!parser.is_null()); + + let mut counts: u32 = 0; + let rv = mp4parse_get_track_count(parser, &mut counts); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(counts, 2); + + let mut indice_video = Mp4parseByteData::default(); + let rv = mp4parse_get_indice_table(parser, 1, &mut indice_video); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(indice_video.length, 1040); + + let mut indice_audio = Mp4parseByteData::default(); + let rv = mp4parse_get_indice_table(parser, 2, &mut indice_audio); + assert_eq!(rv, Mp4parseStatus::Ok); + assert_eq!(indice_audio.length, 1952); + + mp4parse_free(parser); + } +} |