diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
commit | 9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /third_party/rust/mp4parse_capi/src | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/mp4parse_capi/src')
-rw-r--r-- | third_party/rust/mp4parse_capi/src/lib.rs | 1863 |
1 files changed, 1863 insertions, 0 deletions
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..5b362f303f --- /dev/null +++ b/third_party/rust/mp4parse_capi/src/lib.rs @@ -0,0 +1,1863 @@ +//! C API for mp4parse module. +//! +//! Parses ISO Base Media Format aka video/mp4 streams. +//! +//! # Examples +//! +//! ```rust +//! 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/. + +use byteorder::WriteBytesExt; +use std::convert::TryFrom; +use std::convert::TryInto; + +use std::io::Read; + +// Symbols we need from our rust api. +use mp4parse::serialize_opus_header; +use mp4parse::unstable::{create_sample_table, CheckedInteger, Indice}; +use mp4parse::AV1ConfigBox; +use mp4parse::AudioCodecSpecific; +use mp4parse::AvifContext; +use mp4parse::CodecType; +use mp4parse::MediaContext; +// Re-exported so consumers don't have to depend on mp4parse as well +pub use mp4parse::ParseStrictness; +use mp4parse::SampleEntry; +pub use mp4parse::Status as Mp4parseStatus; +use mp4parse::Track; +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, Eq, Debug, Default)] +pub enum Mp4parseTrackType { + #[default] + Video = 0, + Picture = 1, + AuxiliaryVideo = 2, + Audio = 3, + Metadata = 4, +} + +#[allow(non_camel_case_types, clippy::upper_case_acronyms)] +#[repr(C)] +#[derive(PartialEq, Eq, Debug, Default)] +pub enum Mp4parseCodec { + #[default] + Unknown, + Aac, + Flac, + Opus, + Avc, + Vp9, + Av1, + Mp3, + Mp4v, + Jpeg, // for QT JPEG atom in video track + Ac3, + Ec3, + Alac, + H263, + #[cfg(feature = "3gpp")] + AMRNB, + #[cfg(feature = "3gpp")] + AMRWB, +} + +#[repr(C)] +#[derive(PartialEq, Eq, Debug, Default)] +pub enum Mp4ParseEncryptionSchemeType { + #[default] + 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. +} + +#[repr(C)] +#[derive(Default, Debug)] +pub struct Mp4parseTrackInfo { + pub track_type: Mp4parseTrackType, + pub track_id: u32, + pub duration: u64, + pub media_time: CheckedInteger<i64>, + pub time_scale: u32, +} + +#[repr(C)] +#[derive(Debug)] +pub struct Mp4parseByteData { + pub length: usize, + // cheddar can't handle generic type, so it needs to be multiple data types here. + pub data: *const u8, + pub indices: *const Indice, +} + +impl Mp4parseByteData { + fn with_data(slice: &[u8]) -> Self { + Self { + length: slice.len(), + data: if !slice.is_empty() { + slice.as_ptr() + } else { + std::ptr::null() + }, + indices: std::ptr::null(), + } + } +} + +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(); + self.data = data.as_ptr(); + } + + fn set_indices(&mut self, data: &[Indice]) { + self.length = data.len(); + self.indices = data.as_ptr(); + } +} + +#[repr(C)] +#[derive(Default)] +pub struct Mp4parsePsshInfo { + pub data: Mp4parseByteData, +} + +#[repr(u8)] +#[derive(Debug, PartialEq, Eq)] +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, // in ticks + pub time_scale: 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<Indice>>, + // 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(Debug, Default)] +pub enum Mp4parseAvifLoopMode { + #[default] + NoEdits, + LoopByCount, + LoopInfinitely, +} + +#[repr(C)] +#[derive(Debug)] +pub struct Mp4parseAvifInfo { + pub premultiplied_alpha: bool, + pub major_brand: [u8; 4], + pub unsupported_features_bitfield: u32, + /// The size of the image; should never be null unless using permissive parsing + pub spatial_extents: *const mp4parse::ImageSpatialExtentsProperty, + pub nclx_colour_information: *const mp4parse::NclxColourInformation, + pub icc_colour_information: Mp4parseByteData, + pub image_rotation: mp4parse::ImageRotation, + pub image_mirror: *const mp4parse::ImageMirror, + pub pixel_aspect_ratio: *const mp4parse::PixelAspectRatio, + + /// Whether there is a `pitm` reference to the color image present. + pub has_primary_item: bool, + /// Bit depth for the item referenced by `pitm`, or 0 if values are inconsistent. + pub primary_item_bit_depth: u8, + /// Whether there is an `auxl` reference to the `pitm`-accompanying + /// alpha image present. + pub has_alpha_item: bool, + /// Bit depth for the alpha item used by the `pitm`, or 0 if values are inconsistent. + pub alpha_item_bit_depth: u8, + + /// Whether there is a sequence. Can be true with no primary image. + pub has_sequence: bool, + /// Indicates whether the EditListBox requests that the image be looped. + pub loop_mode: Mp4parseAvifLoopMode, + /// Number of times to loop the animation during playback. + /// + /// The duration of the animation specified in `elst` must be looped to fill the + /// duration of the color track. If the resulting loop count is not an integer, + /// then it will be ceiled to play past and fill the entire track's duration. + pub loop_count: u64, + /// The color track's ID, which must be valid if has_sequence is true. + pub color_track_id: u32, + pub color_track_bit_depth: u8, + /// The track ID of the alpha track, will be 0 if no alpha track is present. + pub alpha_track_id: u32, + pub alpha_track_bit_depth: u8, +} + +#[repr(C)] +#[derive(Debug)] +pub struct Mp4parseAvifImage { + pub primary_image: Mp4parseByteData, + /// If no alpha item exists, members' `.length` will be 0 and `.data` will be null + pub alpha_image: Mp4parseByteData, +} + +/// 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, strictness: ParseStrictness) -> 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, _strictness: ParseStrictness) -> mp4parse::Result<Self::Context> { + let r = mp4parse::read_mp4(io); + log::debug!("mp4parse::read_mp4 -> {:?}", r); + r + } +} + +#[derive(Default)] +pub struct Mp4parseAvifParser { + context: AvifContext, + sample_table: TryHashMap<u32, TryVec<Indice>>, +} + +impl Mp4parseAvifParser { + fn context(&self) -> &AvifContext { + &self.context + } +} + +impl ContextParser for Mp4parseAvifParser { + type Context = AvifContext; + + fn with_context(context: Self::Context) -> Self { + Self { + context, + ..Default::default() + } + } + + fn read<T: Read>(io: &mut T, strictness: ParseStrictness) -> mp4parse::Result<Self::Context> { + let r = mp4parse::read_avif(io, strictness); + if r.is_err() { + log::debug!("{:?}", r); + } + log::trace!("mp4parse::read_avif -> {:?}", r); + r + } +} + +#[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, ParseStrictness::Normal, 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, + strictness: ParseStrictness, + parser_out: *mut *mut Mp4parseAvifParser, +) -> Mp4parseStatus { + mp4parse_new_common(io, strictness, parser_out) +} + +unsafe fn mp4parse_new_common<P: ContextParser>( + io: *const Mp4parseIo, + strictness: ParseStrictness, + 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(), strictness) { + Ok(parser) => { + *parser_out = parser; + Mp4parseStatus::Ok + } + Err(status) => status, + } + } +} + +fn mp4parse_new_common_safe<T: Read, P: ContextParser>( + io: &mut T, + strictness: ParseStrictness, +) -> Result<*mut P, Mp4parseStatus> { + P::read(io, strictness) + .map(P::with_context) + .and_then(|x| TryBox::try_new(x).map_err(mp4parse::Error::from)) + .map(TryBox::into_raw) + .map_err(Mp4parseStatus::from) +} + +/// 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 +} + +/// 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::Picture => Mp4parseTrackType::Picture, + TrackType::AuxiliaryVideo => Mp4parseTrackType::AuxiliaryVideo, + TrackType::Audio => Mp4parseTrackType::Audio, + TrackType::Metadata => Mp4parseTrackType::Metadata, + TrackType::Unknown => return Mp4parseStatus::Unsupported, + }; + + let track = &context.tracks[track_index]; + + if let (Some(timescale), Some(_)) = (track.timescale, context.timescale) { + info.time_scale = timescale.0 as u32; + let media_time: CheckedInteger<u64> = track + .media_time + .map_or(0.into(), |media_time| media_time.0.into()); + + let empty_duration: CheckedInteger<u64> = track + .empty_duration + .map_or(0.into(), |empty_duration| empty_duration.0.into()); + + info.media_time = match media_time - empty_duration { + Some(difference) => difference, + None => return Mp4parseStatus::Invalid, + }; + + match track.duration { + Some(duration) => info.duration = duration.0, + None => { + // 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, + #[cfg(feature = "3gpp")] + AudioCodecSpecific::AMRSpecificBox(_) => { + if audio.codec_type == CodecType::AMRNB { + Mp4parseCodec::AMRNB + } else { + Mp4parseCodec::AMRWB + } + } + }; + 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(); + sample_info.extra_data.data = esds.codec_esds.as_ptr(); + sample_info.codec_specific_config.length = esds.decoder_specific_data.len(); + 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(); + 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(); + sample_info.codec_specific_config.data = v.as_ptr(); + } + } + } + } + AudioCodecSpecific::ALACSpecificBox(ref alac) => { + sample_info.codec_specific_config.length = alac.data.len(); + sample_info.codec_specific_config.data = alac.data.as_ptr(); + } + AudioCodecSpecific::MP3 | AudioCodecSpecific::LPCM => (), + #[cfg(feature = "3gpp")] + AudioCodecSpecific::AMRSpecificBox(_) => (), + } + + 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 = + tenc.crypt_byte_block_count.unwrap_or(0); + sample_info.protected_data.skip_byte_block = + tenc.skip_byte_block_count.unwrap_or(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::H263Config(_) => Mp4parseCodec::H263, + #[cfg(feature = "mp4v")] + VideoCodecSpecific::ESDSConfig(_) => Mp4parseCodec::Mp4v, + #[cfg(not(feature = "mp4v"))] + 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::AV1Config(ref config) => { + sample_info.extra_data.set_data(&config.raw_config); + } + 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 = + tenc.crypt_byte_block_count.unwrap_or(0); + sample_info.protected_data.skip_byte_block = + tenc.skip_byte_block_count.unwrap_or(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 struct containing meta information read by previous +/// `mp4parse_avif_new()` call. +/// +/// `color_track_id`and `alpha_track_id` will be 0 if has_sequence is false. +/// `alpha_track_id` will be 0 if no alpha aux track is present. +/// +/// # Safety +/// +/// This function is unsafe because it dereferences both the parser and +/// avif_info raw pointers passed into it. Callers should ensure the parser +/// pointer points to a valid `Mp4parseAvifParser`, and that the avif_info +/// pointer points to a valid `Mp4parseAvifInfo`. +#[no_mangle] +pub unsafe extern "C" fn mp4parse_avif_get_info( + parser: *const Mp4parseAvifParser, + avif_info: *mut Mp4parseAvifInfo, +) -> Mp4parseStatus { + if parser.is_null() || avif_info.is_null() { + return Mp4parseStatus::BadArg; + } + + if let Ok(info) = mp4parse_avif_get_info_safe((*parser).context()) { + *avif_info = info; + Mp4parseStatus::Ok + } else { + Mp4parseStatus::Invalid + } +} + +fn mp4parse_avif_get_info_safe(context: &AvifContext) -> mp4parse::Result<Mp4parseAvifInfo> { + let info = Mp4parseAvifInfo { + premultiplied_alpha: context.premultiplied_alpha, + major_brand: context.major_brand.value, + unsupported_features_bitfield: context.unsupported_features.into_bitfield(), + spatial_extents: context.spatial_extents_ptr()?, + nclx_colour_information: context + .nclx_colour_information_ptr() + .unwrap_or(Ok(std::ptr::null()))?, + icc_colour_information: Mp4parseByteData::with_data( + context.icc_colour_information().unwrap_or(Ok(&[]))?, + ), + image_rotation: context.image_rotation()?, + image_mirror: context.image_mirror_ptr()?, + pixel_aspect_ratio: context.pixel_aspect_ratio_ptr()?, + + has_primary_item: context.primary_item_is_present(), + primary_item_bit_depth: 0, + has_alpha_item: context.alpha_item_is_present(), + alpha_item_bit_depth: 0, + + has_sequence: false, + loop_mode: Mp4parseAvifLoopMode::NoEdits, + loop_count: 0, + color_track_id: 0, + color_track_bit_depth: 0, + alpha_track_id: 0, + alpha_track_bit_depth: 0, + }; + + fn get_bit_depth(data: &[u8]) -> u8 { + if !data.is_empty() && data.iter().all(|v| *v == data[0]) { + data[0] + } else { + 0 + } + } + let primary_item_bit_depth = + get_bit_depth(context.primary_item_bits_per_channel().unwrap_or(Ok(&[]))?); + let alpha_item_bit_depth = + get_bit_depth(context.alpha_item_bits_per_channel().unwrap_or(Ok(&[]))?); + + if let Some(sequence) = &context.sequence { + // Tracks must have track_id and samples + fn get_track<T>(tracks: &TryVec<Track>, pred: T) -> Option<&Track> + where + T: Fn(&Track) -> bool, + { + tracks.iter().find(|track| { + if track.track_id.is_none() { + return false; + } + match &track.stsc { + Some(stsc) => { + if stsc.samples.is_empty() { + return false; + } + if !pred(track) { + return false; + } + stsc.samples.iter().any(|chunk| chunk.samples_per_chunk > 0) + } + _ => false, + } + }) + } + + // Color track will be the first track found + let color_track = match get_track(&sequence.tracks, |_| true) { + Some(v) => v, + _ => return Ok(info), + }; + + // Alpha track will be the first track found with auxl.aux_for_track_id set to color_track's id + let alpha_track = get_track(&sequence.tracks, |track| match &track.tref { + Some(tref) => tref.has_auxl_reference(color_track.track_id.unwrap()), + _ => false, + }); + + fn get_av1c(track: &Track) -> Option<&AV1ConfigBox> { + if let Some(stsd) = &track.stsd { + for entry in &stsd.descriptions { + if let SampleEntry::Video(video_entry) = entry { + if let VideoCodecSpecific::AV1Config(av1c) = &video_entry.codec_specific { + return Some(av1c); + } + } + } + } + + None + } + + let color_track_id = color_track.track_id.unwrap(); + let color_track_bit_depth = match get_av1c(color_track) { + Some(av1c) => av1c.bit_depth, + _ => return Ok(info), + }; + + let (alpha_track_id, alpha_track_bit_depth) = match alpha_track { + Some(track) => ( + track.track_id.unwrap(), + match get_av1c(track) { + Some(av1c) => av1c.bit_depth, + _ => return Ok(info), + }, + ), + _ => (0, 0), + }; + + let (loop_mode, loop_count) = match color_track.tkhd.as_ref().map(|tkhd| tkhd.duration) { + Some(movie_duration) if movie_duration == std::u64::MAX => { + (Mp4parseAvifLoopMode::LoopInfinitely, 0) + } + Some(movie_duration) => match color_track.looped { + Some(true) => match color_track.edited_duration.map(|v| v.0) { + Some(segment_duration) => { + match movie_duration.checked_div(segment_duration).and_then(|n| { + match movie_duration.checked_rem(segment_duration) { + Some(0) => Some(n.saturating_sub(1)), + Some(_) => Some(n), + None => None, + } + }) { + Some(n) => (Mp4parseAvifLoopMode::LoopByCount, n), + None => (Mp4parseAvifLoopMode::LoopInfinitely, 0), + } + } + None => (Mp4parseAvifLoopMode::NoEdits, 0), + }, + Some(false) => (Mp4parseAvifLoopMode::LoopByCount, 0), + None => (Mp4parseAvifLoopMode::NoEdits, 0), + }, + None => (Mp4parseAvifLoopMode::LoopInfinitely, 0), + }; + + return Ok(Mp4parseAvifInfo { + primary_item_bit_depth, + alpha_item_bit_depth, + has_sequence: true, + loop_mode, + loop_count, + color_track_id, + color_track_bit_depth, + alpha_track_id, + alpha_track_bit_depth, + ..info + }); + } + + Ok(info) +} + +/// 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 `Mp4parseAvifImage`. 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_image.coded_data` is set to +/// a positive `length` and non-null `data`, then the `avif_image` contains a +/// valid alpha channel data. Otherwise, the image is opaque. +#[no_mangle] +pub unsafe extern "C" fn mp4parse_avif_get_image( + parser: *const Mp4parseAvifParser, + avif_image: *mut Mp4parseAvifImage, +) -> Mp4parseStatus { + if parser.is_null() || avif_image.is_null() { + return Mp4parseStatus::BadArg; + } + + if let Ok(image) = mp4parse_avif_get_image_safe(&*parser) { + *avif_image = image; + Mp4parseStatus::Ok + } else { + Mp4parseStatus::Invalid + } +} + +pub fn mp4parse_avif_get_image_safe( + parser: &Mp4parseAvifParser, +) -> mp4parse::Result<Mp4parseAvifImage> { + let context = parser.context(); + Ok(Mp4parseAvifImage { + primary_image: Mp4parseByteData::with_data( + context.primary_item_coded_data().unwrap_or(&[]), + ), + alpha_image: Mp4parseByteData::with_data(context.alpha_item_coded_data().unwrap_or(&[])), + }) +} + +/// 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( + &(*parser).context, + &mut (*parser).sample_table, + track_id, + &mut *indices, + ) + .into() +} + +/// Fill the supplied `Mp4parseByteData` with index information from `track`. +/// +/// # Safety +/// +/// This function is unsafe because it dereferences both the parser and +/// indices raw pointers passed to it. Callers should ensure the parser +/// points to a valid `Mp4parseAvifParser` and indices points to a valid +/// `Mp4parseByteData`. +#[no_mangle] +pub unsafe extern "C" fn mp4parse_avif_get_indice_table( + parser: *mut Mp4parseAvifParser, + track_id: u32, + indices: *mut Mp4parseByteData, + timescale: *mut u64, +) -> Mp4parseStatus { + if parser.is_null() { + return Mp4parseStatus::BadArg; + } + + if indices.is_null() { + return Mp4parseStatus::BadArg; + } + + if timescale.is_null() { + return Mp4parseStatus::BadArg; + } + + // Initialize fields to default values to ensure all fields are always valid. + *indices = Default::default(); + + if let Some(sequence) = &(*parser).context.sequence { + // Use the top level timescale, and the track timescale if present. + let mut found_timescale = false; + if let Some(context_timescale) = sequence.timescale { + *timescale = context_timescale.0; + found_timescale = true; + } + let maybe_track_timescale = match sequence + .tracks + .iter() + .find(|track| track.track_id == Some(track_id)) + { + Some(track) => track.timescale, + _ => None, + }; + if let Some(track_timescale) = maybe_track_timescale { + found_timescale = true; + *timescale = track_timescale.0; + } + if !found_timescale { + return Mp4parseStatus::Invalid; + } + return get_indice_table( + sequence, + &mut (*parser).sample_table, + track_id, + &mut *indices, + ) + .into(); + } + + Mp4parseStatus::BadArg +} + +fn get_indice_table( + context: &MediaContext, + sample_table_cache: &mut TryHashMap<u32, TryVec<Indice>>, + track_id: u32, + indices: &mut Mp4parseByteData, +) -> Result<(), Mp4parseStatus> { + 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) = sample_table_cache.get(&track_id) { + indices.set_indices(v); + return Ok(()); + } + + let media_time = match &track.media_time { + &Some(t) => i64::try_from(t.0).ok().map(Into::into), + _ => None, + }; + + let empty_duration: Option<CheckedInteger<_>> = match &track.empty_duration { + &Some(e) => i64::try_from(e.0).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); + sample_table_cache.insert(track_id, v)?; + return Ok(()); + } + + Err(Mp4parseStatus::Invalid) +} + +/// 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 = time.0; + info.time_scale = scale.0; + return Mp4parseStatus::Ok; + }; + Mp4parseStatus::Invalid +} + +/// 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(stsc), Some(stco), Some(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(); + data_len.write_u32::<byteorder::NativeEndian>(content_len)?; + 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 buf = unsafe { std::slice::from_raw_parts_mut(buf, size) }; + match input.read(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, 512); + 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, 2944); + assert_eq!(info.media_time, 1024); + + 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 parse_no_timescale() { + let mut file = std::fs::File::open("tests/no_timescale.mp4").expect("Unknown file"); + let io = Mp4parseIo { + read: Some(valid_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()); + + // The file has a video track, but the track has a timescale of 0, so. + let mut track_info = Mp4parseTrackInfo::default(); + rv = mp4parse_get_track_info(parser, 0, &mut track_info); + assert_eq!(rv, Mp4parseStatus::Invalid); + }; +} |