mp4parse-0.17.0/.cargo_vcs_info.json0000644000000001460000000000100126550ustar { "git": { "sha1": "412bc1775bb03159bf914c6a8b716a5c94625dba" }, "path_in_vcs": "mp4parse" }mp4parse-0.17.0/Cargo.toml0000644000000033040000000000100106520ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "mp4parse" version = "0.17.0" authors = [ "Ralph Giles ", "Matthew Gregan ", "Alfredo Yang ", "Jon Bauman ", "Bryce Seager van Dyk ", ] exclude = [ "*.mp4", "*.avif", "av1-avif/*", ] description = "Parser for ISO base media file format (mp4)" documentation = "https://docs.rs/mp4parse/" readme = "README.md" categories = ["multimedia::video"] license = "MPL-2.0" repository = "https://github.com/mozilla/mp4parse-rust" [lib] bench = false [[bench]] name = "avif_benchmark" harness = false [dependencies.bitreader] version = "0.3.2" [dependencies.byteorder] version = "1.2.1" [dependencies.fallible_collections] version = "0.4" features = ["std_io"] [dependencies.log] version = "0.4" [dependencies.num-traits] version = "0.2.14" [dependencies.static_assertions] version = "1.1.0" [dev-dependencies.criterion] version = "0.4" [dev-dependencies.test-assembler] version = "0.1.2" [dev-dependencies.walkdir] version = "2.3.1" [features] 3gpp = [] meta-xml = [] missing-pixi-permitted = [] mp4v = [] unstable-api = [] [badges.travis-ci] repository = "https://github.com/mozilla/mp4parse-rust" mp4parse-0.17.0/Cargo.toml.orig0000644000000023570000000000100116200ustar [package] name = "mp4parse" version = "0.17.0" authors = [ "Ralph Giles ", "Matthew Gregan ", "Alfredo Yang ", "Jon Bauman ", "Bryce Seager van Dyk ", ] description = "Parser for ISO base media file format (mp4)" documentation = "https://docs.rs/mp4parse/" license = "MPL-2.0" categories = ["multimedia::video"] edition = "2018" repository = "https://github.com/mozilla/mp4parse-rust" # Avoid complaints about trying to package test files. exclude = [ "*.mp4", "*.avif", "av1-avif/*" ] [badges] travis-ci = { repository = "https://github.com/mozilla/mp4parse-rust" } [dependencies] byteorder = "1.2.1" bitreader = { version = "0.3.2" } fallible_collections = { version = "0.4", features = ["std_io"] } num-traits = "0.2.14" log = "0.4" static_assertions = "1.1.0" [dev-dependencies] test-assembler = "0.1.2" walkdir = "2.3.1" criterion = "0.4" [features] missing-pixi-permitted = [] 3gpp = [] meta-xml = [] unstable-api = [] mp4v = [] [[bench]] name = "avif_benchmark" harness = false # See https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options [lib] bench = false mp4parse-0.17.0/Cargo.toml.orig000064400000000000000000000023571046102023000143420ustar 00000000000000[package] name = "mp4parse" version = "0.17.0" authors = [ "Ralph Giles ", "Matthew Gregan ", "Alfredo Yang ", "Jon Bauman ", "Bryce Seager van Dyk ", ] description = "Parser for ISO base media file format (mp4)" documentation = "https://docs.rs/mp4parse/" license = "MPL-2.0" categories = ["multimedia::video"] edition = "2018" repository = "https://github.com/mozilla/mp4parse-rust" # Avoid complaints about trying to package test files. exclude = [ "*.mp4", "*.avif", "av1-avif/*" ] [badges] travis-ci = { repository = "https://github.com/mozilla/mp4parse-rust" } [dependencies] byteorder = "1.2.1" bitreader = { version = "0.3.2" } fallible_collections = { version = "0.4", features = ["std_io"] } num-traits = "0.2.14" log = "0.4" static_assertions = "1.1.0" [dev-dependencies] test-assembler = "0.1.2" walkdir = "2.3.1" criterion = "0.4" [features] missing-pixi-permitted = [] 3gpp = [] meta-xml = [] unstable-api = [] mp4v = [] [[bench]] name = "avif_benchmark" harness = false # See https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options [lib] bench = false mp4parse-0.17.0/LICENSE000064400000000000000000000405261046102023000124600ustar 00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- 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/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. mp4parse-0.17.0/README.md000064400000000000000000000003111046102023000127160ustar 00000000000000`mp4parse` is a parser for ISO base media file format (mp4) written in rust. See [the README in the mp4parse-rust repo](https://github.com/mozilla/mp4parse-rust/blob/master/README.md) for more details.mp4parse-0.17.0/benches/avif_benchmark.rs000064400000000000000000000013501046102023000163570ustar 00000000000000// 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 criterion::{criterion_group, criterion_main, Criterion}; use std::fs::File; fn criterion_benchmark(c: &mut Criterion) { c.bench_function("avif_largest", |b| b.iter(avif_largest)); } criterion_group!(benches, criterion_benchmark); criterion_main!(benches); fn avif_largest() { let input = &mut File::open( "av1-avif/testFiles/Netflix/avif/cosmos_frame05000_yuv444_12bpc_bt2020_pq_qlossless.avif", ) .expect("Unknown file"); assert!(mp4parse::read_avif(input, mp4parse::ParseStrictness::Normal).is_ok()); } mp4parse-0.17.0/src/boxes.rs000064400000000000000000000251111046102023000137210ustar 00000000000000// 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 std::fmt; // 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; macro_rules! box_database { ($($(#[$attr:meta])* $boxenum:ident $boxtype:expr),*,) => { #[derive(Clone, Copy, PartialEq, Eq)] pub enum BoxType { $($(#[$attr])* $boxenum),*, UnknownBox(u32), } impl From for BoxType { fn from(t: u32) -> BoxType { use self::BoxType::*; match t { $($(#[$attr])* $boxtype => $boxenum),*, _ => UnknownBox(t), } } } impl From for u32 { fn from(b: BoxType) -> u32 { use self::BoxType::*; match b { $($(#[$attr])* $boxenum => $boxtype),*, UnknownBox(t) => t, } } } } } impl fmt::Debug for BoxType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let fourcc: FourCC = From::from(*self); fourcc.fmt(f) } } #[derive(Default, Eq, Hash, PartialEq, Clone)] pub struct FourCC { pub value: [u8; 4], } impl From for FourCC { fn from(number: u32) -> FourCC { FourCC { value: number.to_be_bytes(), } } } impl From for FourCC { fn from(t: BoxType) -> FourCC { let box_num: u32 = Into::into(t); From::from(box_num) } } impl From<[u8; 4]> for FourCC { fn from(v: [u8; 4]) -> FourCC { FourCC { value: v } } } impl fmt::Debug for FourCC { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match std::str::from_utf8(&self.value) { Ok(s) => f.write_str(s), Err(_) => self.value.fmt(f), } } } impl fmt::Display for FourCC { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(std::str::from_utf8(&self.value).unwrap_or("null")) } } impl PartialEq<&[u8; 4]> for FourCC { fn eq(&self, other: &&[u8; 4]) -> bool { self.value.eq(*other) } } box_database!( FileTypeBox 0x6674_7970, // "ftyp" MediaDataBox 0x6d64_6174, // "mdat" PrimaryItemBox 0x7069_746d, // "pitm" ItemDataBox 0x6964_6174, // "idat" ItemInfoBox 0x6969_6e66, // "iinf" ItemInfoEntry 0x696e_6665, // "infe" ItemLocationBox 0x696c_6f63, // "iloc" MovieBox 0x6d6f_6f76, // "moov" MovieHeaderBox 0x6d76_6864, // "mvhd" TrackBox 0x7472_616b, // "trak" TrackHeaderBox 0x746b_6864, // "tkhd" TrackReferenceBox 0x7472_6566, // "tref" AuxiliaryBox 0x6175_786C, // "auxl" EditBox 0x6564_7473, // "edts" MediaBox 0x6d64_6961, // "mdia" EditListBox 0x656c_7374, // "elst" MediaHeaderBox 0x6d64_6864, // "mdhd" HandlerBox 0x6864_6c72, // "hdlr" MediaInformationBox 0x6d69_6e66, // "minf" ItemReferenceBox 0x6972_6566, // "iref" ItemPropertiesBox 0x6970_7270, // "iprp" ItemPropertyContainerBox 0x6970_636f, // "ipco" ItemPropertyAssociationBox 0x6970_6d61, // "ipma" ColourInformationBox 0x636f_6c72, // "colr" ImageSpatialExtentsProperty 0x6973_7065, // "ispe" PixelAspectRatioBox 0x7061_7370, // "pasp" PixelInformationBox 0x7069_7869, // "pixi" AuxiliaryTypeProperty 0x6175_7843, // "auxC" CleanApertureBox 0x636c_6170, // "clap" ImageRotation 0x6972_6f74, // "irot" ImageMirror 0x696d_6972, // "imir" OperatingPointSelectorProperty 0x6131_6f70, // "a1op" AV1LayeredImageIndexingProperty 0x6131_6c78, // "a1lx" LayerSelectorProperty 0x6c73_656c, // "lsel" SampleTableBox 0x7374_626c, // "stbl" SampleDescriptionBox 0x7374_7364, // "stsd" TimeToSampleBox 0x7374_7473, // "stts" SampleToChunkBox 0x7374_7363, // "stsc" SampleSizeBox 0x7374_737a, // "stsz" ChunkOffsetBox 0x7374_636f, // "stco" ChunkLargeOffsetBox 0x636f_3634, // "co64" SyncSampleBox 0x7374_7373, // "stss" AVCSampleEntry 0x6176_6331, // "avc1" AVC3SampleEntry 0x6176_6333, // "avc3" - Need to check official name in spec. AVCConfigurationBox 0x6176_6343, // "avcC" H263SampleEntry 0x7332_3633, // "s263" H263SpecificBox 0x6432_3633, // "d263" MP4AudioSampleEntry 0x6d70_3461, // "mp4a" MP4VideoSampleEntry 0x6d70_3476, // "mp4v" #[cfg(feature = "3gpp")] AMRNBSampleEntry 0x7361_6d72, // "samr" - AMR narrow-band #[cfg(feature = "3gpp")] AMRWBSampleEntry 0x7361_7762, // "sawb" - AMR wide-band #[cfg(feature = "3gpp")] AMRSpecificBox 0x6461_6d72, // "damr" ESDBox 0x6573_6473, // "esds" VP8SampleEntry 0x7670_3038, // "vp08" VP9SampleEntry 0x7670_3039, // "vp09" VPCodecConfigurationBox 0x7670_6343, // "vpcC" AV1SampleEntry 0x6176_3031, // "av01" AV1CodecConfigurationBox 0x6176_3143, // "av1C" FLACSampleEntry 0x664c_6143, // "fLaC" FLACSpecificBox 0x6466_4c61, // "dfLa" OpusSampleEntry 0x4f70_7573, // "Opus" OpusSpecificBox 0x644f_7073, // "dOps" ProtectedVisualSampleEntry 0x656e_6376, // "encv" - Need to check official name in spec. ProtectedAudioSampleEntry 0x656e_6361, // "enca" - Need to check official name in spec. MovieExtendsBox 0x6d76_6578, // "mvex" MovieExtendsHeaderBox 0x6d65_6864, // "mehd" QTWaveAtom 0x7761_7665, // "wave" - quicktime atom ProtectionSystemSpecificHeaderBox 0x7073_7368, // "pssh" SchemeInformationBox 0x7363_6869, // "schi" TrackEncryptionBox 0x7465_6e63, // "tenc" ProtectionSchemeInfoBox 0x7369_6e66, // "sinf" OriginalFormatBox 0x6672_6d61, // "frma" SchemeTypeBox 0x7363_686d, // "schm" MP3AudioSampleEntry 0x2e6d_7033, // ".mp3" - from F4V. CompositionOffsetBox 0x6374_7473, // "ctts" LPCMAudioSampleEntry 0x6c70_636d, // "lpcm" - quicktime atom ALACSpecificBox 0x616c_6163, // "alac" - Also used by ALACSampleEntry UuidBox 0x7575_6964, // "uuid" MetadataBox 0x6d65_7461, // "meta" MetadataHeaderBox 0x6d68_6472, // "mhdr" MetadataItemKeysBox 0x6b65_7973, // "keys" MetadataItemListEntry 0x696c_7374, // "ilst" MetadataItemDataEntry 0x6461_7461, // "data" MetadataItemNameBox 0x6e61_6d65, // "name" #[cfg(feature = "meta-xml")] MetadataXMLBox 0x786d_6c20, // "xml " #[cfg(feature = "meta-xml")] MetadataBXMLBox 0x6278_6d6c, // "bxml" UserdataBox 0x7564_7461, // "udta" AlbumEntry 0xa961_6c62, // "©alb" ArtistEntry 0xa941_5254, // "©ART" ArtistLowercaseEntry 0xa961_7274, // "©art" AlbumArtistEntry 0x6141_5254, // "aART" CommentEntry 0xa963_6d74, // "©cmt" DateEntry 0xa964_6179, // "©day" TitleEntry 0xa96e_616d, // "©nam" CustomGenreEntry 0xa967_656e, // "©gen" StandardGenreEntry 0x676e_7265, // "gnre" TrackNumberEntry 0x7472_6b6e, // "trkn" DiskNumberEntry 0x6469_736b, // "disk" ComposerEntry 0xa977_7274, // "©wrt" EncoderEntry 0xa974_6f6f, // "©too" EncodedByEntry 0xa965_6e63, // "©enc" TempoEntry 0x746d_706f, // "tmpo" CopyrightEntry 0x6370_7274, // "cprt" CompilationEntry 0x6370_696c, // "cpil" CoverArtEntry 0x636f_7672, // "covr" AdvisoryEntry 0x7274_6e67, // "rtng" RatingEntry 0x7261_7465, // "rate" GroupingEntry 0xa967_7270, // "©grp" MediaTypeEntry 0x7374_696b, // "stik" PodcastEntry 0x7063_7374, // "pcst" CategoryEntry 0x6361_7467, // "catg" KeywordEntry 0x6b65_7977, // "keyw" PodcastUrlEntry 0x7075_726c, // "purl" PodcastGuidEntry 0x6567_6964, // "egid" DescriptionEntry 0x6465_7363, // "desc" LongDescriptionEntry 0x6c64_6573, // "ldes" LyricsEntry 0xa96c_7972, // "©lyr" TVNetworkNameEntry 0x7476_6e6e, // "tvnn" TVShowNameEntry 0x7476_7368, // "tvsh" TVEpisodeNameEntry 0x7476_656e, // "tven" TVSeasonNumberEntry 0x7476_736e, // "tvsn" TVEpisodeNumberEntry 0x7476_6573, // "tves" PurchaseDateEntry 0x7075_7264, // "purd" GaplessPlaybackEntry 0x7067_6170, // "pgap" OwnerEntry 0x6f77_6e72, // "ownr" HDVideoEntry 0x6864_7664, // "hdvd" SortNameEntry 0x736f_6e6d, // "sonm" SortAlbumEntry 0x736f_616c, // "soal" SortArtistEntry 0x736f_6172, // "soar" SortAlbumArtistEntry 0x736f_6161, // "soaa" SortComposerEntry 0x736f_636f, // "soco" ); mp4parse-0.17.0/src/lib.rs000064400000000000000000006511451046102023000133630ustar 00000000000000//! Module for parsing ISO Base Media Format aka video/mp4 streams. // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // `clippy::upper_case_acronyms` is a nightly-only lint as of 2021-03-15, so we // allow `clippy::unknown_clippy_lints` to ignore it on stable - but // `clippy::unknown_clippy_lints` has been renamed in nightly, so we need to // allow `renamed_and_removed_lints` to ignore a warning for that. #![allow(renamed_and_removed_lints)] #![allow(clippy::unknown_clippy_lints)] #![allow(clippy::upper_case_acronyms)] #[macro_use] extern crate log; extern crate bitreader; extern crate byteorder; extern crate fallible_collections; extern crate num_traits; use bitreader::{BitReader, ReadInto}; use byteorder::{ReadBytesExt, WriteBytesExt}; use fallible_collections::TryRead; use fallible_collections::TryReserveError; use num_traits::Num; use std::convert::{TryFrom, TryInto as _}; use std::fmt; use std::io::Cursor; use std::io::{Read, Take}; #[macro_use] mod macros; mod boxes; use crate::boxes::{BoxType, FourCC}; // Unit tests. #[cfg(test)] mod tests; #[cfg(feature = "unstable-api")] pub mod unstable; /// The HEIF image and image collection brand /// The 'mif1' brand indicates structural requirements on files /// See HEIF (ISO 23008-12:2017) § 10.2.1 pub const MIF1_BRAND: FourCC = FourCC { value: *b"mif1" }; /// The HEIF image sequence brand /// The 'msf1' brand indicates structural requirements on files /// See HEIF (ISO 23008-12:2017) § 10.3.1 pub const MSF1_BRAND: FourCC = FourCC { value: *b"msf1" }; /// The brand to identify AV1 image items /// The 'avif' brand indicates structural requirements on files /// See pub const AVIF_BRAND: FourCC = FourCC { value: *b"avif" }; /// The brand to identify AVIF image sequences /// The 'avis' brand indicates structural requirements on files /// See pub const AVIS_BRAND: FourCC = FourCC { value: *b"avis" }; /// A trait to indicate a type can be infallibly converted to `u64`. /// This should only be implemented for infallible conversions, so only unsigned types are valid. trait ToU64 { fn to_u64(self) -> u64; } /// Statically verify that the platform `usize` can fit within a `u64`. /// If the size won't fit on the given platform, this will fail at compile time, but if a type /// which can fail `TryInto` is used, it may panic. impl ToU64 for usize { fn to_u64(self) -> u64 { static_assertions::const_assert!( std::mem::size_of::() <= std::mem::size_of::() ); self.try_into().expect("usize -> u64 conversion failed") } } /// A trait to indicate a type can be infallibly converted to `usize`. /// This should only be implemented for infallible conversions, so only unsigned types are valid. pub trait ToUsize { fn to_usize(self) -> usize; } /// Statically verify that the given type can fit within a `usize`. /// If the size won't fit on the given platform, this will fail at compile time, but if a type /// which can fail `TryInto` is used, it may panic. macro_rules! impl_to_usize_from { ( $from_type:ty ) => { impl ToUsize for $from_type { fn to_usize(self) -> usize { static_assertions::const_assert!( std::mem::size_of::<$from_type>() <= std::mem::size_of::() ); self.try_into().expect(concat!( stringify!($from_type), " -> usize conversion failed" )) } } }; } impl_to_usize_from!(u8); impl_to_usize_from!(u16); impl_to_usize_from!(u32); /// Indicate the current offset (i.e., bytes already read) in a reader trait Offset { fn offset(&self) -> u64; } /// Wraps a reader to track the current offset struct OffsetReader<'a, T: 'a> { reader: &'a mut T, offset: u64, } impl<'a, T> OffsetReader<'a, T> { fn new(reader: &'a mut T) -> Self { Self { reader, offset: 0 } } } impl<'a, T> Offset for OffsetReader<'a, T> { fn offset(&self) -> u64 { self.offset } } impl<'a, T: Read> Read for OffsetReader<'a, T> { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let bytes_read = self.reader.read(buf)?; trace!("Read {} bytes at offset {}", bytes_read, self.offset); self.offset = self .offset .checked_add(bytes_read.to_u64()) .expect("total bytes read too large for offset type"); Ok(bytes_read) } } pub type TryVec = fallible_collections::TryVec; pub type TryString = fallible_collections::TryVec; pub type TryHashMap = fallible_collections::TryHashMap; pub type TryBox = fallible_collections::TryBox; // To ensure we don't use stdlib allocating types by accident #[allow(dead_code)] struct Vec; #[allow(dead_code)] struct Box; #[allow(dead_code)] struct HashMap; #[allow(dead_code)] struct String; /// The return value to the C API /// Any detail that needs to be communicated to the caller must be encoded here /// since the [`Error`] type's associated data is part of the FFI. #[repr(C)] #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Status { Ok = 0, BadArg = 1, Invalid = 2, Unsupported = 3, Eof = 4, Io = 5, Oom = 6, A1lxEssential, A1opNoEssential, AlacBadMagicCookieSize, AlacFlagsNonzero, Av1cMissing, BitReaderError, BoxBadSize, BoxBadWideSize, CheckParserStateErr, ColrBadQuantity, ColrBadSize, ColrBadType, ColrReservedNonzero, ConstructionMethod, CttsBadSize, CttsBadVersion, DflaBadMetadataBlockSize, DflaFlagsNonzero, DflaMissingMetadata, DflaStreamInfoBadSize, DflaStreamInfoNotFirst, DopsChannelMappingWriteErr, DopsOpusHeadWriteErr, ElstBadVersion, EsdsBadAudioSampleEntry, EsdsBadDescriptor, EsdsDecSpecificIntoTagQuantity, FtypBadSize, FtypNotFirst, HdlrNameNoNul, HdlrNameNotUtf8, HdlrNotFirst, HdlrPredefinedNonzero, HdlrReservedNonzero, HdlrTypeNotPict, HdlrUnsupportedVersion, HdrlBadQuantity, IdatBadQuantity, IdatMissing, IinfBadChild, IinfBadQuantity, IlocBadConstructionMethod, IlocBadExtent, IlocBadExtentCount, IlocBadFieldSize, IlocBadQuantity, IlocBadSize, IlocDuplicateItemId, IlocNotFound, IlocOffsetOverflow, ImageItemType, InfeFlagsNonzero, InvalidUtf8, IpcoIndexOverflow, IpmaBadIndex, IpmaBadItemOrder, IpmaBadQuantity, IpmaBadVersion, IpmaDuplicateItemId, IpmaFlagsNonzero, IpmaIndexZeroNoEssential, IpmaTooBig, IpmaTooSmall, IprpBadChild, IprpBadQuantity, IprpConflict, IrefBadQuantity, IrefRecursion, IspeMissing, ItemTypeMissing, LselNoEssential, MdhdBadTimescale, MdhdBadVersion, MehdBadVersion, MetaBadQuantity, MissingAvifOrAvisBrand, MissingMif1Brand, MoovBadQuantity, MoovMissing, MultipleAlpha, MvhdBadTimescale, MvhdBadVersion, NoImage, PitmBadQuantity, PitmMissing, PitmNotFound, PixiBadChannelCount, PixiMissing, PsshSizeOverflow, ReadBufErr, SchiQuantity, StsdBadAudioSampleEntry, StsdBadVideoSampleEntry, TkhdBadVersion, TxformBeforeIspe, TxformNoEssential, TxformOrder, } #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Feature { A1lx, A1op, Auxc, Av1c, Avis, Clap, Colr, Grid, Imir, Ipro, Irot, Ispe, Lsel, Pasp, Pixi, } impl Feature { fn supported(self) -> bool { match self { Self::Auxc | Self::Av1c | Self::Avis | Self::Colr | Self::Imir | Self::Irot | Self::Ispe | Self::Pasp | Self::Pixi => true, Self::A1lx | Self::A1op | Self::Clap | Self::Grid | Self::Ipro | Self::Lsel => false, } } } impl TryFrom<&ItemProperty> for Feature { type Error = Error; fn try_from(item_property: &ItemProperty) -> Result { Ok(match item_property { ItemProperty::AuxiliaryType(_) => Self::Auxc, ItemProperty::AV1Config(_) => Self::Av1c, ItemProperty::Channels(_) => Self::Pixi, ItemProperty::CleanAperture => Self::Clap, ItemProperty::Colour(_) => Self::Colr, ItemProperty::ImageSpatialExtents(_) => Self::Ispe, ItemProperty::LayeredImageIndexing => Self::A1lx, ItemProperty::LayerSelection => Self::Lsel, ItemProperty::Mirroring(_) => Self::Imir, ItemProperty::OperatingPointSelector => Self::A1op, ItemProperty::PixelAspectRatio(_) => Self::Pasp, ItemProperty::Rotation(_) => Self::Irot, item_property => { error!("No known Feature variant for {:?}", item_property); return Err(Error::Unsupported("missing Feature fox ItemProperty")); } }) } } /// A collection to indicate unsupported features that were encountered during /// parsing. Since the default behavior for many such features is to ignore /// them, this often not fatal and there may be several to report. #[derive(Debug, Default)] pub struct UnsupportedFeatures(u32); impl UnsupportedFeatures { pub fn new() -> Self { Self(0x0) } pub fn into_bitfield(&self) -> u32 { self.0 } fn feature_to_bitfield(feature: Feature) -> u32 { let index = feature as usize; assert!( u8::BITS.to_usize() * std::mem::size_of::() > index, "You're gonna need a bigger bitfield" ); let bitfield = 1u32 << index; assert_eq!(bitfield.count_ones(), 1); bitfield } pub fn insert(&mut self, feature: Feature) { warn!("Unsupported feature: {:?}", feature); self.0 |= Self::feature_to_bitfield(feature); } pub fn contains(&self, feature: Feature) -> bool { self.0 & Self::feature_to_bitfield(feature) != 0x0 } pub fn is_empty(&self) -> bool { self.0 == 0x0 } } impl From for Result { /// A convenience method to enable shortcuts like /// ``` /// # use mp4parse::{Result,Status}; /// # let _: Result<()> = /// Status::MissingAvifOrAvisBrand.into(); /// ``` /// instead of /// ``` /// # use mp4parse::{Error,Result,Status}; /// # let _: Result<()> = /// Err(Error::from(Status::MissingAvifOrAvisBrand)); /// ``` /// Note that `Status::Ok` can't be supported this way and will panic. fn from(parse_status: Status) -> Self { match parse_status { Status::Ok => panic!("Can't determine Ok(_) inner value from Status"), err_status => Err(err_status.into()), } } } /// For convenience of creating an error for an unsupported feature which we /// want to communicate the specific feature back to the C API caller impl From for Error { fn from(parse_status: Status) -> Self { match parse_status { Status::Ok | Status::BadArg | Status::Invalid | Status::Unsupported | Status::Eof | Status::Io | Status::Oom => { panic!("Status -> Error is only for Status:InvalidData errors") } _ => Self::InvalidData(parse_status), } } } impl From for &str { fn from(status: Status) -> Self { match status { Status::Ok | Status::BadArg | Status::Invalid | Status::Unsupported | Status::Eof | Status::Io | Status::Oom => { panic!("Status -> Error is only for specific parsing errors") } Status::A1lxEssential => { "AV1LayeredImageIndexingProperty (a1lx) shall not be marked as essential \ per https://aomediacodec.github.io/av1-avif/#layered-image-indexing-property-description" } Status::A1opNoEssential => { "OperatingPointSelectorProperty (a1op) shall be marked as essential \ per https://aomediacodec.github.io/av1-avif/#operating-point-selector-property-description" } Status::AlacBadMagicCookieSize => { "ALACSpecificBox magic cookie is the wrong size" } Status::AlacFlagsNonzero => { "no-zero alac (ALAC) flags" } Status::Av1cMissing => { "One AV1 Item Configuration Property (av1C) is mandatory for an \ image item of type 'av01' \ per AVIF specification § 2.2.1" } Status::BitReaderError => { "Bitwise read failed" } Status::BoxBadSize => { "malformed size" } Status::BoxBadWideSize => { "malformed wide size" } Status::CheckParserStateErr => { "unread box content or bad parser sync" } Status::ColrBadQuantity => { "Each item shall have at most one property association with a ColourInformationBox (colr) for a given value of colour_type \ per HEIF (ISO/IEC DIS 23008-12) § 6.5.5.1" } Status::ColrBadSize => { "Unexpected size for colr box" } Status::ColrBadType => { "Unsupported colour_type for ColourInformationBox" } Status::ColrReservedNonzero => { "The 7 reserved bits at the end of the ColourInformationBox \ for colour_type == 'nclx' must be 0 \ per ISOBMFF (ISO 14496-12:2020) § 12.1.5.2" } Status::ConstructionMethod => { "construction_method shall be 0 (file) or 1 (idat) per MIAF (ISO 23000-22:2019) § 7.2.1.7" } Status::CttsBadSize => { "insufficient data in 'ctts' box" } Status::CttsBadVersion => { "unsupported version in 'ctts' box" } Status::DflaBadMetadataBlockSize => { "FLACMetadataBlock larger than parent box" } Status::DflaFlagsNonzero => { "no-zero dfLa (FLAC) flags" } Status::DflaMissingMetadata => { "FLACSpecificBox missing metadata" } Status::DflaStreamInfoBadSize => { "FLACSpecificBox STREAMINFO block is the wrong size" } Status::DflaStreamInfoNotFirst => { "FLACSpecificBox must have STREAMINFO metadata first" } Status::DopsChannelMappingWriteErr => { "Couldn't write channel mapping table data." } Status::DopsOpusHeadWriteErr => { "Couldn't write OpusHead tag." } Status::ElstBadVersion => { "unhandled elst version" } Status::EsdsBadAudioSampleEntry => { "malformed audio sample entry" } Status::EsdsBadDescriptor => { "Invalid descriptor." } Status::EsdsDecSpecificIntoTagQuantity => { "There can be only one DecSpecificInfoTag descriptor" } Status::FtypBadSize => { "invalid ftyp size" } Status::FtypNotFirst => { "The FileTypeBox shall be placed as early as possible in the file \ per ISOBMFF (ISO 14496-12:2020) § 4.3.1" } Status::HdlrNameNoNul => { "The HandlerBox 'name' field shall be null-terminated \ per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2" } Status::HdlrNameNotUtf8 => { "The HandlerBox 'name' field shall be valid utf8 \ per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2" } Status::HdlrNotFirst => { "The HandlerBox shall be the first contained box within the MetaBox \ per MIAF (ISO 23000-22:2019) § 7.2.1.5" } Status::HdlrPredefinedNonzero => { "The HandlerBox 'pre_defined' field shall be 0 \ per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2" } Status::HdlrReservedNonzero => { "The HandlerBox 'reserved' fields shall be 0 \ per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2" } Status::HdlrTypeNotPict => { "The HandlerBox handler_type must be 'pict' \ per MIAF (ISO 23000-22:2019) § 7.2.1.5" } Status::HdlrUnsupportedVersion => { "The HandlerBox version shall be 0 (zero) \ per ISOBMFF (ISO 14496-12:2020) § 8.4.3.2" } Status::HdrlBadQuantity => { "There shall be exactly one hdlr box \ per ISOBMFF (ISO 14496-12:2020) § 8.4.3.1" } Status::IdatBadQuantity => { "There shall be zero or one idat boxes \ per ISOBMFF (ISO 14496-12:2020) § 8.11.11" } Status::IdatMissing => { "ItemLocationBox (iloc) construction_method indicates 1 (idat), \ but no idat box is present." } Status::IinfBadChild => { "iinf box shall contain only infe boxes \ per ISOBMFF (ISO 14496-12:2020) § 8.11.6.2" } Status::IinfBadQuantity => { "There shall be zero or one iinf boxes \ per ISOBMFF (ISO 14496-12:2020) § 8.11.6.1" } Status::IlocBadConstructionMethod => { "construction_method is taken from the set 0, 1 or 2 \ per ISOBMFF (ISO 14496-12:2020) § 8.11.3.3" } Status::IlocBadExtent => { "extent_count != 1 requires explicit offset and length \ per ISOBMFF (ISO 14496-12:2020) § 8.11.3.3" } Status::IlocBadExtentCount => { "extent_count must have a value 1 or greater \ per ISOBMFF (ISO 14496-12:2020) § 8.11.3.3" } Status::IlocBadFieldSize => { "value must be in the set {0, 4, 8}" } Status::IlocBadQuantity => { "There shall be zero or one iloc boxes \ per ISOBMFF (ISO 14496-12:2020) § 8.11.3.1" } Status::IlocBadSize => { "invalid iloc size" } Status::IlocDuplicateItemId => { "duplicate item_ID in iloc" } Status::IlocNotFound => { "ItemLocationBox (iloc) contains an extent not present in any mdat or idat box" } Status::IlocOffsetOverflow => { "offset calculation overflow" } Status::ImageItemType => { "Image item type is neither 'av01' nor 'grid'" } Status::InfeFlagsNonzero => { "'infe' flags field shall be 0 \ per ISOBMFF (ISO 14496-12:2020) § 8.11.6.2" } Status::InvalidUtf8 => { "invalid utf8" } Status::IpcoIndexOverflow => { "ipco index overflow" } Status::IpmaBadIndex => { "Invalid property index in ipma" } Status::IpmaBadItemOrder => { "Each ItemPropertyAssociation box shall be ordered by increasing item_ID" } Status::IpmaBadQuantity => { "There shall be at most one ItemPropertyAssociationbox with a given pair of \ values of version and flags \ per ISOBMFF (ISO 14496-12:2020) § 8.11.14.1" } Status::IpmaBadVersion => { "The ipma version 0 should be used unless 32-bit item_ID values are needed \ per ISOBMFF (ISO 14496-12:2020) § 8.11.14.1" } Status::IpmaDuplicateItemId => { "There shall be at most one occurrence of a given item_ID, \ in the set of ItemPropertyAssociationBox boxes \ per ISOBMFF (ISO 14496-12:2020) § 8.11.14.1" } Status::IpmaFlagsNonzero => { "Unless there are more than 127 properties in the ItemPropertyContainerBox, \ flags should be equal to 0 \ per ISOBMFF (ISO 14496-12:2020) § 8.11.14.1" } Status::IpmaIndexZeroNoEssential => { "the essential indicator shall be 0 for property index 0 \ per ISOBMFF (ISO 14496-12:2020) § 8.11.14.3" } Status::IpmaTooBig => { "ipma box exceeds maximum size for entry_count" } Status::IpmaTooSmall => { "ipma box below minimum size for entry_count" } Status::IprpBadChild => { "unexpected iprp child" } Status::IprpBadQuantity => { "There shall be zero or one iprp boxes \ per ISOBMFF (ISO 14496-12:2020) § 8.11.14.1" } Status::IprpConflict => { "conflicting item property values" } Status::IrefBadQuantity => { "There shall be zero or one iref boxes \ per ISOBMFF (ISO 14496-12:2020) § 8.11.12.1" } Status::IrefRecursion => { "from_item_id and to_item_id must be different" } Status::IspeMissing => { "Missing 'ispe' property for image item, required \ per HEIF (ISO/IEC 23008-12:2017) § 6.5.3.1" } Status::ItemTypeMissing => { "No ItemInfoEntry for item_ID" } Status::LselNoEssential => { "LayerSelectorProperty (lsel) shall be marked as essential \ per HEIF (ISO/IEC 23008-12:2017) § 6.5.11.1" } Status::MdhdBadTimescale => { "zero timescale in mdhd" } Status::MdhdBadVersion => { "unhandled mdhd version" } Status::MehdBadVersion => { "unhandled mehd version" } Status::MetaBadQuantity => { "There should be zero or one meta boxes \ per ISOBMFF (ISO 14496-12:2020) § 8.11.1.1" } Status::MissingAvifOrAvisBrand => { "The file shall list 'avif' or 'avis' in the compatible_brands field of the FileTypeBox \ per https://aomediacodec.github.io/av1-avif/#file-constraints" } Status::MissingMif1Brand => { "The FileTypeBox should contain 'mif1' in the compatible_brands list \ per MIAF (ISO 23000-22:2019/Amd. 2:2021) § 7.2.1.2" } Status::MoovBadQuantity => { "Multiple moov boxes found; \ files with avis or msf1 brands shall contain exactly one moov box \ per ISOBMFF (ISO 14496-12:2020) § 8.2.1.1" } Status::MoovMissing => { "No moov box found; \ files with avis or msf1 brands shall contain exactly one moov box \ per ISOBMFF (ISO 14496-12:2020) § 8.2.1.1" } Status::MultipleAlpha => { "multiple alpha planes" } Status::MvhdBadTimescale => { "zero timescale in mvhd" } Status::MvhdBadVersion => { "unhandled mvhd version" } Status::NoImage => "No primary image or image sequence found", Status::PitmBadQuantity => { "There shall be zero or one pitm boxes \ per ISOBMFF (ISO 14496-12:2020) § 8.11.4.1" } Status::PitmMissing => { "Missing required PrimaryItemBox (pitm), required \ per HEIF (ISO/IEC 23008-12:2017) § 10.2.1" } Status::PitmNotFound => { "PrimaryItemBox (pitm) referenced an item ID that was not present" } Status::PixiBadChannelCount => { "invalid num_channels" } Status::PixiMissing => { "The pixel information property shall be associated with every image \ that is displayable (not hidden) \ per MIAF (ISO/IEC 23000-22:2019) specification § 7.3.6.6" } Status::PsshSizeOverflow => { "overflow in read_pssh" } Status::ReadBufErr => { "failed buffer read" } Status::SchiQuantity => { "tenc box should be only one at most in sinf box" } Status::StsdBadAudioSampleEntry => { "malformed audio sample entry" } Status::StsdBadVideoSampleEntry => { "malformed video sample entry" } Status::TkhdBadVersion => { "unhandled tkhd version" } Status::TxformBeforeIspe => { "Every image item shall be associated with one property of \ type ImageSpatialExtentsProperty (ispe), prior to the \ association of all transformative properties. \ per HEIF (ISO/IEC 23008-12:2017) § 6.5.3.1" } Status::TxformNoEssential => { "All transformative properties associated with coded and \ derived images required or conditionally required by this \ document shall be marked as essential \ per MIAF (ISO 23000-22:2019) § 7.3.9" } Status::TxformOrder => { "These properties, if used, shall be indicated to be applied \ in the following order: clean aperture first, then rotation, \ then mirror. \ per MIAF (ISO/IEC 23000-22:2019) § 7.3.6.7" } } } } impl From for Status { fn from(error: Error) -> Self { match error { Error::Unsupported(_) => Self::Unsupported, Error::InvalidData(parse_status) => parse_status, Error::UnexpectedEOF => Self::Eof, Error::Io(_) => { // Getting std::io::ErrorKind::UnexpectedEof is normal // but our From trait implementation should have converted // those to our Error::UnexpectedEOF variant. Self::Io } Error::MoovMissing => Self::MoovMissing, Error::OutOfMemory => Self::Oom, } } } impl From> for Status { fn from(result: Result<(), Status>) -> Self { match result { Ok(()) => Status::Ok, Err(Status::Ok) => unreachable!(), Err(e) => e, } } } impl From> for Status { fn from(result: Result) -> Self { match result { Ok(_) => Status::Ok, Err(e) => Status::from(e), } } } impl From for Status { fn from(_: fallible_collections::TryReserveError) -> Self { Status::Oom } } impl From for Status { fn from(_: std::io::Error) -> Self { Status::Io } } /// Describes parser failures. /// /// This enum wraps the standard `io::Error` type, unified with /// our own parser error states and those of crates we use. #[derive(Debug)] pub enum Error { /// Parse error caused by corrupt or malformed data. /// See the helper [`From for Error`](enum.Error.html#impl-From) InvalidData(Status), /// Parse error caused by limited parser support rather than invalid data. Unsupported(&'static str), /// Reflect `std::io::ErrorKind::UnexpectedEof` for short data. UnexpectedEOF, /// Propagate underlying errors from `std::io`. Io(std::io::Error), /// read_mp4 terminated without detecting a moov box. MoovMissing, /// Out of memory OutOfMemory, } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{self:?}") } } impl std::error::Error for Error {} impl From for Error { fn from(_: bitreader::BitReaderError) -> Error { Status::BitReaderError.into() } } impl From for Error { fn from(err: std::io::Error) -> Error { match err.kind() { std::io::ErrorKind::UnexpectedEof => Error::UnexpectedEOF, _ => Error::Io(err), } } } impl From for Error { fn from(_: std::string::FromUtf8Error) -> Error { Status::InvalidUtf8.into() } } impl From for Error { fn from(_: std::str::Utf8Error) -> Error { Status::InvalidUtf8.into() } } impl From for Error { fn from(_: std::num::TryFromIntError) -> Error { Error::Unsupported("integer conversion failed") } } impl From for std::io::Error { fn from(err: Error) -> Self { let kind = match err { Error::UnexpectedEOF => std::io::ErrorKind::UnexpectedEof, Error::Io(io_err) => return io_err, _ => std::io::ErrorKind::Other, }; Self::new(kind, err) } } impl From for Error { fn from(_: TryReserveError) -> Error { Error::OutOfMemory } } /// Result shorthand using our Error enum. pub type Result = std::result::Result; /// Basic ISO box structure. /// /// mp4 files are a sequence of possibly-nested 'box' structures. Each box /// begins with a header describing the length of the box's data and a /// four-byte box type which identifies the type of the box. Together these /// are enough to interpret the contents of that section of the file. /// /// See ISOBMFF (ISO 14496-12:2020) § 4.2 #[derive(Debug, Clone, Copy)] struct BoxHeader { /// Box type. name: BoxType, /// Size of the box in bytes. size: u64, /// Offset to the start of the contained data (or header size). offset: u64, /// Uuid for extended type. #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 uuid: Option<[u8; 16]>, } impl BoxHeader { const MIN_SIZE: u64 = 8; // 4-byte size + 4-byte type const MIN_LARGE_SIZE: u64 = 16; // 4-byte size + 4-byte type + 16-byte size } /// File type box 'ftyp'. #[derive(Debug)] struct FileTypeBox { #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 major_brand: FourCC, #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 minor_version: u32, compatible_brands: TryVec, } impl FileTypeBox { fn contains(&self, brand: &FourCC) -> bool { self.compatible_brands.contains(brand) || self.major_brand == *brand } } /// Movie header box 'mvhd'. #[derive(Debug)] struct MovieHeaderBox { pub timescale: u32, #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 duration: u64, } #[derive(Debug, Clone, Copy)] pub struct Matrix { pub a: i32, // 16.16 fix point pub b: i32, // 16.16 fix point pub u: i32, // 2.30 fix point pub c: i32, // 16.16 fix point pub d: i32, // 16.16 fix point pub v: i32, // 2.30 fix point pub x: i32, // 16.16 fix point pub y: i32, // 16.16 fix point pub w: i32, // 2.30 fix point } /// Track header box 'tkhd' #[derive(Debug, Clone)] pub struct TrackHeaderBox { track_id: u32, pub disabled: bool, pub duration: u64, pub width: u32, pub height: u32, pub matrix: Matrix, } /// Edit list box 'elst' #[derive(Debug)] struct EditListBox { looped: bool, edits: TryVec, } #[derive(Debug)] struct Edit { segment_duration: u64, media_time: i64, #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 media_rate_integer: i16, #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 media_rate_fraction: i16, } /// Media header box 'mdhd' #[derive(Debug)] struct MediaHeaderBox { timescale: u32, duration: u64, } // Chunk offset box 'stco' or 'co64' #[derive(Debug)] pub struct ChunkOffsetBox { pub offsets: TryVec, } // Sync sample box 'stss' #[derive(Debug)] pub struct SyncSampleBox { pub samples: TryVec, } // Sample to chunk box 'stsc' #[derive(Debug)] pub struct SampleToChunkBox { pub samples: TryVec, } #[derive(Debug)] pub struct SampleToChunk { pub first_chunk: u32, pub samples_per_chunk: u32, pub sample_description_index: u32, } // Sample size box 'stsz' #[derive(Debug)] pub struct SampleSizeBox { pub sample_size: u32, pub sample_sizes: TryVec, } // Time to sample box 'stts' #[derive(Debug)] pub struct TimeToSampleBox { pub samples: TryVec, } #[repr(C)] #[derive(Debug)] pub struct Sample { pub sample_count: u32, pub sample_delta: u32, } #[derive(Debug, Clone, Copy)] pub enum TimeOffsetVersion { Version0(u32), Version1(i32), } #[derive(Debug, Clone)] pub struct TimeOffset { pub sample_count: u32, pub time_offset: TimeOffsetVersion, } #[derive(Debug)] pub struct CompositionOffsetBox { pub samples: TryVec, } // Handler reference box 'hdlr' #[derive(Debug)] struct HandlerBox { handler_type: FourCC, } // Sample description box 'stsd' #[derive(Debug)] pub struct SampleDescriptionBox { pub descriptions: TryVec, } #[derive(Debug)] pub enum SampleEntry { Audio(AudioSampleEntry), Video(VideoSampleEntry), Unknown, } #[derive(Debug)] pub struct TrackReferenceBox { pub references: TryVec, } impl TrackReferenceBox { pub fn has_auxl_reference(&self, track_id: u32) -> bool { self.references.iter().any(|entry| match entry { TrackReferenceEntry::Auxiliary(aux_entry) => aux_entry.track_ids.contains(&track_id), }) } } #[derive(Debug)] pub enum TrackReferenceEntry { Auxiliary(TrackReference), } #[derive(Debug)] pub struct TrackReference { pub track_ids: TryVec, } /// An Elementary Stream Descriptor /// See MPEG-4 Systems (ISO 14496-1:2010) § 7.2.6.5 #[allow(non_camel_case_types)] #[derive(Debug, Default)] pub struct ES_Descriptor { pub audio_codec: CodecType, pub audio_object_type: Option, pub extended_audio_object_type: Option, pub audio_sample_rate: Option, pub audio_channel_count: Option, #[cfg(feature = "mp4v")] pub video_codec: CodecType, pub codec_esds: TryVec, pub decoder_specific_data: TryVec, // Data in DECODER_SPECIFIC_TAG } #[allow(non_camel_case_types)] #[derive(Debug)] pub enum AudioCodecSpecific { ES_Descriptor(ES_Descriptor), FLACSpecificBox(FLACSpecificBox), OpusSpecificBox(OpusSpecificBox), ALACSpecificBox(ALACSpecificBox), MP3, LPCM, #[cfg(feature = "3gpp")] AMRSpecificBox(TryVec), } #[derive(Debug)] pub struct AudioSampleEntry { pub codec_type: CodecType, #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 data_reference_index: u16, pub channelcount: u32, pub samplesize: u16, pub samplerate: f64, pub codec_specific: AudioCodecSpecific, pub protection_info: TryVec, } #[derive(Debug)] pub enum VideoCodecSpecific { AVCConfig(TryVec), VPxConfig(VPxConfigBox), AV1Config(AV1ConfigBox), ESDSConfig(TryVec), H263Config(TryVec), } #[derive(Debug)] pub struct VideoSampleEntry { pub codec_type: CodecType, #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 data_reference_index: u16, pub width: u16, pub height: u16, pub codec_specific: VideoCodecSpecific, pub protection_info: TryVec, } /// Represent a Video Partition Codec Configuration 'vpcC' box (aka vp9). The meaning of each /// field is covered in detail in "VP Codec ISO Media File Format Binding". #[derive(Debug)] pub struct VPxConfigBox { /// An integer that specifies the VP codec profile. #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 profile: u8, /// An integer that specifies a VP codec level all samples conform to the following table. /// For a description of the various levels, please refer to the VP9 Bitstream Specification. #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 level: u8, /// An integer that specifies the bit depth of the luma and color components. Valid values /// are 8, 10, and 12. pub bit_depth: u8, /// Really an enum defined by the "Colour primaries" section of ISO 23091-2:2019 § 8.1. pub colour_primaries: u8, /// Really an enum defined by "VP Codec ISO Media File Format Binding". pub chroma_subsampling: u8, /// Really an enum defined by the "Transfer characteristics" section of ISO 23091-2:2019 § 8.2. #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 transfer_characteristics: u8, /// Really an enum defined by the "Matrix coefficients" section of ISO 23091-2:2019 § 8.3. /// Available in 'VP Codec ISO Media File Format' version 1 only. #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 matrix_coefficients: Option, /// Indicates the black level and range of the luma and chroma signals. 0 = legal range /// (e.g. 16-235 for 8 bit sample depth); 1 = full range (e.g. 0-255 for 8-bit sample depth). #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 video_full_range_flag: bool, /// This is not used for VP8 and VP9 . Intended for binary codec initialization data. pub codec_init: TryVec, } /// See [AV1-ISOBMFF § 2.3.3](https://aomediacodec.github.io/av1-isobmff/#av1codecconfigurationbox-syntax) #[derive(Debug)] pub struct AV1ConfigBox { pub profile: u8, pub level: u8, pub tier: u8, pub bit_depth: u8, pub monochrome: bool, pub chroma_subsampling_x: u8, pub chroma_subsampling_y: u8, pub chroma_sample_position: u8, pub initial_presentation_delay_present: bool, pub initial_presentation_delay_minus_one: u8, // The raw config contained in the av1c box. Because some decoders accept this data as a binary // blob, rather than as structured data, we store the blob here for convenience. pub raw_config: TryVec, } impl AV1ConfigBox { const CONFIG_OBUS_OFFSET: usize = 4; pub fn config_obus(&self) -> &[u8] { &self.raw_config[Self::CONFIG_OBUS_OFFSET..] } } #[derive(Debug)] pub struct FLACMetadataBlock { pub block_type: u8, pub data: TryVec, } /// Represents a FLACSpecificBox 'dfLa' #[derive(Debug)] pub struct FLACSpecificBox { #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 version: u8, pub blocks: TryVec, } #[derive(Debug)] struct ChannelMappingTable { stream_count: u8, coupled_count: u8, channel_mapping: TryVec, } /// Represent an OpusSpecificBox 'dOps' #[derive(Debug)] pub struct OpusSpecificBox { pub version: u8, output_channel_count: u8, pre_skip: u16, input_sample_rate: u32, output_gain: i16, channel_mapping_family: u8, channel_mapping_table: Option, } /// Represent an ALACSpecificBox 'alac' #[derive(Debug)] pub struct ALACSpecificBox { #[allow(dead_code)] // See https://github.com/mozilla/mp4parse-rust/issues/340 version: u8, pub data: TryVec, } #[derive(Debug)] pub struct MovieExtendsBox { pub fragment_duration: Option, } pub type ByteData = TryVec; #[derive(Debug, Default)] pub struct ProtectionSystemSpecificHeaderBox { pub system_id: ByteData, pub kid: TryVec, pub data: ByteData, // The entire pssh box (include header) required by Gecko. pub box_content: ByteData, } #[derive(Debug, Default, Clone)] pub struct SchemeTypeBox { pub scheme_type: FourCC, pub scheme_version: u32, } #[derive(Debug, Default)] pub struct TrackEncryptionBox { pub is_encrypted: u8, pub iv_size: u8, pub kid: TryVec, // Members for pattern encryption schemes pub crypt_byte_block_count: Option, pub skip_byte_block_count: Option, pub constant_iv: Option>, // End pattern encryption scheme members } #[derive(Debug, Default)] pub struct ProtectionSchemeInfoBox { pub original_format: FourCC, pub scheme_type: Option, pub tenc: Option, } /// Represents a userdata box 'udta'. /// Currently, only the metadata atom 'meta' /// is parsed. #[derive(Debug, Default)] pub struct UserdataBox { pub meta: Option, } /// Represents possible contents of the /// ©gen or gnre atoms within a metadata box. /// 'udta.meta.ilst' may only have either a /// standard genre box 'gnre' or a custom /// genre box '©gen', but never both at once. #[derive(Debug, PartialEq)] pub enum Genre { /// A standard ID3v1 numbered genre. StandardGenre(u8), /// Any custom genre string. CustomGenre(TryString), } /// Represents the contents of a 'stik' /// atom that indicates content types within /// iTunes. #[derive(Debug, Clone, Eq, PartialEq)] pub enum MediaType { /// Movie is stored as 0 in a 'stik' atom. Movie, // 0 /// Normal is stored as 1 in a 'stik' atom. Normal, // 1 /// AudioBook is stored as 2 in a 'stik' atom. AudioBook, // 2 /// WhackedBookmark is stored as 5 in a 'stik' atom. WhackedBookmark, // 5 /// MusicVideo is stored as 6 in a 'stik' atom. MusicVideo, // 6 /// ShortFilm is stored as 9 in a 'stik' atom. ShortFilm, // 9 /// TVShow is stored as 10 in a 'stik' atom. TVShow, // 10 /// Booklet is stored as 11 in a 'stik' atom. Booklet, // 11 /// An unknown 'stik' value. Unknown(u8), } /// Represents the parental advisory rating on the track, /// stored within the 'rtng' atom. #[derive(Debug, Clone, Eq, PartialEq)] pub enum AdvisoryRating { /// Clean is always stored as 2 in an 'rtng' atom. Clean, // 2 /// A value of 0 in an 'rtng' atom indicates 'Inoffensive' Inoffensive, // 0 /// Any non 2 or 0 value in 'rtng' indicates the track is explicit. Explicit(u8), } /// Represents the contents of 'ilst' atoms within /// a metadata box 'meta', parsed as iTunes metadata using /// the conventional tags. #[derive(Debug, Default)] pub struct MetadataBox { /// The album name, '©alb' pub album: Option, /// The artist name '©art' or '©ART' pub artist: Option, /// The album artist 'aART' pub album_artist: Option, /// Track comments '©cmt' pub comment: Option, /// The date or year field '©day' /// /// This is stored as an arbitrary string, /// and may not necessarily be in a valid date /// format. pub year: Option, /// The track title '©nam' pub title: Option, /// The track genre '©gen' or 'gnre'. pub genre: Option, /// The track number 'trkn'. pub track_number: Option, /// The disc number 'disk' pub disc_number: Option, /// The total number of tracks on the disc, /// stored in 'trkn' pub total_tracks: Option, /// The total number of discs in the album, /// stored in 'disk' pub total_discs: Option, /// The composer of the track '©wrt' pub composer: Option, /// The encoder used to create this track '©too' pub encoder: Option, /// The encoded-by settingo this track '©enc' pub encoded_by: Option, /// The tempo or BPM of the track 'tmpo' pub beats_per_minute: Option, /// Copyright information of the track 'cprt' pub copyright: Option, /// Whether or not this track is part of a compilation 'cpil' pub compilation: Option, /// The advisory rating of this track 'rtng' pub advisory: Option, /// The personal rating of this track, 'rate'. /// /// This is stored in the box as string data, but /// the format is an integer percentage from 0 - 100, /// where 100 is displayed as 5 stars out of 5. pub rating: Option, /// The grouping this track belongs to '©grp' pub grouping: Option, /// The media type of this track 'stik' pub media_type: Option, // stik /// Whether or not this track is a podcast 'pcst' pub podcast: Option, /// The category of ths track 'catg' pub category: Option, /// The podcast keyword 'keyw' pub keyword: Option, /// The podcast url 'purl' pub podcast_url: Option, /// The podcast episode GUID 'egid' pub podcast_guid: Option, /// The description of the track 'desc' pub description: Option, /// The long description of the track 'ldes'. /// /// Unlike other string fields, the long description field /// can be longer than 256 characters. pub long_description: Option, /// The lyrics of the track '©lyr'. /// /// Unlike other string fields, the lyrics field /// can be longer than 256 characters. pub lyrics: Option, /// The name of the TV network this track aired on 'tvnn'. pub tv_network_name: Option, /// The name of the TV Show for this track 'tvsh'. pub tv_show_name: Option, /// The name of the TV Episode for this track 'tven'. pub tv_episode_name: Option, /// The number of the TV Episode for this track 'tves'. pub tv_episode_number: Option, /// The season of the TV Episode of this track 'tvsn'. pub tv_season: Option, /// The date this track was purchased 'purd'. pub purchase_date: Option, /// Whether or not this track supports gapless playback 'pgap' pub gapless_playback: Option, /// Any cover artwork attached to this track 'covr' /// /// 'covr' is unique in that it may contain multiple 'data' sub-entries, /// each an image file. Here, each subentry's raw binary data is exposed, /// which may contain image data in JPEG or PNG format. pub cover_art: Option>>, /// The owner of the track 'ownr' pub owner: Option, /// Whether or not this track is HD Video 'hdvd' pub hd_video: Option, /// The name of the track to sort by 'sonm' pub sort_name: Option, /// The name of the album to sort by 'soal' pub sort_album: Option, /// The name of the artist to sort by 'soar' pub sort_artist: Option, /// The name of the album artist to sort by 'soaa' pub sort_album_artist: Option, /// The name of the composer to sort by 'soco' pub sort_composer: Option, /// Metadata #[cfg(feature = "meta-xml")] pub xml: Option, } /// See ISOBMFF (ISO 14496-12:2020) § 8.11.2.1 #[cfg(feature = "meta-xml")] #[derive(Debug)] pub enum XmlBox { /// XML metadata StringXmlBox(TryString), /// Binary XML metadata BinaryXmlBox(TryVec), } /// Internal data structures. #[derive(Debug, Default)] pub struct MediaContext { pub timescale: Option, /// Tracks found in the file. pub tracks: TryVec, pub mvex: Option, pub psshs: TryVec, pub userdata: Option>, #[cfg(feature = "meta-xml")] pub metadata: Option>, } /// An ISOBMFF item as described by an iloc box. For the sake of avoiding copies, /// this can either be represented by the `Location` variant, which indicates /// where the data exists within a `DataBox` stored separately, or the `Data` /// variant which owns the data. Unfortunately, it's not simple to represent /// this as a [`std::borrow::Cow`], or other reference-based type, because /// multiple instances may references different parts of the same [`DataBox`] /// and we want to avoid the copy that splitting the storage would entail. enum IsobmffItem { MdatLocation(Extent), IdatLocation(Extent), Data(TryVec), } impl fmt::Debug for IsobmffItem { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self { IsobmffItem::MdatLocation(extent) | IsobmffItem::IdatLocation(extent) => f .debug_struct("IsobmffItem::Location") .field("0", &format_args!("{extent:?}")) .finish(), IsobmffItem::Data(data) => f .debug_struct("IsobmffItem::Data") .field("0", &format_args!("{} bytes", data.len())) .finish(), } } } #[derive(Debug)] struct AvifItem { /// The `item_ID` from ISOBMFF (ISO 14496-12:2020) § 8.11.3 /// /// See [`read_iloc`] id: ItemId, /// AV1 Image Item per image_data: IsobmffItem, } impl AvifItem { fn with_inline_data(id: ItemId) -> Self { Self { id, image_data: IsobmffItem::Data(TryVec::new()), } } } #[derive(Default, Debug)] pub struct AvifContext { /// Level of deviation from the specification before failing the parse strictness: ParseStrictness, /// Storage elements which can be located anywhere within the "file" identified by /// [`BoxType::ItemLocationBox`]es using [`ConstructionMethod::File`]. /// Referred to by the [`IsobmffItem`]`::*Location` variants of the `AvifItem`s in this struct media_storage: TryVec, /// Similar to `media_storage`, but for a single optional chunk of storage within the /// MetaBox itentified by [`BoxType::ItemLocationBox`]es using [`ConstructionMethod::Idat`]. item_data_box: Option, /// The item indicated by the `pitm` box, See ISOBMFF (ISO 14496-12:2020) § 8.11.4 /// May be `None` in the pure image sequence case. primary_item: Option, /// Associated alpha channel for the primary item, if any alpha_item: Option, /// If true, divide RGB values by the alpha value. /// See `prem` in MIAF (ISO 23000-22:2019) § 7.3.5.2 pub premultiplied_alpha: bool, /// All properties associated with `primary_item` or `alpha_item` item_properties: ItemPropertiesBox, /// Should probably only ever be [`AVIF_BRAND`] or [`AVIS_BRAND`], but other values /// are legal as long as one of the two is the `compatible_brand` list. pub major_brand: FourCC, /// Information on the sequence contained in the image, or None if not present pub sequence: Option, /// A collection of unsupported features encountered during the parse pub unsupported_features: UnsupportedFeatures, } impl AvifContext { pub fn primary_item_is_present(&self) -> bool { self.primary_item.is_some() } pub fn primary_item_coded_data(&self) -> Option<&[u8]> { self.primary_item .as_ref() .map(|item| self.item_as_slice(item)) } pub fn primary_item_bits_per_channel(&self) -> Option> { self.primary_item .as_ref() .map(|item| self.image_bits_per_channel(item.id)) } pub fn alpha_item_is_present(&self) -> bool { self.alpha_item.is_some() } pub fn alpha_item_coded_data(&self) -> Option<&[u8]> { self.alpha_item .as_ref() .map(|item| self.item_as_slice(item)) } pub fn alpha_item_bits_per_channel(&self) -> Option> { self.alpha_item .as_ref() .map(|item| self.image_bits_per_channel(item.id)) } fn image_bits_per_channel(&self, item_id: ItemId) -> Result<&[u8]> { match self .item_properties .get(item_id, BoxType::PixelInformationBox)? { Some(ItemProperty::Channels(pixi)) => Ok(pixi.bits_per_channel.as_slice()), Some(other_property) => panic!("property key mismatch: {:?}", other_property), None => Ok(&[]), } } pub fn spatial_extents_ptr(&self) -> Result<*const ImageSpatialExtentsProperty> { if let Some(primary_item) = &self.primary_item { match self .item_properties .get(primary_item.id, BoxType::ImageSpatialExtentsProperty)? { Some(ItemProperty::ImageSpatialExtents(ispe)) => Ok(ispe), Some(other_property) => panic!("property key mismatch: {:?}", other_property), None => { fail_with_status_if( self.strictness != ParseStrictness::Permissive, Status::IspeMissing, )?; Ok(std::ptr::null()) } } } else { Ok(std::ptr::null()) } } /// Returns None if there is no primary item or it has no associated NCLX colour boxes. pub fn nclx_colour_information_ptr(&self) -> Option> { if let Some(primary_item) = &self.primary_item { match self.item_properties.get_multiple(primary_item.id, |prop| { matches!(prop, ItemProperty::Colour(ColourInformation::Nclx(_))) }) { Ok(nclx_colr_boxes) => match *nclx_colr_boxes.as_slice() { [] => None, [ItemProperty::Colour(ColourInformation::Nclx(nclx)), ..] => { if nclx_colr_boxes.len() > 1 { warn!("Multiple nclx colr boxes, using first"); } Some(Ok(nclx)) } _ => unreachable!("Expect only ColourInformation::Nclx(_) matches"), }, Err(e) => Some(Err(e)), } } else { None } } /// Returns None if there is no primary item or it has no associated ICC colour boxes. pub fn icc_colour_information(&self) -> Option> { if let Some(primary_item) = &self.primary_item { match self.item_properties.get_multiple(primary_item.id, |prop| { matches!(prop, ItemProperty::Colour(ColourInformation::Icc(_, _))) }) { Ok(icc_colr_boxes) => match *icc_colr_boxes.as_slice() { [] => None, [ItemProperty::Colour(ColourInformation::Icc(icc, _)), ..] => { if icc_colr_boxes.len() > 1 { warn!("Multiple ICC profiles in colr boxes, using first"); } Some(Ok(icc.bytes.as_slice())) } _ => unreachable!("Expect only ColourInformation::Icc(_) matches"), }, Err(e) => Some(Err(e)), } } else { None } } pub fn image_rotation(&self) -> Result { if let Some(primary_item) = &self.primary_item { match self .item_properties .get(primary_item.id, BoxType::ImageRotation)? { Some(ItemProperty::Rotation(irot)) => Ok(*irot), Some(other_property) => panic!("property key mismatch: {:?}", other_property), None => Ok(ImageRotation::D0), } } else { Ok(ImageRotation::D0) } } pub fn image_mirror_ptr(&self) -> Result<*const ImageMirror> { if let Some(primary_item) = &self.primary_item { match self .item_properties .get(primary_item.id, BoxType::ImageMirror)? { Some(ItemProperty::Mirroring(imir)) => Ok(imir), Some(other_property) => panic!("property key mismatch: {:?}", other_property), None => Ok(std::ptr::null()), } } else { Ok(std::ptr::null()) } } pub fn pixel_aspect_ratio_ptr(&self) -> Result<*const PixelAspectRatio> { if let Some(primary_item) = &self.primary_item { match self .item_properties .get(primary_item.id, BoxType::PixelAspectRatioBox)? { Some(ItemProperty::PixelAspectRatio(pasp)) => Ok(pasp), Some(other_property) => panic!("property key mismatch: {:?}", other_property), None => Ok(std::ptr::null()), } } else { Ok(std::ptr::null()) } } /// A helper for the various `AvifItem`s to expose a reference to the /// underlying data while avoiding copies. fn item_as_slice<'a>(&'a self, item: &'a AvifItem) -> &'a [u8] { match &item.image_data { IsobmffItem::MdatLocation(extent) => { for mdat in &self.media_storage { if let Some(slice) = mdat.get(extent) { return slice; } } unreachable!( "IsobmffItem::MdatLocation requires the location exists in AvifContext::media_storage" ); } IsobmffItem::IdatLocation(extent) => { self.item_data_box .as_ref() .and_then(|idat| idat.get(extent)) .unwrap_or_else(|| unreachable!("IsobmffItem::IdatLocation equires the location exists in AvifContext::item_data_box")) } IsobmffItem::Data(data) => data.as_slice(), } } } struct AvifMeta { item_references: TryVec, item_properties: ItemPropertiesBox, /// Required for AvifImageType::Primary, but optional otherwise /// See HEIF (ISO/IEC 23008-12:2017) § 7.1, 10.2.1 primary_item_id: Option, item_infos: TryVec, iloc_items: TryHashMap, item_data_box: Option, } #[derive(Debug)] enum DataBoxMetadata { Idat, Mdat { /// Offset of `data` from the beginning of the "file". See ConstructionMethod::File. /// Note: the file may not be an actual file, read_avif supports any `&mut impl Read` /// source for input. However we try to match the terminology used in the spec. file_offset: u64, }, } /// Represents either an Item Data Box (ISOBMFF (ISO 14496-12:2020) § 8.11.11) /// Or a Media Data Box (ISOBMFF (ISO 14496-12:2020) § 8.1.1) struct DataBox { metadata: DataBoxMetadata, data: TryVec, } impl fmt::Debug for DataBox { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("DataBox") .field("metadata", &self.metadata) .field("data", &format_args!("{} bytes", self.data.len())) .finish() } } fn u64_to_usize_logged(x: u64) -> Option { match x.try_into() { Ok(x) => Some(x), Err(e) => { error!("{:?} converting {:?}", e, x); None } } } impl DataBox { fn from_mdat(file_offset: u64, data: TryVec) -> Self { Self { metadata: DataBoxMetadata::Mdat { file_offset }, data, } } fn from_idat(data: TryVec) -> Self { Self { metadata: DataBoxMetadata::Idat, data, } } fn data(&self) -> &[u8] { &self.data } /// Convert an absolute offset to an offset relative to the beginning of the /// slice [`DataBox::data`] returns. Returns None if the offset would be /// negative or if the offset would overflow a `usize`. fn start(&self, offset: u64) -> Option { match self.metadata { DataBoxMetadata::Idat => u64_to_usize_logged(offset), DataBoxMetadata::Mdat { file_offset } => { let start = offset.checked_sub(file_offset); if start.is_none() { error!("Overflow subtracting {} + {}", offset, file_offset); } u64_to_usize_logged(start?) } } } /// Returns an appropriate variant of [`IsobmffItem`] to describe the extent /// referencing data within this type of box. fn location(&self, extent: &Extent) -> IsobmffItem { match self.metadata { DataBoxMetadata::Idat => IsobmffItem::IdatLocation(extent.clone()), DataBoxMetadata::Mdat { .. } => IsobmffItem::MdatLocation(extent.clone()), } } /// Return a slice from the DataBox specified by the provided `extent`. /// Returns `None` if the extent isn't fully contained by the DataBox or if /// either the offset or length (if the extent is bounded) of the slice /// would overflow a `usize`. fn get<'a>(&'a self, extent: &'a Extent) -> Option<&'a [u8]> { match extent { Extent::WithLength { offset, len } => { let start = self.start(*offset)?; let end = start.checked_add(*len); if end.is_none() { error!("Overflow adding {} + {}", start, len); } self.data().get(start..end?) } Extent::ToEnd { offset } => { let start = self.start(*offset)?; self.data().get(start..) } } } } #[cfg(test)] mod media_data_box_tests { use super::*; impl DataBox { fn at_offset(file_offset: u64, data: std::vec::Vec) -> Self { DataBox { metadata: DataBoxMetadata::Mdat { file_offset }, data: data.into(), } } } #[test] fn extent_with_length_before_mdat_returns_none() { let mdat = DataBox::at_offset(100, vec![1; 5]); let extent = Extent::WithLength { offset: 0, len: 2 }; assert!(mdat.get(&extent).is_none()); } #[test] fn extent_to_end_before_mdat_returns_none() { let mdat = DataBox::at_offset(100, vec![1; 5]); let extent = Extent::ToEnd { offset: 0 }; assert!(mdat.get(&extent).is_none()); } #[test] fn extent_with_length_crossing_front_mdat_boundary_returns_none() { let mdat = DataBox::at_offset(100, vec![1; 5]); let extent = Extent::WithLength { offset: 99, len: 3 }; assert!(mdat.get(&extent).is_none()); } #[test] fn extent_with_length_which_is_subset_of_mdat() { let mdat = DataBox::at_offset(100, vec![1; 5]); let extent = Extent::WithLength { offset: 101, len: 2, }; assert_eq!(mdat.get(&extent), Some(&[1, 1][..])); } #[test] fn extent_to_end_which_is_subset_of_mdat() { let mdat = DataBox::at_offset(100, vec![1; 5]); let extent = Extent::ToEnd { offset: 101 }; assert_eq!(mdat.get(&extent), Some(&[1, 1, 1, 1][..])); } #[test] fn extent_with_length_which_is_all_of_mdat() { let mdat = DataBox::at_offset(100, vec![1; 5]); let extent = Extent::WithLength { offset: 100, len: 5, }; assert_eq!(mdat.get(&extent), Some(mdat.data.as_slice())); } #[test] fn extent_to_end_which_is_all_of_mdat() { let mdat = DataBox::at_offset(100, vec![1; 5]); let extent = Extent::ToEnd { offset: 100 }; assert_eq!(mdat.get(&extent), Some(mdat.data.as_slice())); } #[test] fn extent_with_length_crossing_back_mdat_boundary_returns_none() { let mdat = DataBox::at_offset(100, vec![1; 5]); let extent = Extent::WithLength { offset: 103, len: 3, }; assert!(mdat.get(&extent).is_none()); } #[test] fn extent_with_length_after_mdat_returns_none() { let mdat = DataBox::at_offset(100, vec![1; 5]); let extent = Extent::WithLength { offset: 200, len: 2, }; assert!(mdat.get(&extent).is_none()); } #[test] fn extent_to_end_after_mdat_returns_none() { let mdat = DataBox::at_offset(100, vec![1; 5]); let extent = Extent::ToEnd { offset: 200 }; assert!(mdat.get(&extent).is_none()); } #[test] fn extent_with_length_which_overflows_usize() { let mdat = DataBox::at_offset(std::u64::MAX - 1, vec![1; 5]); let extent = Extent::WithLength { offset: std::u64::MAX, len: std::usize::MAX, }; assert!(mdat.get(&extent).is_none()); } // The end of the range would overflow `usize` if it were calculated, but // because the range end is unbounded, we don't calculate it. #[test] fn extent_to_end_which_overflows_usize() { let mdat = DataBox::at_offset(std::u64::MAX - 1, vec![1; 5]); let extent = Extent::ToEnd { offset: std::u64::MAX, }; assert_eq!(mdat.get(&extent), Some(&[1, 1, 1, 1][..])); } } #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] struct PropertyIndex(u16); #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd)] struct ItemId(u32); impl ItemId { fn read(src: &mut impl ReadBytesExt, version: u8) -> Result { Ok(ItemId(if version == 0 { be_u16(src)?.into() } else { be_u32(src)? })) } } /// Used for 'infe' boxes within 'iinf' boxes /// See ISOBMFF (ISO 14496-12:2020) § 8.11.6 /// Only versions {2, 3} are supported #[derive(Debug)] struct ItemInfoEntry { item_id: ItemId, item_type: u32, } /// See ISOBMFF (ISO 14496-12:2020) § 8.11.12 #[derive(Debug)] struct SingleItemTypeReferenceBox { item_type: FourCC, from_item_id: ItemId, to_item_id: ItemId, } /// Potential sizes (in bytes) of variable-sized fields of the 'iloc' box /// See ISOBMFF (ISO 14496-12:2020) § 8.11.3 #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum IlocFieldSize { Zero, Four, Eight, } impl IlocFieldSize { fn as_bits(&self) -> u8 { match self { IlocFieldSize::Zero => 0, IlocFieldSize::Four => 32, IlocFieldSize::Eight => 64, } } } impl TryFrom for IlocFieldSize { type Error = Error; fn try_from(value: u8) -> Result { match value { 0 => Ok(Self::Zero), 4 => Ok(Self::Four), 8 => Ok(Self::Eight), _ => Status::IlocBadFieldSize.into(), } } } #[derive(Debug, PartialEq, Eq)] enum IlocVersion { Zero, One, Two, } impl TryFrom for IlocVersion { type Error = Error; fn try_from(value: u8) -> Result { match value { 0 => Ok(Self::Zero), 1 => Ok(Self::One), 2 => Ok(Self::Two), _ => Err(Error::Unsupported("unsupported version in 'iloc' box")), } } } /// Used for 'iloc' boxes /// See ISOBMFF (ISO 14496-12:2020) § 8.11.3 /// `base_offset` is omitted since it is integrated into the ranges in `extents` /// `data_reference_index` is omitted, since only 0 (i.e., this file) is supported #[derive(Debug)] struct ItemLocationBoxItem { construction_method: ConstructionMethod, /// Unused for ConstructionMethod::Idat extents: TryVec, } /// See ISOBMFF (ISO 14496-12:2020) § 8.11.3 /// /// Note: per MIAF (ISO 23000-22:2019) § 7.2.1.7:
/// > MIAF image items are constrained as follows:
/// > — `construction_method` shall be equal to 0 for MIAF image items that are coded image items.
/// > — `construction_method` shall be equal to 0 or 1 for MIAF image items that are derived image items. #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum ConstructionMethod { File = 0, Idat = 1, Item = 2, } /// Describes a region where a item specified by an `ItemLocationBoxItem` is stored. /// The offset is `u64` since that's the maximum possible size and since the relative /// nature of `DataBox` means this can still possibly succeed even in the case /// that the raw value exceeds std::usize::MAX on platforms where that type is smaller /// than u64. However, `len` is stored as a `usize` since no value larger than /// `std::usize::MAX` can be used in a successful indexing operation in rust. /// `extent_index` is omitted since it's only used for ConstructionMethod::Item which /// is currently not implemented. #[derive(Clone, Debug)] enum Extent { WithLength { offset: u64, len: usize }, ToEnd { offset: u64 }, } #[derive(Debug, PartialEq, Eq, Default)] pub enum TrackType { Audio, Video, Picture, AuxiliaryVideo, Metadata, #[default] Unknown, } // This type is used by mp4parse_capi since it needs to be passed from FFI consumers // The C-visible struct is renamed via mp4parse_capi/cbindgen.toml to match naming conventions #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub enum ParseStrictness { Permissive, // Error only on ambiguous inputs #[default] Normal, // Error on "shall" directives, log warnings for "should" Strict, // Error on "should" directives } fn fail_with_status_if(violation: bool, status: Status) -> Result<()> { let error = Error::from(status); if violation { Err(error) } else { warn!("{:?}", error); Ok(()) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum CodecType { #[default] Unknown, MP3, AAC, FLAC, Opus, H264, // 14496-10 MP4V, // 14496-2 AV1, VP9, VP8, EncryptedVideo, EncryptedAudio, LPCM, // QT ALAC, H263, #[cfg(feature = "3gpp")] AMRNB, #[cfg(feature = "3gpp")] AMRWB, } /// The media's global (mvhd) timescale in units per second. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct MediaTimeScale(pub u64); /// A time to be scaled by the media's global (mvhd) timescale. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct MediaScaledTime(pub u64); /// The track's local (mdhd) timescale. /// Members are timescale units per second and the track id. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct TrackTimeScale(pub T, pub usize); /// A time to be scaled by the track's local (mdhd) timescale. /// Members are time in scale units and the track id. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct TrackScaledTime(pub T, pub usize); impl std::ops::Add for TrackScaledTime where T: num_traits::CheckedAdd, { type Output = Option; fn add(self, other: TrackScaledTime) -> Self::Output { self.0.checked_add(&other.0).map(|sum| Self(sum, self.1)) } } #[derive(Debug, Default)] pub struct Track { pub id: usize, pub track_type: TrackType, pub looped: Option, pub empty_duration: Option, pub edited_duration: Option, pub media_time: Option>, pub timescale: Option>, pub duration: Option>, pub track_id: Option, pub tkhd: Option, // TODO(kinetik): find a nicer way to export this. pub stsd: Option, pub stts: Option, pub stsc: Option, pub stsz: Option, pub stco: Option, // It is for stco or co64. pub stss: Option, pub ctts: Option, pub tref: Option, } impl Track { fn new(id: usize) -> Track { Track { id, ..Default::default() } } } /// See ISOBMFF (ISO 14496-12:2020) § 4.2 struct BMFFBox<'a, T: 'a> { head: BoxHeader, content: Take<&'a mut T>, } struct BoxIter<'a, T: 'a> { src: &'a mut T, } impl<'a, T: Read> BoxIter<'a, T> { fn new(src: &mut T) -> BoxIter { BoxIter { src } } fn next_box(&mut self) -> Result>> { let r = read_box_header(self.src); match r { Ok(h) => Ok(Some(BMFFBox { head: h, content: self.src.take(h.size.saturating_sub(h.offset)), })), Err(Error::UnexpectedEOF) => Ok(None), Err(e) => Err(e), } } } impl<'a, T: Read> Read for BMFFBox<'a, T> { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.content.read(buf) } } impl<'a, T: Read> TryRead for BMFFBox<'a, T> { fn try_read_to_end(&mut self, buf: &mut TryVec) -> std::io::Result { fallible_collections::try_read_up_to(self, self.bytes_left(), buf) } } impl<'a, T: Offset> Offset for BMFFBox<'a, T> { fn offset(&self) -> u64 { self.content.get_ref().offset() } } impl<'a, T: Read> BMFFBox<'a, T> { fn bytes_left(&self) -> u64 { self.content.limit() } fn get_header(&self) -> &BoxHeader { &self.head } fn box_iter<'b>(&'b mut self) -> BoxIter> { BoxIter::new(self) } } impl<'a, T> Drop for BMFFBox<'a, T> { fn drop(&mut self) { if self.content.limit() > 0 { let name: FourCC = From::from(self.head.name); debug!("Dropping {} bytes in '{}'", self.content.limit(), name); } } } /// Read and parse a box header. /// /// Call this first to determine the type of a particular mp4 box /// and its length. Used internally for dispatching to specific /// parsers for the internal content, or to get the length to /// skip unknown or uninteresting boxes. /// /// See ISOBMFF (ISO 14496-12:2020) § 4.2 fn read_box_header(src: &mut T) -> Result { let size32 = match be_u32(src) { Ok(v) => v, Err(error) => return Err(error), }; let name = BoxType::from(be_u32(src)?); let size = match size32 { // valid only for top-level box and indicates it's the last box in the file. usually mdat. 0 => { if name == BoxType::MediaDataBox { 0 } else { return Err(Error::Unsupported("unknown sized box")); } } 1 => be_u64(src)?, _ => u64::from(size32), }; trace!("read_box_header: name: {:?}, size: {}", name, size); let mut offset = match size32 { 1 => BoxHeader::MIN_LARGE_SIZE, _ => BoxHeader::MIN_SIZE, }; let uuid = if name == BoxType::UuidBox { if size >= offset + 16 { let mut buffer = [0u8; 16]; let count = src.read(&mut buffer)?; offset += count.to_u64(); if count == 16 { Some(buffer) } else { debug!("malformed uuid (short read)"); return Err(Error::UnexpectedEOF); } } else { None } } else { None }; match size32 { 0 => (), 1 if offset > size => return Err(Error::from(Status::BoxBadWideSize)), _ if offset > size => return Err(Error::from(Status::BoxBadSize)), _ => (), } Ok(BoxHeader { name, size, offset, uuid, }) } /// Parse the extra header fields for a full box. fn read_fullbox_extra(src: &mut T) -> Result<(u8, u32)> { let version = src.read_u8()?; let flags_a = src.read_u8()?; let flags_b = src.read_u8()?; let flags_c = src.read_u8()?; Ok(( version, u32::from(flags_a) << 16 | u32::from(flags_b) << 8 | u32::from(flags_c), )) } // Parse the extra fields for a full box whose flag fields must be zero. fn read_fullbox_version_no_flags(src: &mut T) -> Result { let (version, flags) = read_fullbox_extra(src)?; if flags != 0 { return Err(Error::Unsupported("expected flags to be 0")); } Ok(version) } /// Skip over the entire contents of a box. fn skip_box_content(src: &mut BMFFBox) -> Result<()> { // Skip the contents of unknown chunks. let to_skip = { let header = src.get_header(); debug!("{:?} (skipped)", header); header .size .checked_sub(header.offset) .ok_or(Error::Unsupported("Skipping past unknown sized box"))? }; assert_eq!(to_skip, src.bytes_left()); skip(src, to_skip) } /// Skip over the remain data of a box. fn skip_box_remain(src: &mut BMFFBox) -> Result<()> { let remain = { let header = src.get_header(); let len = src.bytes_left(); debug!("remain {} (skipped) in {:?}", len, header); len }; skip(src, remain) } #[derive(Debug)] enum AvifImageType { Primary, Sequence, Both, } impl AvifImageType { fn has_primary(&self) -> bool { match self { Self::Primary | Self::Both => true, Self::Sequence => false, } } fn has_sequence(&self) -> bool { match self { Self::Primary => false, Self::Sequence | Self::Both => true, } } } /// Read the contents of an AVIF file pub fn read_avif(f: &mut T, strictness: ParseStrictness) -> Result { debug!("read_avif(strictness: {:?})", strictness); let mut f = OffsetReader::new(f); let mut iter = BoxIter::new(&mut f); let expected_image_type; let mut unsupported_features = UnsupportedFeatures::new(); // 'ftyp' box must occur first; see ISOBMFF (ISO 14496-12:2020) § 4.3.1 let major_brand = if let Some(mut b) = iter.next_box()? { if b.head.name == BoxType::FileTypeBox { let ftyp = read_ftyp(&mut b)?; let has_avif_brand = ftyp.contains(&AVIF_BRAND); let has_avis_brand = ftyp.contains(&AVIS_BRAND); let has_mif1_brand = ftyp.contains(&MIF1_BRAND); let has_msf1_brand = ftyp.contains(&MSF1_BRAND); let primary_image_expected = has_mif1_brand || has_avif_brand; let image_sequence_expected = has_msf1_brand || has_avis_brand; expected_image_type = if primary_image_expected && image_sequence_expected { AvifImageType::Both } else if primary_image_expected { AvifImageType::Primary } else if image_sequence_expected { AvifImageType::Sequence } else { return Status::NoImage.into(); }; debug!("expected_image_type: {:?}", expected_image_type); if primary_image_expected && !has_mif1_brand { fail_with_status_if( strictness == ParseStrictness::Strict, Status::MissingMif1Brand, )?; } if !has_avif_brand && !has_avis_brand { fail_with_status_if( strictness != ParseStrictness::Permissive, Status::MissingAvifOrAvisBrand, )?; } ftyp.major_brand } else { return Status::FtypNotFirst.into(); } } else { return Status::FtypNotFirst.into(); }; let mut meta = None; let mut image_sequence = None; let mut media_storage = TryVec::new(); loop { let mut b = match iter.next_box() { Ok(Some(b)) => b, Ok(_) => break, Err(Error::UnexpectedEOF) => { if strictness == ParseStrictness::Strict { return Err(Error::UnexpectedEOF); } break; } Err(error) => return Err(error), }; trace!("read_avif parsing {:?} box", b.head.name); match b.head.name { BoxType::MetadataBox => { if meta.is_some() { return Status::MetaBadQuantity.into(); } meta = Some(read_avif_meta( &mut b, strictness, &mut unsupported_features, )?); } BoxType::MovieBox if expected_image_type.has_sequence() => { if image_sequence.is_some() { return Status::MoovBadQuantity.into(); } image_sequence = Some(read_moov(&mut b, None)?); } BoxType::MediaDataBox => { let file_offset = b.offset(); let data = if b.head.size == 0 { // Unknown sized `mdat`, read in chunks until EOF. const BUF_SIZE: usize = 64 * 1024; let mut data = TryVec::with_capacity(BUF_SIZE)?; loop { let got = fallible_collections::try_read_up_to( b.content.get_mut(), BUF_SIZE as u64, &mut data, )?; if got == 0 { // Mark `content` as consumed. b.content.set_limit(0); break; } } data } else { b.read_into_try_vec()? }; media_storage.push(DataBox::from_mdat(file_offset, data))?; } _ => { let result = skip_box_content(&mut b); // Allow garbage at EOF if we aren't in strict mode. if b.bytes_left() > 0 && strictness != ParseStrictness::Strict { break; } result?; } } check_parser_state!(b.content); } let AvifMeta { item_references, item_properties, primary_item_id, item_infos, iloc_items, item_data_box, } = meta.ok_or_else(|| Error::from(Status::MetaBadQuantity))?; let (alpha_item_id, premultiplied_alpha) = if let Some(primary_item_id) = primary_item_id { let mut alpha_item_ids = item_references .iter() // Auxiliary image for the primary image .filter(|iref| { iref.to_item_id == primary_item_id && iref.from_item_id != primary_item_id && iref.item_type == b"auxl" }) .map(|iref| iref.from_item_id) // which has the alpha property .filter(|&item_id| item_properties.is_alpha(item_id)); let alpha_item_id = alpha_item_ids.next(); if alpha_item_ids.next().is_some() { return Status::MultipleAlpha.into(); } let premultiplied_alpha = alpha_item_id.map_or(false, |alpha_item_id| { item_references.iter().any(|iref| { iref.from_item_id == primary_item_id && iref.to_item_id == alpha_item_id && iref.item_type == b"prem" }) }); (alpha_item_id, premultiplied_alpha) } else { (None, false) }; debug!("primary_item_id: {:?}", primary_item_id); debug!("alpha_item_id: {:?}", alpha_item_id); let mut primary_item = None; let mut alpha_item = None; // store data or record location of relevant items for (item_id, loc) in iloc_items { let item = if Some(item_id) == primary_item_id { &mut primary_item } else if Some(item_id) == alpha_item_id { &mut alpha_item } else { continue; }; assert!(item.is_none()); // If our item is spread over multiple extents, we'll need to copy it // into a contiguous buffer. Otherwise, we can just store the extent // and return a pointer into the mdat/idat later to avoid the copy. if loc.extents.len() > 1 { *item = Some(AvifItem::with_inline_data(item_id)) } trace!( "{:?} construction_method: {:?}", item_id, loc.construction_method ); // Generalize the process of connecting items to their data; returns // true if the extent is successfully added to the AvifItem let mut find_and_add_to_item = |extent: &Extent, dat: &DataBox| -> Result { if let Some(extent_slice) = dat.get(extent) { match item { None => { trace!("Using IsobmffItem::Location"); *item = Some(AvifItem { id: item_id, image_data: dat.location(extent), }); } Some(AvifItem { image_data: IsobmffItem::Data(bytes), .. }) => { trace!("Using IsobmffItem::Data"); // We could potentially optimize memory usage by trying to avoid reading // or storing dat boxes which aren't used by our API, but for now it seems // like unnecessary complexity bytes.extend_from_slice(extent_slice)?; } _ => unreachable!(), } return Ok(true); } Ok(false) }; match loc.construction_method { ConstructionMethod::File => { for extent in loc.extents { let mut found = false; // try to find an mdat which contains the extent for mdat in media_storage.iter() { if find_and_add_to_item(&extent, mdat)? { found = true; break; } } if !found { return Status::IlocNotFound.into(); } } } ConstructionMethod::Idat => { if let Some(idat) = &item_data_box { for extent in loc.extents { let found = find_and_add_to_item(&extent, idat)?; if !found { return Status::IlocNotFound.into(); } } } else { return Status::IdatMissing.into(); } } ConstructionMethod::Item => { fail_with_status_if( strictness != ParseStrictness::Permissive, Status::ConstructionMethod, )?; } } assert!(item.is_some()); } if (primary_item_id.is_some() && primary_item.is_none()) || (alpha_item_id.is_some() && alpha_item.is_none()) { fail_with_status_if(strictness == ParseStrictness::Strict, Status::PitmNotFound)?; } assert!(primary_item.is_none() || primary_item_id.is_some()); assert!(alpha_item.is_none() || alpha_item_id.is_some()); if expected_image_type.has_primary() && primary_item_id.is_none() { fail_with_status_if( strictness != ParseStrictness::Permissive, Status::PitmMissing, )?; } // Lacking a brand that requires them, it's fine for moov boxes to exist in // BMFF files; they're simply ignored if expected_image_type.has_sequence() && image_sequence.is_none() { fail_with_status_if( strictness != ParseStrictness::Permissive, Status::MoovMissing, )?; } // Returns true iff `id` is `Some` and there is no corresponding property for it let missing_property_for = |id: Option, property: BoxType| -> bool { id.map_or(false, |id| { item_properties .get(id, property) .map_or(true, |opt| opt.is_none()) }) }; // Generalize the property checks so we can apply them to primary and alpha items let mut check_image_item = |item: &mut Option| -> Result<()> { let item_id = item.as_ref().map(|item| item.id); let item_type = item_id.and_then(|item_id| { item_infos .iter() .find(|item_info| item_id == item_info.item_id) .map(|item_info| item_info.item_type) }); match item_type.map(u32::to_be_bytes).as_ref() { Some(b"av01") => { if missing_property_for(item_id, BoxType::AV1CodecConfigurationBox) { fail_with_status_if( strictness != ParseStrictness::Permissive, Status::Av1cMissing, )?; } if missing_property_for(item_id, BoxType::PixelInformationBox) { // The requirement to include pixi is in the process of being changed // to allowing its omission to imply a default value. In anticipation // of that, only give an error in strict mode // See https://github.com/MPEGGroup/MIAF/issues/9 fail_with_status_if( if cfg!(feature = "missing-pixi-permitted") { strictness == ParseStrictness::Strict } else { strictness != ParseStrictness::Permissive }, Status::PixiMissing, )?; } if missing_property_for(item_id, BoxType::ImageSpatialExtentsProperty) { fail_with_status_if( strictness != ParseStrictness::Permissive, Status::IspeMissing, )?; } } Some(b"grid") => { // TODO: https://github.com/mozilla/mp4parse-rust/issues/198 unsupported_features.insert(Feature::Grid); *item = None; } Some(_other_type) => return Status::ImageItemType.into(), None => { if item.is_some() { return Status::ItemTypeMissing.into(); } } } if let Some(AvifItem { id, .. }) = item { if item_properties.forbidden_items.contains(id) { error!("Not processing item id {:?} since it is associated with essential, but unsupported properties", id); *item = None; } } Ok(()) }; check_image_item(&mut primary_item)?; check_image_item(&mut alpha_item)?; Ok(AvifContext { strictness, media_storage, item_data_box, primary_item, alpha_item, premultiplied_alpha, item_properties, major_brand, sequence: image_sequence, unsupported_features, }) } /// Parse a metadata box in the context of an AVIF /// Currently requires the primary item to be an av01 item type and generates /// an error otherwise. /// See ISOBMFF (ISO 14496-12:2020) § 8.11.1 fn read_avif_meta( src: &mut BMFFBox, strictness: ParseStrictness, unsupported_features: &mut UnsupportedFeatures, ) -> Result { let version = read_fullbox_version_no_flags(src)?; if version != 0 { return Err(Error::Unsupported("unsupported meta version")); } let mut read_handler_box = false; let mut primary_item_id = None; let mut item_infos = None; let mut iloc_items = None; let mut item_references = None; let mut item_properties = None; let mut item_data_box = None; let mut iter = src.box_iter(); while let Some(mut b) = iter.next_box()? { trace!("read_avif_meta parsing {:?} box", b.head.name); if !read_handler_box && b.head.name != BoxType::HandlerBox { fail_with_status_if( strictness != ParseStrictness::Permissive, Status::HdlrNotFirst, )?; } match b.head.name { BoxType::HandlerBox => { if read_handler_box { return Status::HdrlBadQuantity.into(); } let HandlerBox { handler_type } = read_hdlr(&mut b, strictness)?; if handler_type != b"pict" { fail_with_status_if( strictness != ParseStrictness::Permissive, Status::HdlrTypeNotPict, )?; } read_handler_box = true; } BoxType::ItemInfoBox => { if item_infos.is_some() { return Status::IinfBadQuantity.into(); } item_infos = Some(read_iinf(&mut b, strictness, unsupported_features)?); } BoxType::ItemLocationBox => { if iloc_items.is_some() { return Status::IlocBadQuantity.into(); } iloc_items = Some(read_iloc(&mut b)?); } BoxType::PrimaryItemBox => { if primary_item_id.is_some() { return Status::PitmBadQuantity.into(); } primary_item_id = Some(read_pitm(&mut b)?); } BoxType::ItemReferenceBox => { if item_references.is_some() { return Status::IrefBadQuantity.into(); } item_references = Some(read_iref(&mut b)?); } BoxType::ItemPropertiesBox => { if item_properties.is_some() { return Status::IprpBadQuantity.into(); } item_properties = Some(read_iprp( &mut b, MIF1_BRAND, strictness, unsupported_features, )?); } BoxType::ItemDataBox => { if item_data_box.is_some() { return Status::IdatBadQuantity.into(); } let data = b.read_into_try_vec()?; item_data_box = Some(DataBox::from_idat(data)); } _ => skip_box_content(&mut b)?, } check_parser_state!(b.content); } Ok(AvifMeta { item_properties: item_properties.unwrap_or_default(), item_references: item_references.unwrap_or_default(), primary_item_id, item_infos: item_infos.unwrap_or_default(), iloc_items: iloc_items.unwrap_or_default(), item_data_box, }) } /// Parse a Primary Item Box /// See ISOBMFF (ISO 14496-12:2020) § 8.11.4 fn read_pitm(src: &mut BMFFBox) -> Result { let version = read_fullbox_version_no_flags(src)?; let item_id = ItemId(match version { 0 => be_u16(src)?.into(), 1 => be_u32(src)?, _ => return Err(Error::Unsupported("unsupported pitm version")), }); Ok(item_id) } /// Parse an Item Information Box /// See ISOBMFF (ISO 14496-12:2020) § 8.11.6 fn read_iinf( src: &mut BMFFBox, strictness: ParseStrictness, unsupported_features: &mut UnsupportedFeatures, ) -> Result> { let version = read_fullbox_version_no_flags(src)?; match version { 0 | 1 => (), _ => return Err(Error::Unsupported("unsupported iinf version")), } let entry_count = if version == 0 { be_u16(src)?.to_usize() } else { be_u32(src)?.to_usize() }; let mut item_infos = TryVec::with_capacity(entry_count)?; let mut iter = src.box_iter(); while let Some(mut b) = iter.next_box()? { if b.head.name != BoxType::ItemInfoEntry { return Status::IinfBadChild.into(); } if let Some(infe) = read_infe(&mut b, strictness, unsupported_features)? { item_infos.push(infe)?; } check_parser_state!(b.content); } Ok(item_infos) } /// A simple wrapper to interpret a u32 as a 4-byte string in big-endian /// order without requiring any allocation. struct U32BE(u32); impl std::fmt::Display for U32BE { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match std::str::from_utf8(&self.0.to_be_bytes()) { Ok(s) => f.write_str(s), Err(_) => write!(f, "{:x?}", self.0), } } } /// Parse an Item Info Entry /// See ISOBMFF (ISO 14496-12:2020) § 8.11.6.2 fn read_infe( src: &mut BMFFBox, strictness: ParseStrictness, unsupported_features: &mut UnsupportedFeatures, ) -> Result> { let (version, flags) = read_fullbox_extra(src)?; // According to the standard, it seems the flags field shall be 0, but at // least one sample AVIF image has a nonzero value. // See https://github.com/AOMediaCodec/av1-avif/issues/146 if flags != 0 { fail_with_status_if( strictness == ParseStrictness::Strict, Status::InfeFlagsNonzero, )?; } // mif1 brand (see HEIF (ISO 23008-12:2017) § 10.2.1) only requires v2 and 3 let item_id = ItemId(match version { 2 => be_u16(src)?.into(), 3 => be_u32(src)?, _ => return Err(Error::Unsupported("unsupported version in 'infe' box")), }); let item_protection_index = be_u16(src)?; let item_type = be_u32(src)?; debug!("infe {:?} item_type: {}", item_id, U32BE(item_type)); // There are some additional fields here, but they're not of interest to us skip_box_remain(src)?; if item_protection_index != 0 { unsupported_features.insert(Feature::Ipro); Ok(None) } else { Ok(Some(ItemInfoEntry { item_id, item_type })) } } /// Parse an Item Reference Box /// See ISOBMFF (ISO 14496-12:2020) § 8.11.12 fn read_iref(src: &mut BMFFBox) -> Result> { let mut item_references = TryVec::new(); let version = read_fullbox_version_no_flags(src)?; if version > 1 { return Err(Error::Unsupported("iref version")); } let mut iter = src.box_iter(); while let Some(mut b) = iter.next_box()? { trace!("read_iref parsing {:?} referenceType", b.head.name); let from_item_id = ItemId::read(&mut b, version)?; let reference_count = be_u16(&mut b)?; item_references.reserve(reference_count.to_usize())?; for _ in 0..reference_count { let to_item_id = ItemId::read(&mut b, version)?; if from_item_id == to_item_id { return Status::IrefRecursion.into(); } item_references.push(SingleItemTypeReferenceBox { item_type: b.head.name.into(), from_item_id, to_item_id, })?; } check_parser_state!(b.content); } trace!("read_iref -> {:#?}", item_references); Ok(item_references) } /// Parse an Item Properties Box /// /// See ISOBMFF (ISO 14496-12:2020) § 8.11.14) /// /// Note: HEIF (ISO 23008-12:2017) § 9.3.1 also defines the `iprp` box and /// related types, but lacks additional requirements specified in 14496-12:2020. /// /// Note: Currently HEIF (ISO 23008-12:2017) § 6.5.5.1 specifies "At most one" /// `colr` box per item, but this is being amended in [DIS 23008-12](https://www.iso.org/standard/83650.html). /// The new text is likely to be "At most one for a given value of `colour_type`", /// so this implementation adheres to that language for forward compatibility. fn read_iprp( src: &mut BMFFBox, brand: FourCC, strictness: ParseStrictness, unsupported_features: &mut UnsupportedFeatures, ) -> Result { let mut iter = src.box_iter(); let properties = match iter.next_box()? { Some(mut b) if b.head.name == BoxType::ItemPropertyContainerBox => { read_ipco(&mut b, strictness) } Some(_) => Status::IprpBadChild.into(), None => Err(Error::UnexpectedEOF), }?; let mut ipma_version_and_flag_values_seen = TryVec::with_capacity(1)?; let mut association_entries = TryVec::::new(); let mut forbidden_items = TryVec::new(); while let Some(mut b) = iter.next_box()? { if b.head.name != BoxType::ItemPropertyAssociationBox { return Status::IprpBadChild.into(); } let (version, flags) = read_fullbox_extra(&mut b)?; if ipma_version_and_flag_values_seen.contains(&(version, flags)) { fail_with_status_if( strictness != ParseStrictness::Permissive, Status::IpmaBadQuantity, )?; } if flags != 0 && properties.len() <= 127 { fail_with_status_if( strictness == ParseStrictness::Strict, Status::IpmaFlagsNonzero, )?; } ipma_version_and_flag_values_seen.push((version, flags))?; for association_entry in read_ipma(&mut b, strictness, version, flags)? { if forbidden_items.contains(&association_entry.item_id) { warn!( "Skipping {:?} since the item referenced shall not be processed", association_entry ); } if let Some(previous_entry) = association_entries .iter() .find(|e| association_entry.item_id == e.item_id) { error!( "Duplicate ipma entries for item_id\n1: {:?}\n2: {:?}", previous_entry, association_entry ); // It's technically possible to make sense of this situation by merging ipma // boxes, but this is a "shall" requirement, so we'd only do it in // ParseStrictness::Permissive mode, and this hasn't shown up in the wild return Status::IpmaDuplicateItemId.into(); } const TRANSFORM_ORDER: &[BoxType] = &[ BoxType::ImageSpatialExtentsProperty, BoxType::CleanApertureBox, BoxType::ImageRotation, BoxType::ImageMirror, ]; let mut prev_transform_index = None; // Realistically, there should only ever be 1 nclx and 1 icc let mut colour_type_indexes: TryHashMap = TryHashMap::with_capacity(2)?; for a in &association_entry.associations { if a.property_index == PropertyIndex(0) { if a.essential { fail_with_status_if( strictness != ParseStrictness::Permissive, Status::IpmaIndexZeroNoEssential, )?; } continue; } if let Some(property) = properties.get(&a.property_index) { assert!(brand == MIF1_BRAND); let feature = Feature::try_from(property); let property_supported = match feature { Ok(feature) => { if feature.supported() { true } else { unsupported_features.insert(feature); false } } Err(_) => false, }; if !property_supported { if a.essential && strictness != ParseStrictness::Permissive { error!("Unsupported essential property {:?}", property); forbidden_items.push(association_entry.item_id)?; } else { debug!( "Ignoring unknown {} property {:?}", if a.essential { "essential" } else { "non-essential" }, property ); } } // Check additional requirements on specific properties match property { ItemProperty::AV1Config(_) | ItemProperty::CleanAperture | ItemProperty::Mirroring(_) | ItemProperty::Rotation(_) => { if !a.essential { warn!("{:?} is missing required 'essential' bit", property); // This is a "shall", but it is likely to change, so only // fail if using strict parsing. // See https://github.com/mozilla/mp4parse-rust/issues/284 fail_with_status_if( strictness == ParseStrictness::Strict, Status::TxformNoEssential, )?; } } // NOTE: this is contrary to the published specification; see doc comment // at the beginning of this function for more details ItemProperty::Colour(colr) => { let colour_type = colr.colour_type(); if let Some(prev_colr_index) = colour_type_indexes.get(&colour_type) { warn!( "Multiple '{}' type colr associations with {:?}: {:?} and {:?}", colour_type, association_entry.item_id, a.property_index, prev_colr_index ); fail_with_status_if( strictness != ParseStrictness::Permissive, Status::ColrBadQuantity, )?; } else { colour_type_indexes.insert(colour_type, a.property_index)?; } } // The following properties are unsupported, but we still enforce that // they've been correctly marked as essential or not. ItemProperty::LayeredImageIndexing => { assert!(feature.is_ok() && unsupported_features.contains(feature?)); if a.essential { fail_with_status_if( strictness != ParseStrictness::Permissive, Status::A1lxEssential, )?; } } ItemProperty::LayerSelection => { assert!(feature.is_ok() && unsupported_features.contains(feature?)); if a.essential { assert!( forbidden_items.contains(&association_entry.item_id) || strictness == ParseStrictness::Permissive ); } else { fail_with_status_if( strictness != ParseStrictness::Permissive, Status::LselNoEssential, )?; } } ItemProperty::OperatingPointSelector => { assert!(feature.is_ok() && unsupported_features.contains(feature?)); if a.essential { assert!( forbidden_items.contains(&association_entry.item_id) || strictness == ParseStrictness::Permissive ); } else { fail_with_status_if( strictness != ParseStrictness::Permissive, Status::A1opNoEssential, )?; } } other_property => { trace!("No additional checks for {:?}", other_property); } } if let Some(transform_index) = TRANSFORM_ORDER .iter() .position(|t| *t == BoxType::from(property)) { if let Some(prev) = prev_transform_index { if prev >= transform_index { error!( "Invalid property order: {:?} after {:?}", TRANSFORM_ORDER[transform_index], TRANSFORM_ORDER[prev] ); fail_with_status_if( strictness != ParseStrictness::Permissive, if TRANSFORM_ORDER[transform_index] == BoxType::ImageSpatialExtentsProperty { Status::TxformBeforeIspe } else { Status::TxformOrder }, )?; } } prev_transform_index = Some(transform_index); } } else { error!( "Missing property at {:?} for {:?}", a.property_index, association_entry.item_id ); fail_with_status_if( strictness != ParseStrictness::Permissive, Status::IpmaBadIndex, )?; } } association_entries.push(association_entry)? } check_parser_state!(b.content); } let iprp = ItemPropertiesBox { properties, association_entries, forbidden_items, }; trace!("read_iprp -> {:#?}", iprp); Ok(iprp) } /// See ISOBMFF (ISO 14496-12:2020) § 8.11.14.1 /// Variants with no associated data are recognized but not necessarily supported. /// See [`Feature`] to determine support. #[derive(Debug)] pub enum ItemProperty { AuxiliaryType(AuxiliaryTypeProperty), AV1Config(AV1ConfigBox), Channels(PixelInformation), CleanAperture, Colour(ColourInformation), ImageSpatialExtents(ImageSpatialExtentsProperty), LayeredImageIndexing, LayerSelection, Mirroring(ImageMirror), OperatingPointSelector, PixelAspectRatio(PixelAspectRatio), Rotation(ImageRotation), /// Necessary to validate property indices in read_iprp Unsupported(BoxType), } impl From<&ItemProperty> for BoxType { fn from(item_property: &ItemProperty) -> Self { match item_property { ItemProperty::AuxiliaryType(_) => BoxType::AuxiliaryTypeProperty, ItemProperty::AV1Config(_) => BoxType::AV1CodecConfigurationBox, ItemProperty::CleanAperture => BoxType::CleanApertureBox, ItemProperty::Colour(_) => BoxType::ColourInformationBox, ItemProperty::LayeredImageIndexing => BoxType::AV1LayeredImageIndexingProperty, ItemProperty::LayerSelection => BoxType::LayerSelectorProperty, ItemProperty::Mirroring(_) => BoxType::ImageMirror, ItemProperty::OperatingPointSelector => BoxType::OperatingPointSelectorProperty, ItemProperty::PixelAspectRatio(_) => BoxType::PixelAspectRatioBox, ItemProperty::Rotation(_) => BoxType::ImageRotation, ItemProperty::ImageSpatialExtents(_) => BoxType::ImageSpatialExtentsProperty, ItemProperty::Channels(_) => BoxType::PixelInformationBox, ItemProperty::Unsupported(box_type) => *box_type, } } } #[derive(Debug)] struct ItemPropertyAssociationEntry { item_id: ItemId, associations: TryVec, } /// For storing ItemPropertyAssociation data /// See ISOBMFF (ISO 14496-12:2020) § 8.11.14.1 #[derive(Debug)] struct Association { essential: bool, property_index: PropertyIndex, } /// See ISOBMFF (ISO 14496-12:2020) § 8.11.14.1 /// /// The properties themselves are stored in `properties`, but the items they're /// associated with are stored in `association_entries`. It's necessary to /// maintain this indirection because multiple items can reference the same /// property. For example, both the primary item and alpha item can share the /// same [`ImageSpatialExtentsProperty`]. #[derive(Debug, Default)] pub struct ItemPropertiesBox { /// `ItemPropertyContainerBox property_container` in the spec properties: TryHashMap, /// `ItemPropertyAssociationBox association[]` in the spec association_entries: TryVec, /// Items that shall not be processed due to unsupported properties that /// have been marked essential. /// See HEIF (ISO/IEC 23008-12:2017) § 9.3.1 forbidden_items: TryVec, } impl ItemPropertiesBox { /// For displayable images `av1C`, `pixi` and `ispe` are mandatory, `colr` /// is typically included too, so we might as well use an even power of 2. const MIN_PROPERTIES: usize = 4; fn is_alpha(&self, item_id: ItemId) -> bool { match self.get(item_id, BoxType::AuxiliaryTypeProperty) { Ok(Some(ItemProperty::AuxiliaryType(urn))) => { urn.aux_type.as_slice() == "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha".as_bytes() } Ok(Some(other_property)) => panic!("property key mismatch: {:?}", other_property), Ok(None) => false, Err(e) => { error!( "is_alpha: Error checking AuxiliaryTypeProperty ({}), returning false", e ); false } } } fn get(&self, item_id: ItemId, property_type: BoxType) -> Result> { match self .get_multiple(item_id, |prop| BoxType::from(prop) == property_type)? .as_slice() { &[] => Ok(None), &[single_value] => Ok(Some(single_value)), multiple_values => { error!( "Multiple values for {:?}: {:?}", property_type, multiple_values ); // TODO: add test Status::IprpConflict.into() } } } fn get_multiple( &self, item_id: ItemId, filter: impl Fn(&ItemProperty) -> bool, ) -> Result> { let mut values = TryVec::new(); for entry in &self.association_entries { for a in &entry.associations { if entry.item_id == item_id { match self.properties.get(&a.property_index) { Some(ItemProperty::Unsupported(_)) => {} Some(property) if filter(property) => values.push(property)?, _ => {} } } } } Ok(values) } } /// An upper bound which can be used to check overflow at compile time trait UpperBounded { const MAX: u64; } /// Implement type $name as a newtype wrapper around an unsigned int which /// implements the UpperBounded trait. macro_rules! impl_bounded { ( $name:ident, $inner:ty ) => { #[derive(Clone, Copy)] pub struct $name($inner); impl $name { pub const fn new(n: $inner) -> Self { Self(n) } #[allow(dead_code)] pub fn get(self) -> $inner { self.0 } } impl UpperBounded for $name { const MAX: u64 = <$inner>::MAX as u64; } }; } /// Implement type $name as a type representing the product of two unsigned ints /// which implements the UpperBounded trait. macro_rules! impl_bounded_product { ( $name:ident, $multiplier:ty, $multiplicand:ty, $inner:ty) => { #[derive(Clone, Copy)] pub struct $name($inner); impl $name { pub fn new(value: $inner) -> Self { assert!(value <= Self::MAX); Self(value) } pub fn get(self) -> $inner { self.0 } } impl UpperBounded for $name { const MAX: u64 = <$multiplier>::MAX * <$multiplicand>::MAX; } }; } mod bounded_uints { use crate::UpperBounded; impl_bounded!(U8, u8); impl_bounded!(U16, u16); impl_bounded!(U32, u32); impl_bounded!(U64, u64); impl_bounded_product!(U32MulU8, U32, U8, u64); impl_bounded_product!(U32MulU16, U32, U16, u64); impl UpperBounded for std::num::NonZeroU8 { const MAX: u64 = u8::MAX as u64; } } use crate::bounded_uints::*; /// Implement the multiplication operator for $lhs * $rhs giving $output, which /// is internally represented as $inner. The operation is statically checked /// to ensure the product won't overflow $inner, nor exceed <$output>::MAX. macro_rules! impl_mul { ( ($lhs:ty , $rhs:ty) => ($output:ty, $inner:ty) ) => { impl std::ops::Mul<$rhs> for $lhs { type Output = $output; fn mul(self, rhs: $rhs) -> Self::Output { static_assertions::const_assert!( <$output as UpperBounded>::MAX <= <$inner>::MAX as u64 ); static_assertions::const_assert!( <$lhs as UpperBounded>::MAX * <$rhs as UpperBounded>::MAX <= <$output as UpperBounded>::MAX ); let lhs: $inner = self.get().into(); let rhs: $inner = rhs.get().into(); Self::Output::new(lhs.checked_mul(rhs).expect("infallible")) } } }; } impl_mul!((U8, std::num::NonZeroU8) => (U16, u16)); impl_mul!((U32, std::num::NonZeroU8) => (U32MulU8, u64)); impl_mul!((U32, U16) => (U32MulU16, u64)); impl std::ops::Add for U32MulU8 { type Output = U64; fn add(self, rhs: U32MulU16) -> Self::Output { static_assertions::const_assert!(U32MulU8::MAX + U32MulU16::MAX < U64::MAX); let lhs: u64 = self.get(); let rhs: u64 = rhs.get(); Self::Output::new(lhs.checked_add(rhs).expect("infallible")) } } const MAX_IPMA_ASSOCIATION_COUNT: U8 = U8::new(u8::MAX); /// After reading only the `entry_count` field of an ipma box, we can check its /// basic validity and calculate (assuming validity) the number of associations /// which will be contained (allowing preallocation of the storage). /// All the arithmetic is compile-time verified to not overflow via supporting /// types implementing the UpperBounded trait. Types are declared explicitly to /// show there isn't any accidental inference to primitive types. /// /// See ISOBMFF (ISO 14496-12:2020) § 8.11.14.1 fn calculate_ipma_total_associations( version: u8, bytes_left: u64, entry_count: U32, num_association_bytes: std::num::NonZeroU8, ) -> Result { let min_entry_bytes = std::num::NonZeroU8::new(1 /* association_count */ + if version == 0 { 2 } else { 4 }) .unwrap(); let total_non_association_bytes: U32MulU8 = entry_count * min_entry_bytes; let total_association_bytes: u64 = if let Some(difference) = bytes_left.checked_sub(total_non_association_bytes.get()) { // All the storage for the `essential` and `property_index` parts (assuming a valid ipma box size) difference } else { return Status::IpmaTooSmall.into(); }; let max_association_bytes_per_entry: U16 = MAX_IPMA_ASSOCIATION_COUNT * num_association_bytes; let max_total_association_bytes: U32MulU16 = entry_count * max_association_bytes_per_entry; let max_bytes_left: U64 = total_non_association_bytes + max_total_association_bytes; if bytes_left > max_bytes_left.get() { return Status::IpmaTooBig.into(); } let total_associations: u64 = total_association_bytes / u64::from(num_association_bytes.get()); Ok(total_associations.try_into()?) } /// Parse an ItemPropertyAssociation box /// /// See ISOBMFF (ISO 14496-12:2020) § 8.11.14.1 fn read_ipma( src: &mut BMFFBox, strictness: ParseStrictness, version: u8, flags: u32, ) -> Result> { let entry_count = be_u32(src)?; let num_association_bytes = std::num::NonZeroU8::new(if flags & 1 == 1 { 2 } else { 1 }).unwrap(); let total_associations = calculate_ipma_total_associations( version, src.bytes_left(), U32::new(entry_count), num_association_bytes, )?; // Assuming most items will have at least `MIN_PROPERTIES` and knowing the // total number of item -> property associations (`total_associations`), // we can provide a good estimate for how many elements we'll need in this // vector, even though we don't know precisely how many items there will be // properties for. let mut entries = TryVec::::with_capacity( total_associations / ItemPropertiesBox::MIN_PROPERTIES, )?; for _ in 0..entry_count { let item_id = ItemId::read(src, version)?; if let Some(previous_association) = entries.last() { #[allow(clippy::comparison_chain)] if previous_association.item_id > item_id { return Status::IpmaBadItemOrder.into(); } else if previous_association.item_id == item_id { return Status::IpmaDuplicateItemId.into(); } } let association_count = src.read_u8()?; let mut associations = TryVec::with_capacity(association_count.to_usize())?; for _ in 0..association_count { let association = src .take(num_association_bytes.get().into()) .read_into_try_vec()?; let mut association = BitReader::new(association.as_slice()); let essential = association.read_bool()?; let property_index = PropertyIndex(association.read_u16(association.remaining().try_into()?)?); associations.push(Association { essential, property_index, })?; } entries.push(ItemPropertyAssociationEntry { item_id, associations, })?; } check_parser_state!(src.content); if version != 0 { if let Some(ItemPropertyAssociationEntry { item_id: max_item_id, .. }) = entries.last() { if *max_item_id <= ItemId(u16::MAX.into()) { fail_with_status_if( strictness == ParseStrictness::Strict, Status::IpmaBadVersion, )?; } } } trace!("read_ipma -> {:#?}", entries); Ok(entries) } /// Parse an ItemPropertyContainerBox /// /// See ISOBMFF (ISO 14496-12:2020) § 8.11.14.1 fn read_ipco( src: &mut BMFFBox, strictness: ParseStrictness, ) -> Result> { let mut properties = TryHashMap::with_capacity(ItemPropertiesBox::MIN_PROPERTIES)?; let mut index = PropertyIndex(1); // ipma uses 1-based indexing let mut iter = src.box_iter(); while let Some(mut b) = iter.next_box()? { let property = match b.head.name { BoxType::AuxiliaryTypeProperty => ItemProperty::AuxiliaryType(read_auxc(&mut b)?), BoxType::AV1CodecConfigurationBox => ItemProperty::AV1Config(read_av1c(&mut b)?), BoxType::ColourInformationBox => ItemProperty::Colour(read_colr(&mut b, strictness)?), BoxType::ImageMirror => ItemProperty::Mirroring(read_imir(&mut b)?), BoxType::ImageRotation => ItemProperty::Rotation(read_irot(&mut b)?), BoxType::ImageSpatialExtentsProperty => { ItemProperty::ImageSpatialExtents(read_ispe(&mut b)?) } BoxType::PixelAspectRatioBox => ItemProperty::PixelAspectRatio(read_pasp(&mut b)?), BoxType::PixelInformationBox => ItemProperty::Channels(read_pixi(&mut b)?), other_box_type => { // Even if we didn't do anything with other property types, we still store // a record at the index to identify invalid indices in ipma boxes skip_box_remain(&mut b)?; let item_property = match other_box_type { BoxType::AV1LayeredImageIndexingProperty => ItemProperty::LayeredImageIndexing, BoxType::CleanApertureBox => ItemProperty::CleanAperture, BoxType::LayerSelectorProperty => ItemProperty::LayerSelection, BoxType::OperatingPointSelectorProperty => ItemProperty::OperatingPointSelector, _ => { warn!("No ItemProperty variant for {:?}", other_box_type); ItemProperty::Unsupported(other_box_type) } }; debug!("Storing empty record {:?}", item_property); item_property } }; properties.insert(index, property)?; index = PropertyIndex( index .0 .checked_add(1) // must include ignored properties to have correct indexes .ok_or_else(|| Error::from(Status::IpcoIndexOverflow))?, ); check_parser_state!(b.content); } Ok(properties) } #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ImageSpatialExtentsProperty { image_width: u32, image_height: u32, } /// Parse image spatial extents property /// /// See HEIF (ISO 23008-12:2017) § 6.5.3.1 fn read_ispe(src: &mut BMFFBox) -> Result { if read_fullbox_version_no_flags(src)? != 0 { return Err(Error::Unsupported("ispe version")); } let image_width = be_u32(src)?; let image_height = be_u32(src)?; Ok(ImageSpatialExtentsProperty { image_width, image_height, }) } #[repr(C)] #[derive(Debug)] pub struct PixelAspectRatio { h_spacing: u32, v_spacing: u32, } /// Parse pixel aspect ratio property /// /// See HEIF (ISO 23008-12:2017) § 6.5.4.1 /// See ISOBMFF (ISO 14496-12:2020) § 12.1.4.2 fn read_pasp(src: &mut BMFFBox) -> Result { let h_spacing = be_u32(src)?; let v_spacing = be_u32(src)?; Ok(PixelAspectRatio { h_spacing, v_spacing, }) } #[derive(Debug)] pub struct PixelInformation { bits_per_channel: TryVec, } /// Parse pixel information /// See HEIF (ISO 23008-12:2017) § 6.5.6 fn read_pixi(src: &mut BMFFBox) -> Result { let version = read_fullbox_version_no_flags(src)?; if version != 0 { return Err(Error::Unsupported("pixi version")); } let num_channels = src.read_u8()?; let mut bits_per_channel = TryVec::with_capacity(num_channels.to_usize())?; let num_channels_read = src.try_read_to_end(&mut bits_per_channel)?; if u8::try_from(num_channels_read)? != num_channels { return Status::PixiBadChannelCount.into(); } check_parser_state!(src.content); Ok(PixelInformation { bits_per_channel }) } /// Despite [Rec. ITU-T H.273] (12/2016) defining the CICP fields as having a /// range of 0-255, and only a small fraction of those values being used, /// ISOBMFF (ISO 14496-12:2020) § 12.1.5 defines them as 16-bit values in the /// `colr` box. Since we have no use for the additional range, and it would /// complicate matters later, we fallibly convert before storing the input. /// /// [Rec. ITU-T H.273]: https://www.itu.int/rec/T-REC-H.273-201612-I/en #[repr(C)] #[derive(Debug)] pub struct NclxColourInformation { colour_primaries: u8, transfer_characteristics: u8, matrix_coefficients: u8, full_range_flag: bool, } /// The raw bytes of the ICC profile #[repr(C)] pub struct IccColourInformation { bytes: TryVec, } impl fmt::Debug for IccColourInformation { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("IccColourInformation") .field("data", &format_args!("{} bytes", self.bytes.len())) .finish() } } #[repr(C)] #[derive(Debug)] pub enum ColourInformation { Nclx(NclxColourInformation), Icc(IccColourInformation, FourCC), } impl ColourInformation { fn colour_type(&self) -> FourCC { match self { Self::Nclx(_) => FourCC::from(*b"nclx"), Self::Icc(_, colour_type) => colour_type.clone(), } } } /// Parse colour information /// See ISOBMFF (ISO 14496-12:2020) § 12.1.5 fn read_colr( src: &mut BMFFBox, strictness: ParseStrictness, ) -> Result { let colour_type = be_u32(src)?.to_be_bytes(); match &colour_type { b"nclx" => { const NUM_RESERVED_BITS: u8 = 7; let colour_primaries = be_u16(src)?.try_into()?; let transfer_characteristics = be_u16(src)?.try_into()?; let matrix_coefficients = be_u16(src)?.try_into()?; let bytes = src.read_into_try_vec()?; let mut bit_reader = BitReader::new(&bytes); let full_range_flag = bit_reader.read_bool()?; if bit_reader.remaining() != NUM_RESERVED_BITS.into() { error!( "read_colr expected {} reserved bits, found {}", NUM_RESERVED_BITS, bit_reader.remaining() ); return Status::ColrBadSize.into(); } if bit_reader.read_u8(NUM_RESERVED_BITS)? != 0 { fail_with_status_if( strictness != ParseStrictness::Permissive, Status::ColrReservedNonzero, )?; } Ok(ColourInformation::Nclx(NclxColourInformation { colour_primaries, transfer_characteristics, matrix_coefficients, full_range_flag, })) } b"rICC" | b"prof" => Ok(ColourInformation::Icc( IccColourInformation { bytes: src.read_into_try_vec()?, }, FourCC::from(colour_type), )), _ => { error!("read_colr colour_type: {:?}", colour_type); Status::ColrBadType.into() } } } #[repr(C)] #[derive(Clone, Copy, Debug)] /// Rotation in the positive (that is, anticlockwise) direction /// Visualized in terms of starting with (⥠) UPWARDS HARPOON WITH BARB LEFT FROM BAR /// similar to a DIGIT ONE (1) pub enum ImageRotation { /// ⥠ UPWARDS HARPOON WITH BARB LEFT FROM BAR D0, /// ⥞ LEFTWARDS HARPOON WITH BARB DOWN FROM BAR D90, /// ⥝ DOWNWARDS HARPOON WITH BARB RIGHT FROM BAR D180, /// ⥛ RIGHTWARDS HARPOON WITH BARB UP FROM BAR D270, } /// Parse image rotation box /// See HEIF (ISO 23008-12:2017) § 6.5.10 fn read_irot(src: &mut BMFFBox) -> Result { let irot = src.read_into_try_vec()?; let mut irot = BitReader::new(&irot); let _reserved = irot.read_u8(6)?; let image_rotation = match irot.read_u8(2)? { 0 => ImageRotation::D0, 1 => ImageRotation::D90, 2 => ImageRotation::D180, 3 => ImageRotation::D270, _ => unreachable!(), }; check_parser_state!(src.content); Ok(image_rotation) } /// The axis about which the image is mirrored (opposite of flip) /// Visualized in terms of starting with (⥠) UPWARDS HARPOON WITH BARB LEFT FROM BAR /// similar to a DIGIT ONE (1) #[repr(C)] #[derive(Debug)] pub enum ImageMirror { /// top and bottom parts exchanged /// ⥡ DOWNWARDS HARPOON WITH BARB LEFT FROM BAR TopBottom, /// left and right parts exchanged /// ⥜ UPWARDS HARPOON WITH BARB RIGHT FROM BAR LeftRight, } /// Parse image mirroring box /// See HEIF (ISO 23008-12:2017) § 6.5.12
/// Note: [ISO/IEC 23008-12:2017/DAmd 2](https://www.iso.org/standard/81688.html) /// reverses the interpretation of the 'imir' box in § 6.5.12.3: /// > `axis` specifies a vertical (`axis` = 0) or horizontal (`axis` = 1) axis /// > for the mirroring operation. /// /// is replaced with: /// > `mode` specifies how the mirroring is performed: 0 indicates that the top /// > and bottom parts of the image are exchanged; 1 specifies that the left and /// > right parts are exchanged. /// > /// > NOTE: In Exif, orientation tag can be used to signal mirroring operations. /// > Exif orientation tag 4 corresponds to `mode` = 0 of `ImageMirror`, and /// > Exif orientation tag 2 corresponds to `mode` = 1 accordingly. /// /// This implementation conforms to the text in Draft Amendment 2, which is the /// opposite of the published standard as of 4 June 2021. fn read_imir(src: &mut BMFFBox) -> Result { let imir = src.read_into_try_vec()?; let mut imir = BitReader::new(&imir); let _reserved = imir.read_u8(7)?; let image_mirror = match imir.read_u8(1)? { 0 => ImageMirror::TopBottom, 1 => ImageMirror::LeftRight, _ => unreachable!(), }; check_parser_state!(src.content); Ok(image_mirror) } /// See HEIF (ISO 23008-12:2017) § 6.5.8 #[derive(Debug, PartialEq)] pub struct AuxiliaryTypeProperty { aux_type: TryString, aux_subtype: TryString, } /// Parse image properties for auxiliary images /// See HEIF (ISO 23008-12:2017) § 6.5.8 fn read_auxc(src: &mut BMFFBox) -> Result { let version = read_fullbox_version_no_flags(src)?; if version != 0 { return Err(Error::Unsupported("auxC version")); } let mut aux = TryString::new(); src.try_read_to_end(&mut aux)?; let (aux_type, aux_subtype): (TryString, TryVec); if let Some(nul_byte_pos) = aux.iter().position(|&b| b == b'\0') { let (a, b) = aux.as_slice().split_at(nul_byte_pos); aux_type = a.try_into()?; aux_subtype = (b[1..]).try_into()?; } else { aux_type = aux; aux_subtype = TryVec::new(); } Ok(AuxiliaryTypeProperty { aux_type, aux_subtype, }) } /// Parse an item location box inside a meta box /// See ISOBMFF (ISO 14496-12:2020) § 8.11.3 fn read_iloc(src: &mut BMFFBox) -> Result> { let version: IlocVersion = read_fullbox_version_no_flags(src)?.try_into()?; let iloc = src.read_into_try_vec()?; let mut iloc = BitReader::new(&iloc); let offset_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?; let length_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?; let base_offset_size: IlocFieldSize = iloc.read_u8(4)?.try_into()?; let index_size: Option = match version { IlocVersion::One | IlocVersion::Two => Some(iloc.read_u8(4)?.try_into()?), IlocVersion::Zero => { let _reserved = iloc.read_u8(4)?; None } }; let item_count = match version { IlocVersion::Zero | IlocVersion::One => iloc.read_u32(16)?, IlocVersion::Two => iloc.read_u32(32)?, }; let mut items = TryHashMap::with_capacity(item_count.to_usize())?; for _ in 0..item_count { let item_id = ItemId(match version { IlocVersion::Zero | IlocVersion::One => iloc.read_u32(16)?, IlocVersion::Two => iloc.read_u32(32)?, }); // The spec isn't entirely clear how an `iloc` should be interpreted for version 0, // which has no `construction_method` field. It does say: // "For maximum compatibility, version 0 of this box should be used in preference to // version 1 with `construction_method==0`, or version 2 when possible." // We take this to imply version 0 can be interpreted as using file offsets. let construction_method = match version { IlocVersion::Zero => ConstructionMethod::File, IlocVersion::One | IlocVersion::Two => { let _reserved = iloc.read_u16(12)?; match iloc.read_u16(4)? { 0 => ConstructionMethod::File, 1 => ConstructionMethod::Idat, 2 => ConstructionMethod::Item, _ => return Status::IlocBadConstructionMethod.into(), } } }; let data_reference_index = iloc.read_u16(16)?; if data_reference_index != 0 { return Err(Error::Unsupported( "external file references (iloc.data_reference_index != 0) are not supported", )); } let base_offset = iloc.read_u64(base_offset_size.as_bits())?; let extent_count = iloc.read_u16(16)?; if extent_count < 1 { return Status::IlocBadExtentCount.into(); } // "If only one extent is used (extent_count = 1) then either or both of the // offset and length may be implied" if extent_count != 1 && (offset_size == IlocFieldSize::Zero || length_size == IlocFieldSize::Zero) { return Status::IlocBadExtent.into(); } let mut extents = TryVec::with_capacity(extent_count.to_usize())?; for _ in 0..extent_count { // Parsed but currently ignored, see `Extent` let _extent_index = match &index_size { None | Some(IlocFieldSize::Zero) => None, Some(index_size) => { debug_assert!(version == IlocVersion::One || version == IlocVersion::Two); Some(iloc.read_u64(index_size.as_bits())?) } }; // Per ISOBMFF (ISO 14496-12:2020) § 8.11.3.1: // "If the offset is not identified (the field has a length of zero), then the // beginning of the source (offset 0) is implied" // This behavior will follow from BitReader::read_u64(0) -> 0. let extent_offset = iloc.read_u64(offset_size.as_bits())?; let extent_length = iloc.read_u64(length_size.as_bits())?.try_into()?; // "If the length is not specified, or specified as zero, then the entire length of // the source is implied" (ibid) let offset = base_offset .checked_add(extent_offset) .ok_or_else(|| Error::from(Status::IlocOffsetOverflow))?; let extent = if extent_length == 0 { Extent::ToEnd { offset } } else { Extent::WithLength { offset, len: extent_length, } }; extents.push(extent)?; } let loc = ItemLocationBoxItem { construction_method, extents, }; if items.insert(item_id, loc)?.is_some() { return Status::IlocDuplicateItemId.into(); } } if iloc.remaining() == 0 { Ok(items) } else { Status::IlocBadSize.into() } } /// Read the contents of a box, including sub boxes. pub fn read_mp4(f: &mut T) -> Result { let mut context = None; let mut found_ftyp = false; // TODO(kinetik): Top-level parsing should handle zero-sized boxes // rather than throwing an error. let mut iter = BoxIter::new(f); while let Some(mut b) = iter.next_box()? { // box ordering: ftyp before any variable length box (inc. moov), // but may not be first box in file if file signatures etc. present // fragmented mp4 order: ftyp, moov, pairs of moof/mdat (1-multiple), mfra // "special": uuid, wide (= 8 bytes) // isom: moov, mdat, free, skip, udta, ftyp, moof, mfra // iso2: pdin, meta // iso3: meco // iso5: styp, sidx, ssix, prft // unknown, maybe: id32 // qt: pnot // possibly allow anything where all printable and/or all lowercase printable // "four printable characters from the ISO 8859-1 character set" match b.head.name { BoxType::FileTypeBox => { let ftyp = read_ftyp(&mut b)?; found_ftyp = true; debug!("{:?}", ftyp); } BoxType::MovieBox => { context = Some(read_moov(&mut b, context)?); } #[cfg(feature = "meta-xml")] BoxType::MetadataBox => { if let Some(ctx) = &mut context { ctx.metadata = Some(read_meta(&mut b)); } } _ => skip_box_content(&mut b)?, }; check_parser_state!(b.content); if context.is_some() { debug!( "found moov {}, could stop pure 'moov' parser now", if found_ftyp { "and ftyp" } else { "but no ftyp" } ); } } // XXX(kinetik): This isn't perfect, as a "moov" with no contents is // treated as okay but we haven't found anything useful. Needs more // thought for clearer behaviour here. context.ok_or(Error::MoovMissing) } /// Parse a Movie Header Box /// See ISOBMFF (ISO 14496-12:2020) § 8.2.2 fn parse_mvhd(f: &mut BMFFBox) -> Result> { let mvhd = read_mvhd(f)?; debug!("{:?}", mvhd); if mvhd.timescale == 0 { return Status::MvhdBadTimescale.into(); } let timescale = Some(MediaTimeScale(u64::from(mvhd.timescale))); Ok(timescale) } /// Parse a Movie Box /// See ISOBMFF (ISO 14496-12:2020) § 8.2.1 /// Note that despite the spec indicating "exactly one" moov box should exist at /// the file container level, we support reading and merging multiple moov boxes /// such as with tests/test_case_1185230.mp4. fn read_moov(f: &mut BMFFBox, context: Option) -> Result { let MediaContext { mut timescale, mut tracks, mut mvex, mut psshs, mut userdata, #[cfg(feature = "meta-xml")] metadata, } = context.unwrap_or_default(); let mut iter = f.box_iter(); while let Some(mut b) = iter.next_box()? { match b.head.name { BoxType::MovieHeaderBox => { timescale = parse_mvhd(&mut b)?; } BoxType::TrackBox => { let mut track = Track::new(tracks.len()); read_trak(&mut b, &mut track)?; tracks.push(track)?; } BoxType::MovieExtendsBox => { mvex = Some(read_mvex(&mut b)?); debug!("{:?}", mvex); } BoxType::ProtectionSystemSpecificHeaderBox => { let pssh = read_pssh(&mut b)?; debug!("{:?}", pssh); psshs.push(pssh)?; } BoxType::UserdataBox => { userdata = Some(read_udta(&mut b)); debug!("{:?}", userdata); if let Some(Err(_)) = userdata { // There was an error parsing userdata. Such failures are not fatal to overall // parsing, just skip the rest of the box. skip_box_remain(&mut b)?; } } _ => skip_box_content(&mut b)?, }; check_parser_state!(b.content); } Ok(MediaContext { timescale, tracks, mvex, psshs, userdata, #[cfg(feature = "meta-xml")] metadata, }) } fn read_pssh(src: &mut BMFFBox) -> Result { let len = src.bytes_left(); let mut box_content = read_buf(src, len)?; let (system_id, kid, data) = { let pssh = &mut Cursor::new(&box_content); let (version, _) = read_fullbox_extra(pssh)?; let system_id = read_buf(pssh, 16)?; let mut kid = TryVec::::new(); if version > 0 { const KID_ELEMENT_SIZE: usize = 16; let count = be_u32(pssh)?.to_usize(); kid.reserve( count .checked_mul(KID_ELEMENT_SIZE) .ok_or_else(|| Error::from(Status::PsshSizeOverflow))?, )?; for _ in 0..count { let item = read_buf(pssh, KID_ELEMENT_SIZE.to_u64())?; kid.push(item)?; } } let data_size = be_u32(pssh)?; let data = read_buf(pssh, data_size.into())?; (system_id, kid, data) }; let mut pssh_box = TryVec::new(); write_be_u32(&mut pssh_box, src.head.size.try_into()?)?; pssh_box.extend_from_slice(b"pssh")?; pssh_box.append(&mut box_content)?; Ok(ProtectionSystemSpecificHeaderBox { system_id, kid, data, box_content: pssh_box, }) } /// Parse a Movie Extends Box /// See ISOBMFF (ISO 14496-12:2020) § 8.8.1 fn read_mvex(src: &mut BMFFBox) -> Result { let mut iter = src.box_iter(); let mut fragment_duration = None; while let Some(mut b) = iter.next_box()? { match b.head.name { BoxType::MovieExtendsHeaderBox => { let duration = read_mehd(&mut b)?; fragment_duration = Some(duration); } _ => skip_box_content(&mut b)?, } } Ok(MovieExtendsBox { fragment_duration }) } fn read_mehd(src: &mut BMFFBox) -> Result { let (version, _) = read_fullbox_extra(src)?; let fragment_duration = match version { 1 => be_u64(src)?, 0 => u64::from(be_u32(src)?), _ => return Status::MehdBadVersion.into(), }; Ok(MediaScaledTime(fragment_duration)) } /// Parse a Track Box /// See ISOBMFF (ISO 14496-12:2020) § 8.3.1. fn read_trak(f: &mut BMFFBox, track: &mut Track) -> Result<()> { let mut iter = f.box_iter(); while let Some(mut b) = iter.next_box()? { match b.head.name { BoxType::TrackHeaderBox => { let tkhd = read_tkhd(&mut b)?; track.track_id = Some(tkhd.track_id); track.tkhd = Some(tkhd.clone()); debug!("{:?}", tkhd); } BoxType::EditBox => read_edts(&mut b, track)?, BoxType::MediaBox => read_mdia(&mut b, track)?, BoxType::TrackReferenceBox => track.tref = Some(read_tref(&mut b)?), _ => skip_box_content(&mut b)?, }; check_parser_state!(b.content); } Ok(()) } fn read_edts(f: &mut BMFFBox, track: &mut Track) -> Result<()> { let mut iter = f.box_iter(); while let Some(mut b) = iter.next_box()? { match b.head.name { BoxType::EditListBox => { let elst = read_elst(&mut b)?; track.looped = Some(elst.looped); if elst.edits.is_empty() { debug!("empty edit list"); continue; } let mut empty_duration = 0; let mut idx = 0; if elst.edits[idx].media_time == -1 { if elst.edits.len() < 2 { debug!("expected additional edit, ignoring edit list"); continue; } empty_duration = elst.edits[idx].segment_duration; idx += 1; } track.empty_duration = Some(MediaScaledTime(empty_duration)); let media_time = elst.edits[idx].media_time; if media_time < 0 { debug!("unexpected negative media time in edit"); } track.edited_duration = Some(MediaScaledTime(elst.edits[idx].segment_duration)); track.media_time = Some(TrackScaledTime::( std::cmp::max(0, media_time) as u64, track.id, )); if elst.edits.len() > 2 { debug!("ignoring edit list with {} entries", elst.edits.len()); } debug!("{:?}", elst); } _ => skip_box_content(&mut b)?, }; check_parser_state!(b.content); } Ok(()) } #[allow(clippy::type_complexity)] // Allow the complex return, maybe rework in future fn parse_mdhd( f: &mut BMFFBox, track: &mut Track, ) -> Result<( MediaHeaderBox, Option>, Option>, )> { let mdhd = read_mdhd(f)?; let duration = match mdhd.duration { std::u64::MAX => None, duration => Some(TrackScaledTime::(duration, track.id)), }; if mdhd.timescale == 0 { return Status::MdhdBadTimescale.into(); } let timescale = Some(TrackTimeScale::(u64::from(mdhd.timescale), track.id)); Ok((mdhd, duration, timescale)) } fn read_mdia(f: &mut BMFFBox, track: &mut Track) -> Result<()> { let mut iter = f.box_iter(); while let Some(mut b) = iter.next_box()? { match b.head.name { BoxType::MediaHeaderBox => { let (mdhd, duration, timescale) = parse_mdhd(&mut b, track)?; track.duration = duration; track.timescale = timescale; debug!("{:?}", mdhd); } BoxType::HandlerBox => { let hdlr = read_hdlr(&mut b, ParseStrictness::Permissive)?; match hdlr.handler_type.value.as_ref() { b"vide" => track.track_type = TrackType::Video, b"pict" => track.track_type = TrackType::Picture, b"auxv" => track.track_type = TrackType::AuxiliaryVideo, b"soun" => track.track_type = TrackType::Audio, b"meta" => track.track_type = TrackType::Metadata, _ => (), } debug!("{:?}", hdlr); } BoxType::MediaInformationBox => read_minf(&mut b, track)?, _ => skip_box_content(&mut b)?, }; check_parser_state!(b.content); } Ok(()) } fn read_tref(f: &mut BMFFBox) -> Result { // Will likely only see trefs with one auxl let mut references = TryVec::with_capacity(1)?; let mut iter = f.box_iter(); while let Some(mut b) = iter.next_box()? { match b.head.name { BoxType::AuxiliaryBox => { references.push(TrackReferenceEntry::Auxiliary(read_tref_auxl(&mut b)?))? } _ => skip_box_content(&mut b)?, }; check_parser_state!(b.content); } Ok(TrackReferenceBox { references }) } fn read_tref_auxl(f: &mut BMFFBox) -> Result { let num_track_ids = (f.bytes_left() / std::mem::size_of::().to_u64()).try_into()?; let mut track_ids = TryVec::with_capacity(num_track_ids)?; for _ in 0..num_track_ids { track_ids.push(be_u32(f)?)?; } Ok(TrackReference { track_ids }) } fn read_minf(f: &mut BMFFBox, track: &mut Track) -> Result<()> { let mut iter = f.box_iter(); while let Some(mut b) = iter.next_box()? { match b.head.name { BoxType::SampleTableBox => read_stbl(&mut b, track)?, _ => skip_box_content(&mut b)?, }; check_parser_state!(b.content); } Ok(()) } fn read_stbl(f: &mut BMFFBox, track: &mut Track) -> Result<()> { let mut iter = f.box_iter(); while let Some(mut b) = iter.next_box()? { match b.head.name { BoxType::SampleDescriptionBox => { let stsd = read_stsd(&mut b, track)?; debug!("{:?}", stsd); track.stsd = Some(stsd); } BoxType::TimeToSampleBox => { let stts = read_stts(&mut b)?; debug!("{:?}", stts); track.stts = Some(stts); } BoxType::SampleToChunkBox => { let stsc = read_stsc(&mut b)?; debug!("{:?}", stsc); track.stsc = Some(stsc); } BoxType::SampleSizeBox => { let stsz = read_stsz(&mut b)?; debug!("{:?}", stsz); track.stsz = Some(stsz); } BoxType::ChunkOffsetBox => { let stco = read_stco(&mut b)?; debug!("{:?}", stco); track.stco = Some(stco); } BoxType::ChunkLargeOffsetBox => { let co64 = read_co64(&mut b)?; debug!("{:?}", co64); track.stco = Some(co64); } BoxType::SyncSampleBox => { let stss = read_stss(&mut b)?; debug!("{:?}", stss); track.stss = Some(stss); } BoxType::CompositionOffsetBox => { let ctts = read_ctts(&mut b)?; debug!("{:?}", ctts); track.ctts = Some(ctts); } _ => skip_box_content(&mut b)?, }; check_parser_state!(b.content); } Ok(()) } /// Parse an ftyp box. /// See ISOBMFF (ISO 14496-12:2020) § 4.3 fn read_ftyp(src: &mut BMFFBox) -> Result { let major = be_u32(src)?; let minor = be_u32(src)?; let bytes_left = src.bytes_left(); if bytes_left % 4 != 0 { return Status::FtypBadSize.into(); } // Is a brand_count of zero valid? let brand_count = bytes_left / 4; let mut brands = TryVec::with_capacity(brand_count.try_into()?)?; for _ in 0..brand_count { brands.push(be_u32(src)?.into())?; } Ok(FileTypeBox { major_brand: From::from(major), minor_version: minor, compatible_brands: brands, }) } /// Parse an mvhd box. fn read_mvhd(src: &mut BMFFBox) -> Result { let (version, _) = read_fullbox_extra(src)?; match version { // 64 bit creation and modification times. 1 => { skip(src, 16)?; } // 32 bit creation and modification times. 0 => { skip(src, 8)?; } _ => return Status::MvhdBadVersion.into(), } let timescale = be_u32(src)?; let duration = match version { 1 => be_u64(src)?, 0 => { let d = be_u32(src)?; if d == std::u32::MAX { std::u64::MAX } else { u64::from(d) } } _ => unreachable!("Should have returned Status::MvhdBadVersion"), }; // Skip remaining fields. skip(src, 80)?; Ok(MovieHeaderBox { timescale, duration, }) } /// Parse a tkhd box. fn read_tkhd(src: &mut BMFFBox) -> Result { let (version, flags) = read_fullbox_extra(src)?; let disabled = flags & 0x1u32 == 0 || flags & 0x2u32 == 0; match version { // 64 bit creation and modification times. 1 => { skip(src, 16)?; } // 32 bit creation and modification times. 0 => { skip(src, 8)?; } _ => return Status::TkhdBadVersion.into(), } let track_id = be_u32(src)?; skip(src, 4)?; let duration = match version { 1 => be_u64(src)?, 0 => u64::from(be_u32(src)?), _ => unreachable!("Should have returned Status::TkhdBadVersion"), }; // Skip uninteresting fields. skip(src, 16)?; let matrix = Matrix { a: be_i32(src)?, b: be_i32(src)?, u: be_i32(src)?, c: be_i32(src)?, d: be_i32(src)?, v: be_i32(src)?, x: be_i32(src)?, y: be_i32(src)?, w: be_i32(src)?, }; let width = be_u32(src)?; let height = be_u32(src)?; Ok(TrackHeaderBox { track_id, disabled, duration, width, height, matrix, }) } /// Parse a elst box. /// See ISOBMFF (ISO 14496-12:2020) § 8.6.6 fn read_elst(src: &mut BMFFBox) -> Result { let (version, flags) = read_fullbox_extra(src)?; let edit_count = be_u32(src)?; let mut edits = TryVec::with_capacity(edit_count.to_usize())?; for _ in 0..edit_count { let (segment_duration, media_time) = match version { 1 => { // 64 bit segment duration and media times. (be_u64(src)?, be_i64(src)?) } 0 => { // 32 bit segment duration and media times. (u64::from(be_u32(src)?), i64::from(be_i32(src)?)) } _ => return Status::ElstBadVersion.into(), }; let media_rate_integer = be_i16(src)?; let media_rate_fraction = be_i16(src)?; edits.push(Edit { segment_duration, media_time, media_rate_integer, media_rate_fraction, })?; } // Padding could be added in some contents. skip_box_remain(src)?; Ok(EditListBox { looped: flags == 1, edits, }) } /// Parse a mdhd box. fn read_mdhd(src: &mut BMFFBox) -> Result { let (version, _) = read_fullbox_extra(src)?; let (timescale, duration) = match version { 1 => { // Skip 64-bit creation and modification times. skip(src, 16)?; // 64 bit duration. (be_u32(src)?, be_u64(src)?) } 0 => { // Skip 32-bit creation and modification times. skip(src, 8)?; // 32 bit duration. let timescale = be_u32(src)?; let duration = { // Since we convert the 32-bit duration to 64-bit by // upcasting, we need to preserve the special all-1s // ("unknown") case by hand. let d = be_u32(src)?; if d == std::u32::MAX { std::u64::MAX } else { u64::from(d) } }; (timescale, duration) } _ => return Status::MdhdBadVersion.into(), }; // Skip uninteresting fields. skip(src, 4)?; Ok(MediaHeaderBox { timescale, duration, }) } /// Parse a stco box. /// See ISOBMFF (ISO 14496-12:2020) § 8.7.5 fn read_stco(src: &mut BMFFBox) -> Result { let (_, _) = read_fullbox_extra(src)?; let offset_count = be_u32(src)?; let mut offsets = TryVec::with_capacity(offset_count.to_usize())?; for _ in 0..offset_count { offsets.push(be_u32(src)?.into())?; } // Padding could be added in some contents. skip_box_remain(src)?; Ok(ChunkOffsetBox { offsets }) } /// Parse a co64 box. /// See ISOBMFF (ISO 14496-12:2020) § 8.7.5 fn read_co64(src: &mut BMFFBox) -> Result { let (_, _) = read_fullbox_extra(src)?; let offset_count = be_u32(src)?; let mut offsets = TryVec::with_capacity(offset_count.to_usize())?; for _ in 0..offset_count { offsets.push(be_u64(src)?)?; } // Padding could be added in some contents. skip_box_remain(src)?; Ok(ChunkOffsetBox { offsets }) } /// Parse a stss box. /// See ISOBMFF (ISO 14496-12:2020) § 8.6.2 fn read_stss(src: &mut BMFFBox) -> Result { let (_, _) = read_fullbox_extra(src)?; let sample_count = be_u32(src)?; let mut samples = TryVec::with_capacity(sample_count.to_usize())?; for _ in 0..sample_count { samples.push(be_u32(src)?)?; } // Padding could be added in some contents. skip_box_remain(src)?; Ok(SyncSampleBox { samples }) } /// Parse a stsc box. /// See ISOBMFF (ISO 14496-12:2020) § 8.7.4 fn read_stsc(src: &mut BMFFBox) -> Result { let (_, _) = read_fullbox_extra(src)?; let sample_count = be_u32(src)?; let mut samples = TryVec::with_capacity(sample_count.to_usize())?; for _ in 0..sample_count { let first_chunk = be_u32(src)?; let samples_per_chunk = be_u32(src)?; let sample_description_index = be_u32(src)?; samples.push(SampleToChunk { first_chunk, samples_per_chunk, sample_description_index, })?; } // Padding could be added in some contents. skip_box_remain(src)?; Ok(SampleToChunkBox { samples }) } /// Parse a Composition Time to Sample Box /// See ISOBMFF (ISO 14496-12:2020) § 8.6.1.3 fn read_ctts(src: &mut BMFFBox) -> Result { let (version, _) = read_fullbox_extra(src)?; let counts = be_u32(src)?; if counts .checked_mul(8) .map_or(true, |bytes| u64::from(bytes) > src.bytes_left()) { return Status::CttsBadSize.into(); } let mut offsets = TryVec::with_capacity(counts.to_usize())?; for _ in 0..counts { let (sample_count, time_offset) = match version { // According to spec, Version0 shoule be used when version == 0; // however, some buggy contents have negative value when version == 0. // So we always use Version1 here. 0..=1 => { let count = be_u32(src)?; let offset = TimeOffsetVersion::Version1(be_i32(src)?); (count, offset) } _ => { return Status::CttsBadVersion.into(); } }; offsets.push(TimeOffset { sample_count, time_offset, })?; } check_parser_state!(src.content); Ok(CompositionOffsetBox { samples: offsets }) } /// Parse a stsz box. /// See ISOBMFF (ISO 14496-12:2020) § 8.7.3.2 fn read_stsz(src: &mut BMFFBox) -> Result { let (_, _) = read_fullbox_extra(src)?; let sample_size = be_u32(src)?; let sample_count = be_u32(src)?; let mut sample_sizes = TryVec::new(); if sample_size == 0 { sample_sizes.reserve(sample_count.to_usize())?; for _ in 0..sample_count { sample_sizes.push(be_u32(src)?)?; } } // Padding could be added in some contents. skip_box_remain(src)?; Ok(SampleSizeBox { sample_size, sample_sizes, }) } /// Parse a stts box. /// See ISOBMFF (ISO 14496-12:2020) § 8.6.1.2 fn read_stts(src: &mut BMFFBox) -> Result { let (_, _) = read_fullbox_extra(src)?; let sample_count = be_u32(src)?; let mut samples = TryVec::with_capacity(sample_count.to_usize())?; for _ in 0..sample_count { let sample_count = be_u32(src)?; let sample_delta = be_u32(src)?; samples.push(Sample { sample_count, sample_delta, })?; } // Padding could be added in some contents. skip_box_remain(src)?; Ok(TimeToSampleBox { samples }) } /// Parse a VPx Config Box. fn read_vpcc(src: &mut BMFFBox) -> Result { let (version, _) = read_fullbox_extra(src)?; let supported_versions = [0, 1]; if !supported_versions.contains(&version) { return Err(Error::Unsupported("unknown vpcC version")); } let profile = src.read_u8()?; let level = src.read_u8()?; let ( bit_depth, colour_primaries, chroma_subsampling, transfer_characteristics, matrix_coefficients, video_full_range_flag, ) = if version == 0 { let (bit_depth, colour_primaries) = { let byte = src.read_u8()?; ((byte >> 4) & 0x0f, byte & 0x0f) }; // Note, transfer_characteristics was known as transfer_function in v0 let (chroma_subsampling, transfer_characteristics, video_full_range_flag) = { let byte = src.read_u8()?; ((byte >> 4) & 0x0f, (byte >> 1) & 0x07, (byte & 1) == 1) }; ( bit_depth, colour_primaries, chroma_subsampling, transfer_characteristics, None, video_full_range_flag, ) } else { let (bit_depth, chroma_subsampling, video_full_range_flag) = { let byte = src.read_u8()?; ((byte >> 4) & 0x0f, (byte >> 1) & 0x07, (byte & 1) == 1) }; let colour_primaries = src.read_u8()?; let transfer_characteristics = src.read_u8()?; let matrix_coefficients = src.read_u8()?; ( bit_depth, colour_primaries, chroma_subsampling, transfer_characteristics, Some(matrix_coefficients), video_full_range_flag, ) }; let codec_init_size = be_u16(src)?; let codec_init = read_buf(src, codec_init_size.into())?; // TODO(rillian): validate field value ranges. Ok(VPxConfigBox { profile, level, bit_depth, colour_primaries, chroma_subsampling, transfer_characteristics, matrix_coefficients, video_full_range_flag, codec_init, }) } /// See [AV1-ISOBMFF § 2.3.3](https://aomediacodec.github.io/av1-isobmff/#av1codecconfigurationbox-syntax) fn read_av1c(src: &mut BMFFBox) -> Result { // We want to store the raw config as well as a structured (parsed) config, so create a copy of // the raw config so we have it later, and then parse the structured data from that. let raw_config = src.read_into_try_vec()?; let mut raw_config_slice = raw_config.as_slice(); let marker_byte = raw_config_slice.read_u8()?; if marker_byte & 0x80 != 0x80 { return Err(Error::Unsupported("missing av1C marker bit")); } if marker_byte & 0x7f != 0x01 { return Err(Error::Unsupported("missing av1C marker bit")); } let profile_byte = raw_config_slice.read_u8()?; let profile = (profile_byte & 0xe0) >> 5; let level = profile_byte & 0x1f; let flags_byte = raw_config_slice.read_u8()?; let tier = (flags_byte & 0x80) >> 7; let bit_depth = match flags_byte & 0x60 { 0x60 => 12, 0x40 => 10, _ => 8, }; let monochrome = flags_byte & 0x10 == 0x10; let chroma_subsampling_x = (flags_byte & 0x08) >> 3; let chroma_subsampling_y = (flags_byte & 0x04) >> 2; let chroma_sample_position = flags_byte & 0x03; let delay_byte = raw_config_slice.read_u8()?; let initial_presentation_delay_present = (delay_byte & 0x10) == 0x10; let initial_presentation_delay_minus_one = if initial_presentation_delay_present { delay_byte & 0x0f } else { 0 }; Ok(AV1ConfigBox { profile, level, tier, bit_depth, monochrome, chroma_subsampling_x, chroma_subsampling_y, chroma_sample_position, initial_presentation_delay_present, initial_presentation_delay_minus_one, raw_config, }) } fn read_flac_metadata(src: &mut BMFFBox) -> Result { let temp = src.read_u8()?; let block_type = temp & 0x7f; let length = be_u24(src)?.into(); if length > src.bytes_left() { return Status::DflaBadMetadataBlockSize.into(); } let data = read_buf(src, length)?; Ok(FLACMetadataBlock { block_type, data }) } /// See MPEG-4 Systems (ISO 14496-1:2010) § 7.2.6.5 fn find_descriptor(data: &[u8], esds: &mut ES_Descriptor) -> Result<()> { // Tags for elementary stream description const ESDESCR_TAG: u8 = 0x03; const DECODER_CONFIG_TAG: u8 = 0x04; const DECODER_SPECIFIC_TAG: u8 = 0x05; let mut remains = data; // Descriptor length should be more than 2 bytes. while remains.len() > 2 { let des = &mut Cursor::new(remains); let tag = des.read_u8()?; // See MPEG-4 Systems (ISO 14496-1:2010) § 8.3.3 for interpreting size of expandable classes let mut end: u32 = 0; // It's u8 without declaration type that is incorrect. // MSB of extend_or_len indicates more bytes, up to 4 bytes. for _ in 0..4 { if des.position() == remains.len().to_u64() { // There's nothing more to read, the 0x80 was actually part of // the content, and not an extension size. end = des.position() as u32; break; } let extend_or_len = des.read_u8()?; end = (end << 7) + u32::from(extend_or_len & 0x7F); if (extend_or_len & 0b1000_0000) == 0 { end += des.position() as u32; break; } } if end.to_usize() > remains.len() || u64::from(end) < des.position() { return Status::EsdsBadDescriptor.into(); } let descriptor = &remains[des.position().try_into()?..end.to_usize()]; match tag { ESDESCR_TAG => { read_es_descriptor(descriptor, esds)?; } DECODER_CONFIG_TAG => { read_dc_descriptor(descriptor, esds)?; } DECODER_SPECIFIC_TAG => { read_ds_descriptor(descriptor, esds)?; } _ => { debug!("Unsupported descriptor, tag {}", tag); } } remains = &remains[end.to_usize()..remains.len()]; debug!("remains.len(): {}", remains.len()); } Ok(()) } fn get_audio_object_type(bit_reader: &mut BitReader) -> Result { let mut audio_object_type: u16 = ReadInto::read(bit_reader, 5)?; // Extend audio object type, for example, HE-AAC. if audio_object_type == 31 { let audio_object_type_ext: u16 = ReadInto::read(bit_reader, 6)?; audio_object_type = 32 + audio_object_type_ext; } Ok(audio_object_type) } /// See MPEG-4 Systems (ISO 14496-1:2010) § 7.2.6.7 and probably 14496-3 somewhere? fn read_ds_descriptor(data: &[u8], esds: &mut ES_Descriptor) -> Result<()> { #[cfg(feature = "mp4v")] // Check if we are in a Visual esda Box. if esds.video_codec != CodecType::Unknown { esds.decoder_specific_data.extend_from_slice(data)?; return Ok(()); } // We are in an Audio esda Box. let frequency_table = vec![ (0x0, 96000), (0x1, 88200), (0x2, 64000), (0x3, 48000), (0x4, 44100), (0x5, 32000), (0x6, 24000), (0x7, 22050), (0x8, 16000), (0x9, 12000), (0xa, 11025), (0xb, 8000), (0xc, 7350), ]; let bit_reader = &mut BitReader::new(data); let mut audio_object_type = get_audio_object_type(bit_reader)?; let sample_index: u32 = ReadInto::read(bit_reader, 4)?; // Sample frequency could be from table, or retrieved from stream directly // if index is 0x0f. let sample_frequency = match sample_index { 0x0F => Some(ReadInto::read(bit_reader, 24)?), _ => frequency_table .iter() .find(|item| item.0 == sample_index) .map(|x| x.1), }; let channel_configuration: u16 = ReadInto::read(bit_reader, 4)?; let extended_audio_object_type = match audio_object_type { 5 | 29 => Some(5), _ => None, }; if audio_object_type == 5 || audio_object_type == 29 { // We have an explicit signaling for BSAC extension, should the decoder // decode the BSAC extension (all Gecko's AAC decoders do), then this is // what the stream will actually look like once decoded. let _extended_sample_index = ReadInto::read(bit_reader, 4)?; let _extended_sample_frequency: Option = match _extended_sample_index { 0x0F => Some(ReadInto::read(bit_reader, 24)?), _ => frequency_table .iter() .find(|item| item.0 == sample_index) .map(|x| x.1), }; audio_object_type = get_audio_object_type(bit_reader)?; let _extended_channel_configuration = match audio_object_type { 22 => ReadInto::read(bit_reader, 4)?, _ => channel_configuration, }; }; match audio_object_type { 1..=4 | 6 | 7 | 17 | 19..=23 => { if sample_frequency.is_none() { return Err(Error::Unsupported("unknown frequency")); } // parsing GASpecificConfig // If the sampling rate is not one of the rates listed in the right // column in Table 4.82, the sampling frequency dependent tables // (code tables, scale factor band tables etc.) must be deduced in // order for the bitstream payload to be parsed. Since a given // sampling frequency is associated with only one sampling frequency // table, and since maximum flexibility is desired in the range of // possible sampling frequencies, the following table shall be used // to associate an implied sampling frequency with the desired // sampling frequency dependent tables. let sample_frequency_value = match sample_frequency.unwrap() { 0..=9390 => 8000, 9391..=11501 => 11025, 11502..=13855 => 12000, 13856..=18782 => 16000, 18783..=23003 => 22050, 23004..=27712 => 24000, 27713..=37565 => 32000, 37566..=46008 => 44100, 46009..=55425 => 48000, 55426..=75131 => 64000, 75132..=92016 => 88200, _ => 96000, }; bit_reader.skip(1)?; // frameLengthFlag let depend_on_core_order: u8 = ReadInto::read(bit_reader, 1)?; if depend_on_core_order > 0 { bit_reader.skip(14)?; // codeCoderDelay } bit_reader.skip(1)?; // extensionFlag let channel_counts = match channel_configuration { 0 => { debug!("Parsing program_config_element for channel counts"); bit_reader.skip(4)?; // element_instance_tag bit_reader.skip(2)?; // object_type bit_reader.skip(4)?; // sampling_frequency_index let num_front_channel: u8 = ReadInto::read(bit_reader, 4)?; let num_side_channel: u8 = ReadInto::read(bit_reader, 4)?; let num_back_channel: u8 = ReadInto::read(bit_reader, 4)?; let num_lfe_channel: u8 = ReadInto::read(bit_reader, 2)?; bit_reader.skip(3)?; // num_assoc_data bit_reader.skip(4)?; // num_valid_cc let mono_mixdown_present: bool = ReadInto::read(bit_reader, 1)?; if mono_mixdown_present { bit_reader.skip(4)?; // mono_mixdown_element_number } let stereo_mixdown_present: bool = ReadInto::read(bit_reader, 1)?; if stereo_mixdown_present { bit_reader.skip(4)?; // stereo_mixdown_element_number } let matrix_mixdown_idx_present: bool = ReadInto::read(bit_reader, 1)?; if matrix_mixdown_idx_present { bit_reader.skip(2)?; // matrix_mixdown_idx bit_reader.skip(1)?; // pseudo_surround_enable } let mut _channel_counts = 0; _channel_counts += read_surround_channel_count(bit_reader, num_front_channel)?; _channel_counts += read_surround_channel_count(bit_reader, num_side_channel)?; _channel_counts += read_surround_channel_count(bit_reader, num_back_channel)?; _channel_counts += read_surround_channel_count(bit_reader, num_lfe_channel)?; _channel_counts } 1..=7 => channel_configuration, // Amendment 4 of the AAC standard in 2013 below 11 => 7, // 6.1 Amendment 4 of the AAC standard in 2013 12 | 14 => 8, // 7.1 (a/d) of ITU BS.2159 _ => { return Err(Error::Unsupported("invalid channel configuration")); } }; esds.audio_object_type = Some(audio_object_type); esds.extended_audio_object_type = extended_audio_object_type; esds.audio_sample_rate = Some(sample_frequency_value); esds.audio_channel_count = Some(channel_counts); if !esds.decoder_specific_data.is_empty() { return Status::EsdsDecSpecificIntoTagQuantity.into(); } esds.decoder_specific_data.extend_from_slice(data)?; Ok(()) } _ => Err(Error::Unsupported("unknown aac audio object type")), } } fn read_surround_channel_count(bit_reader: &mut BitReader, channels: u8) -> Result { let mut count = 0; for _ in 0..channels { let is_cpe: bool = ReadInto::read(bit_reader, 1)?; count += if is_cpe { 2 } else { 1 }; bit_reader.skip(4)?; } Ok(count) } /// See MPEG-4 Systems (ISO 14496-1:2010) § 7.2.6.6 fn read_dc_descriptor(data: &[u8], esds: &mut ES_Descriptor) -> Result<()> { let des = &mut Cursor::new(data); let object_profile = des.read_u8()?; #[cfg(feature = "mp4v")] { esds.video_codec = match object_profile { 0x20..=0x24 => CodecType::MP4V, _ => CodecType::Unknown, }; } // Skip uninteresting fields. skip(des, 12)?; if data.len().to_u64() > des.position() { find_descriptor(&data[des.position().try_into()?..data.len()], esds)?; } esds.audio_codec = match object_profile { 0x40 | 0x66 | 0x67 => CodecType::AAC, 0x69 | 0x6B => CodecType::MP3, _ => CodecType::Unknown, }; debug!( "read_dc_descriptor: esds.audio_codec = {:?}", esds.audio_codec ); Ok(()) } /// See MPEG-4 Systems (ISO 14496-1:2010) § 7.2.6.5 fn read_es_descriptor(data: &[u8], esds: &mut ES_Descriptor) -> Result<()> { let des = &mut Cursor::new(data); skip(des, 2)?; let esds_flags = des.read_u8()?; // Stream dependency flag, first bit from left most. if esds_flags & 0x80 > 0 { // Skip uninteresting fields. skip(des, 2)?; } // Url flag, second bit from left most. if esds_flags & 0x40 > 0 { // Skip uninteresting fields. let skip_es_len = u64::from(des.read_u8()?) + 2; skip(des, skip_es_len)?; } if data.len().to_u64() > des.position() { find_descriptor(&data[des.position().try_into()?..data.len()], esds)?; } Ok(()) } /// See MP4 (ISO 14496-14:2020) § 6.7.2 fn read_esds(src: &mut BMFFBox) -> Result { let (_, _) = read_fullbox_extra(src)?; let esds_array = read_buf(src, src.bytes_left())?; let mut es_data = ES_Descriptor::default(); find_descriptor(&esds_array, &mut es_data)?; es_data.codec_esds = esds_array; Ok(es_data) } /// Parse `FLACSpecificBox`. /// See [Encapsulation of FLAC in ISO Base Media File Format](https://github.com/xiph/flac/blob/master/doc/isoflac.txt) § 3.3.2 fn read_dfla(src: &mut BMFFBox) -> Result { let (version, flags) = read_fullbox_extra(src)?; if version != 0 { return Err(Error::Unsupported("unknown dfLa (FLAC) version")); } if flags != 0 { return Status::DflaFlagsNonzero.into(); } let mut blocks = TryVec::new(); while src.bytes_left() > 0 { let block = read_flac_metadata(src)?; blocks.push(block)?; } // The box must have at least one meta block, and the first block // must be the METADATA_BLOCK_STREAMINFO if blocks.is_empty() { return Status::DflaMissingMetadata.into(); } else if blocks[0].block_type != 0 { return Status::DflaStreamInfoNotFirst.into(); } else if blocks[0].data.len() != 34 { return Status::DflaStreamInfoBadSize.into(); } Ok(FLACSpecificBox { version, blocks }) } /// Parse `OpusSpecificBox`. fn read_dops(src: &mut BMFFBox) -> Result { let version = src.read_u8()?; if version != 0 { return Err(Error::Unsupported("unknown dOps (Opus) version")); } let output_channel_count = src.read_u8()?; let pre_skip = be_u16(src)?; let input_sample_rate = be_u32(src)?; let output_gain = be_i16(src)?; let channel_mapping_family = src.read_u8()?; let channel_mapping_table = if channel_mapping_family == 0 { None } else { let stream_count = src.read_u8()?; let coupled_count = src.read_u8()?; let channel_mapping = read_buf(src, output_channel_count.into())?; Some(ChannelMappingTable { stream_count, coupled_count, channel_mapping, }) }; // TODO(kinetik): validate field value ranges. Ok(OpusSpecificBox { version, output_channel_count, pre_skip, input_sample_rate, output_gain, channel_mapping_family, channel_mapping_table, }) } /// Re-serialize the Opus codec-specific config data as an `OpusHead` packet. /// /// Some decoders expect the initialization data in the format used by the /// Ogg and WebM encapsulations. To support this we prepend the `OpusHead` /// tag and byte-swap the data from big- to little-endian relative to the /// dOps box. pub fn serialize_opus_header( opus: &OpusSpecificBox, dst: &mut W, ) -> Result<()> { match dst.write(b"OpusHead") { Err(e) => return Err(Error::from(e)), Ok(bytes) => { if bytes != 8 { return Status::DopsOpusHeadWriteErr.into(); } } } // In mp4 encapsulation, the version field is 0, but in ogg // it is 1. While decoders generally accept zero as well, write // out the version of the header we're supporting rather than // whatever we parsed out of mp4. dst.write_u8(1)?; dst.write_u8(opus.output_channel_count)?; dst.write_u16::(opus.pre_skip)?; dst.write_u32::(opus.input_sample_rate)?; dst.write_i16::(opus.output_gain)?; dst.write_u8(opus.channel_mapping_family)?; match opus.channel_mapping_table { None => {} Some(ref table) => { dst.write_u8(table.stream_count)?; dst.write_u8(table.coupled_count)?; match dst.write(&table.channel_mapping) { Err(e) => return Err(Error::from(e)), Ok(bytes) => { if bytes != table.channel_mapping.len() { return Status::DopsChannelMappingWriteErr.into(); } } } } }; Ok(()) } /// Parse `ALACSpecificBox`. fn read_alac(src: &mut BMFFBox) -> Result { let (version, flags) = read_fullbox_extra(src)?; if version != 0 { return Err(Error::Unsupported("unknown alac (ALAC) version")); } if flags != 0 { return Status::AlacFlagsNonzero.into(); } let length = match src.bytes_left() { x @ 24 | x @ 48 => x, _ => { return Status::AlacBadMagicCookieSize.into(); } }; let data = read_buf(src, length)?; Ok(ALACSpecificBox { version, data }) } /// Parse a Handler Reference Box.
/// See ISOBMFF (ISO 14496-12:2020) § 8.4.3
/// See [\[ISOBMFF\]: reserved (field = 0;) handling is ambiguous](https://github.com/MPEGGroup/FileFormat/issues/36) fn read_hdlr(src: &mut BMFFBox, strictness: ParseStrictness) -> Result { if read_fullbox_version_no_flags(src)? != 0 { return Status::HdlrUnsupportedVersion.into(); } let pre_defined = be_u32(src)?; if pre_defined != 0 { fail_with_status_if( strictness == ParseStrictness::Strict, Status::HdlrPredefinedNonzero, )?; } let handler_type = FourCC::from(be_u32(src)?); for _ in 1..=3 { let reserved = be_u32(src)?; if reserved != 0 { fail_with_status_if( strictness == ParseStrictness::Strict, Status::HdlrReservedNonzero, )?; } } match std::str::from_utf8(src.read_into_try_vec()?.as_slice()) { Ok(name) => { match name.bytes().position(|b| b == b'\0') { None => fail_with_status_if( strictness != ParseStrictness::Permissive, Status::HdlrNameNoNul, )?, // `name` must be nul-terminated and any trailing bytes after the first nul ignored. // See https://github.com/MPEGGroup/FileFormat/issues/35 Some(_) => (), } } Err(_) => fail_with_status_if( strictness != ParseStrictness::Permissive, Status::HdlrNameNotUtf8, )?, } Ok(HandlerBox { handler_type }) } /// Parse an video description inside an stsd box. fn read_video_sample_entry(src: &mut BMFFBox) -> Result { let name = src.get_header().name; let codec_type = match name { BoxType::AVCSampleEntry | BoxType::AVC3SampleEntry => CodecType::H264, BoxType::MP4VideoSampleEntry => CodecType::MP4V, BoxType::VP8SampleEntry => CodecType::VP8, BoxType::VP9SampleEntry => CodecType::VP9, BoxType::AV1SampleEntry => CodecType::AV1, BoxType::ProtectedVisualSampleEntry => CodecType::EncryptedVideo, BoxType::H263SampleEntry => CodecType::H263, _ => { debug!("Unsupported video codec, box {:?} found", name); CodecType::Unknown } }; // Skip uninteresting fields. skip(src, 6)?; let data_reference_index = be_u16(src)?; // Skip uninteresting fields. skip(src, 16)?; let width = be_u16(src)?; let height = be_u16(src)?; // Skip uninteresting fields. skip(src, 50)?; // Skip clap/pasp/etc. for now. let mut codec_specific = None; let mut protection_info = TryVec::new(); let mut iter = src.box_iter(); while let Some(mut b) = iter.next_box()? { match b.head.name { BoxType::AVCConfigurationBox => { if (name != BoxType::AVCSampleEntry && name != BoxType::AVC3SampleEntry && name != BoxType::ProtectedVisualSampleEntry) || codec_specific.is_some() { return Status::StsdBadVideoSampleEntry.into(); } let avcc_size = b .head .size .checked_sub(b.head.offset) .expect("offset invalid"); let avcc = read_buf(&mut b.content, avcc_size)?; debug!("{:?} (avcc)", avcc); // TODO(kinetik): Parse avcC box? For now we just stash the data. codec_specific = Some(VideoCodecSpecific::AVCConfig(avcc)); } BoxType::H263SpecificBox => { if (name != BoxType::H263SampleEntry) || codec_specific.is_some() { return Status::StsdBadVideoSampleEntry.into(); } let h263_dec_spec_struc_size = b .head .size .checked_sub(b.head.offset) .expect("offset invalid"); let h263_dec_spec_struc = read_buf(&mut b.content, h263_dec_spec_struc_size)?; debug!("{:?} (h263DecSpecStruc)", h263_dec_spec_struc); codec_specific = Some(VideoCodecSpecific::H263Config(h263_dec_spec_struc)); } BoxType::VPCodecConfigurationBox => { // vpcC if (name != BoxType::VP8SampleEntry && name != BoxType::VP9SampleEntry && name != BoxType::ProtectedVisualSampleEntry) || codec_specific.is_some() { return Status::StsdBadVideoSampleEntry.into(); } let vpcc = read_vpcc(&mut b)?; codec_specific = Some(VideoCodecSpecific::VPxConfig(vpcc)); } BoxType::AV1CodecConfigurationBox => { if name != BoxType::AV1SampleEntry && name != BoxType::ProtectedVisualSampleEntry { return Status::StsdBadVideoSampleEntry.into(); } let av1c = read_av1c(&mut b)?; codec_specific = Some(VideoCodecSpecific::AV1Config(av1c)); } BoxType::ESDBox => { if name != BoxType::MP4VideoSampleEntry || codec_specific.is_some() { return Status::StsdBadVideoSampleEntry.into(); } #[cfg(not(feature = "mp4v"))] { let (_, _) = read_fullbox_extra(&mut b.content)?; // Subtract 4 extra to offset the members of fullbox not // accounted for in head.offset let esds_size = b .head .size .checked_sub(b.head.offset + 4) .expect("offset invalid"); let esds = read_buf(&mut b.content, esds_size)?; codec_specific = Some(VideoCodecSpecific::ESDSConfig(esds)); } #[cfg(feature = "mp4v")] { // Read ES_Descriptor inside an esds box. // See ISOBMFF (ISO 14496-1:2010) § 7.2.6.5 let esds = read_esds(&mut b)?; codec_specific = Some(VideoCodecSpecific::ESDSConfig(esds.decoder_specific_data)); } } BoxType::ProtectionSchemeInfoBox => { if name != BoxType::ProtectedVisualSampleEntry { return Status::StsdBadVideoSampleEntry.into(); } let sinf = read_sinf(&mut b)?; debug!("{:?} (sinf)", sinf); protection_info.push(sinf)?; } _ => { debug!("Unsupported video codec, box {:?} found", b.head.name); skip_box_content(&mut b)?; } } check_parser_state!(b.content); } Ok( codec_specific.map_or(SampleEntry::Unknown, |codec_specific| { SampleEntry::Video(VideoSampleEntry { codec_type, data_reference_index, width, height, codec_specific, protection_info, }) }), ) } fn read_qt_wave_atom(src: &mut BMFFBox) -> Result { let mut codec_specific = None; let mut iter = src.box_iter(); while let Some(mut b) = iter.next_box()? { match b.head.name { BoxType::ESDBox => { let esds = read_esds(&mut b)?; codec_specific = Some(esds); } _ => skip_box_content(&mut b)?, } } codec_specific.ok_or_else(|| Error::from(Status::EsdsBadAudioSampleEntry)) } /// Parse an audio description inside an stsd box. /// See ISOBMFF (ISO 14496-12:2020) § 12.2.3 fn read_audio_sample_entry(src: &mut BMFFBox) -> Result { let name = src.get_header().name; // Skip uninteresting fields. skip(src, 6)?; let data_reference_index = be_u16(src)?; // XXX(kinetik): This is "reserved" in BMFF, but some old QT MOV variant // uses it, need to work out if we have to support it. Without checking // here and reading extra fields after samplerate (or bailing with an // error), the parser loses sync completely. let version = be_u16(src)?; // Skip uninteresting fields. skip(src, 6)?; let mut channelcount = u32::from(be_u16(src)?); let samplesize = be_u16(src)?; // Skip uninteresting fields. skip(src, 4)?; let mut samplerate = f64::from(be_u32(src)? >> 16); // 16.16 fixed point; match version { 0 => (), 1 => { // Quicktime sound sample description version 1. // Skip uninteresting fields. skip(src, 16)?; } 2 => { // Quicktime sound sample description version 2. skip(src, 4)?; samplerate = f64::from_bits(be_u64(src)?); channelcount = be_u32(src)?; skip(src, 20)?; } _ => { return Err(Error::Unsupported( "unsupported non-isom audio sample entry", )) } } let (mut codec_type, mut codec_specific) = match name { BoxType::MP3AudioSampleEntry => (CodecType::MP3, Some(AudioCodecSpecific::MP3)), BoxType::LPCMAudioSampleEntry => (CodecType::LPCM, Some(AudioCodecSpecific::LPCM)), // Some mp4 file with AMR doesn't have AMRSpecificBox "damr" in followed while loop, // we use empty box by default. #[cfg(feature = "3gpp")] BoxType::AMRNBSampleEntry => ( CodecType::AMRNB, Some(AudioCodecSpecific::AMRSpecificBox(Default::default())), ), #[cfg(feature = "3gpp")] BoxType::AMRWBSampleEntry => ( CodecType::AMRWB, Some(AudioCodecSpecific::AMRSpecificBox(Default::default())), ), _ => (CodecType::Unknown, None), }; let mut protection_info = TryVec::new(); let mut iter = src.box_iter(); while let Some(mut b) = iter.next_box()? { match b.head.name { BoxType::ESDBox => { if (name != BoxType::MP4AudioSampleEntry && name != BoxType::ProtectedAudioSampleEntry) || codec_specific.is_some() { return Status::StsdBadAudioSampleEntry.into(); } let esds = read_esds(&mut b)?; codec_type = esds.audio_codec; codec_specific = Some(AudioCodecSpecific::ES_Descriptor(esds)); } BoxType::FLACSpecificBox => { if (name != BoxType::FLACSampleEntry && name != BoxType::ProtectedAudioSampleEntry) || codec_specific.is_some() { return Status::StsdBadAudioSampleEntry.into(); } let dfla = read_dfla(&mut b)?; codec_type = CodecType::FLAC; codec_specific = Some(AudioCodecSpecific::FLACSpecificBox(dfla)); } BoxType::OpusSpecificBox => { if (name != BoxType::OpusSampleEntry && name != BoxType::ProtectedAudioSampleEntry) || codec_specific.is_some() { return Status::StsdBadAudioSampleEntry.into(); } let dops = read_dops(&mut b)?; codec_type = CodecType::Opus; codec_specific = Some(AudioCodecSpecific::OpusSpecificBox(dops)); } BoxType::ALACSpecificBox => { if name != BoxType::ALACSpecificBox || codec_specific.is_some() { return Status::StsdBadAudioSampleEntry.into(); } let alac = read_alac(&mut b)?; codec_type = CodecType::ALAC; codec_specific = Some(AudioCodecSpecific::ALACSpecificBox(alac)); } BoxType::QTWaveAtom => { let qt_esds = read_qt_wave_atom(&mut b)?; codec_type = qt_esds.audio_codec; codec_specific = Some(AudioCodecSpecific::ES_Descriptor(qt_esds)); } BoxType::ProtectionSchemeInfoBox => { if name != BoxType::ProtectedAudioSampleEntry { return Status::StsdBadAudioSampleEntry.into(); } let sinf = read_sinf(&mut b)?; debug!("{:?} (sinf)", sinf); codec_type = CodecType::EncryptedAudio; protection_info.push(sinf)?; } #[cfg(feature = "3gpp")] BoxType::AMRSpecificBox => { if codec_type != CodecType::AMRNB && codec_type != CodecType::AMRWB { return Status::StsdBadAudioSampleEntry.into(); } let amr_dec_spec_struc_size = b .head .size .checked_sub(b.head.offset) .expect("offset invalid"); let amr_dec_spec_struc = read_buf(&mut b.content, amr_dec_spec_struc_size)?; debug!("{:?} (AMRDecSpecStruc)", amr_dec_spec_struc); codec_specific = Some(AudioCodecSpecific::AMRSpecificBox(amr_dec_spec_struc)); } _ => { debug!("Unsupported audio codec, box {:?} found", b.head.name); skip_box_content(&mut b)?; } } check_parser_state!(b.content); } Ok( codec_specific.map_or(SampleEntry::Unknown, |codec_specific| { SampleEntry::Audio(AudioSampleEntry { codec_type, data_reference_index, channelcount, samplesize, samplerate, codec_specific, protection_info, }) }), ) } /// Parse a stsd box. /// See ISOBMFF (ISO 14496-12:2020) § 8.5.2 /// See MP4 (ISO 14496-14:2020) § 6.7.2 fn read_stsd(src: &mut BMFFBox, track: &mut Track) -> Result { let (_, flags) = read_fullbox_extra(src)?; if flags != 0 { warn!( "Unexpected `flags` value for SampleDescriptionBox (stsd): {}", flags ); } let description_count = be_u32(src)?.to_usize(); let mut descriptions = TryVec::with_capacity(description_count)?; let mut iter = src.box_iter(); while descriptions.len() < description_count { if let Some(mut b) = iter.next_box()? { let description = match track.track_type { TrackType::Video => read_video_sample_entry(&mut b), TrackType::Picture => read_video_sample_entry(&mut b), TrackType::AuxiliaryVideo => read_video_sample_entry(&mut b), TrackType::Audio => read_audio_sample_entry(&mut b), TrackType::Metadata => Err(Error::Unsupported("metadata track")), TrackType::Unknown => Err(Error::Unsupported("unknown track type")), }; let description = match description { Ok(desc) => desc, Err(Error::Unsupported(_)) => { // read_{audio,video}_desc may have returned Unsupported // after partially reading the box content, so we can't // simply use skip_box_content here. let to_skip = b.bytes_left(); skip(&mut b, to_skip)?; SampleEntry::Unknown } Err(e) => return Err(e), }; descriptions.push(description)?; check_parser_state!(b.content); } else { break; } } // Padding could be added in some contents. skip_box_remain(src)?; Ok(SampleDescriptionBox { descriptions }) } fn read_sinf(src: &mut BMFFBox) -> Result { let mut sinf = ProtectionSchemeInfoBox::default(); let mut iter = src.box_iter(); while let Some(mut b) = iter.next_box()? { match b.head.name { BoxType::OriginalFormatBox => { sinf.original_format = FourCC::from(be_u32(&mut b)?); } BoxType::SchemeTypeBox => { sinf.scheme_type = Some(read_schm(&mut b)?); } BoxType::SchemeInformationBox => { // We only need tenc box in schi box so far. sinf.tenc = read_schi(&mut b)?; } _ => skip_box_content(&mut b)?, } check_parser_state!(b.content); } Ok(sinf) } fn read_schi(src: &mut BMFFBox) -> Result> { let mut tenc = None; let mut iter = src.box_iter(); while let Some(mut b) = iter.next_box()? { match b.head.name { BoxType::TrackEncryptionBox => { if tenc.is_some() { return Status::SchiQuantity.into(); } tenc = Some(read_tenc(&mut b)?); } _ => skip_box_content(&mut b)?, } } Ok(tenc) } fn read_tenc(src: &mut BMFFBox) -> Result { let (version, _) = read_fullbox_extra(src)?; // reserved byte skip(src, 1)?; // the next byte is used to signal the default pattern in version >= 1 let (default_crypt_byte_block, default_skip_byte_block) = match version { 0 => { skip(src, 1)?; (None, None) } _ => { let pattern_byte = src.read_u8()?; let crypt_bytes = pattern_byte >> 4; let skip_bytes = pattern_byte & 0x0f; (Some(crypt_bytes), Some(skip_bytes)) } }; let default_is_encrypted = src.read_u8()?; let default_iv_size = src.read_u8()?; let default_kid = read_buf(src, 16)?; // If default_is_encrypted == 1 && default_iv_size == 0 we expect a default_constant_iv let default_constant_iv = match (default_is_encrypted, default_iv_size) { (1, 0) => { let default_constant_iv_size = src.read_u8()?; Some(read_buf(src, default_constant_iv_size.into())?) } _ => None, }; Ok(TrackEncryptionBox { is_encrypted: default_is_encrypted, iv_size: default_iv_size, kid: default_kid, crypt_byte_block_count: default_crypt_byte_block, skip_byte_block_count: default_skip_byte_block, constant_iv: default_constant_iv, }) } fn read_schm(src: &mut BMFFBox) -> Result { // Flags can be used to signal presence of URI in the box, but we don't // use the URI so don't bother storing the flags. let (_, _) = read_fullbox_extra(src)?; let scheme_type = FourCC::from(be_u32(src)?); let scheme_version = be_u32(src)?; // Null terminated scheme URI may follow, but we don't use it right now. skip_box_remain(src)?; Ok(SchemeTypeBox { scheme_type, scheme_version, }) } /// Parse a metadata box inside a moov, trak, or mdia box. /// See ISOBMFF (ISO 14496-12:2020) § 8.10.1. fn read_udta(src: &mut BMFFBox) -> Result { let mut iter = src.box_iter(); let mut udta = UserdataBox { meta: None }; while let Some(mut b) = iter.next_box()? { match b.head.name { BoxType::MetadataBox => { let meta = read_meta(&mut b)?; udta.meta = Some(meta); } _ => skip_box_content(&mut b)?, }; check_parser_state!(b.content); } Ok(udta) } /// Parse the meta box /// See ISOBMFF (ISO 14496-12:2020) § 8.11.1 fn read_meta(src: &mut BMFFBox) -> Result { let (_, _) = read_fullbox_extra(src)?; let mut iter = src.box_iter(); let mut meta = MetadataBox::default(); while let Some(mut b) = iter.next_box()? { match b.head.name { BoxType::MetadataItemListEntry => read_ilst(&mut b, &mut meta)?, #[cfg(feature = "meta-xml")] BoxType::MetadataXMLBox => read_xml_(&mut b, &mut meta)?, #[cfg(feature = "meta-xml")] BoxType::MetadataBXMLBox => read_bxml(&mut b, &mut meta)?, _ => skip_box_content(&mut b)?, }; check_parser_state!(b.content); } Ok(meta) } /// Parse a XML box inside a meta box /// See ISOBMFF (ISO 14496-12:2020) § 8.11.2 #[cfg(feature = "meta-xml")] fn read_xml_(src: &mut BMFFBox, meta: &mut MetadataBox) -> Result<()> { if read_fullbox_version_no_flags(src)? != 0 { return Err(Error::Unsupported("unsupported XmlBox version")); } meta.xml = Some(XmlBox::StringXmlBox(src.read_into_try_vec()?)); Ok(()) } /// Parse a Binary XML box inside a meta box /// See ISOBMFF (ISO 14496-12:2020) § 8.11.2 #[cfg(feature = "meta-xml")] fn read_bxml(src: &mut BMFFBox, meta: &mut MetadataBox) -> Result<()> { if read_fullbox_version_no_flags(src)? != 0 { return Err(Error::Unsupported("unsupported XmlBox version")); } meta.xml = Some(XmlBox::BinaryXmlBox(src.read_into_try_vec()?)); Ok(()) } /// Parse a metadata box inside a udta box fn read_ilst(src: &mut BMFFBox, meta: &mut MetadataBox) -> Result<()> { let mut iter = src.box_iter(); while let Some(mut b) = iter.next_box()? { match b.head.name { BoxType::AlbumEntry => meta.album = read_ilst_string_data(&mut b)?, BoxType::ArtistEntry | BoxType::ArtistLowercaseEntry => { meta.artist = read_ilst_string_data(&mut b)? } BoxType::AlbumArtistEntry => meta.album_artist = read_ilst_string_data(&mut b)?, BoxType::CommentEntry => meta.comment = read_ilst_string_data(&mut b)?, BoxType::DateEntry => meta.year = read_ilst_string_data(&mut b)?, BoxType::TitleEntry => meta.title = read_ilst_string_data(&mut b)?, BoxType::CustomGenreEntry => { meta.genre = read_ilst_string_data(&mut b)?.map(Genre::CustomGenre) } BoxType::StandardGenreEntry => { meta.genre = read_ilst_u8_data(&mut b)? .and_then(|gnre| Some(Genre::StandardGenre(gnre.get(1).copied()?))) } BoxType::ComposerEntry => meta.composer = read_ilst_string_data(&mut b)?, BoxType::EncoderEntry => meta.encoder = read_ilst_string_data(&mut b)?, BoxType::EncodedByEntry => meta.encoded_by = read_ilst_string_data(&mut b)?, BoxType::CopyrightEntry => meta.copyright = read_ilst_string_data(&mut b)?, BoxType::GroupingEntry => meta.grouping = read_ilst_string_data(&mut b)?, BoxType::CategoryEntry => meta.category = read_ilst_string_data(&mut b)?, BoxType::KeywordEntry => meta.keyword = read_ilst_string_data(&mut b)?, BoxType::PodcastUrlEntry => meta.podcast_url = read_ilst_string_data(&mut b)?, BoxType::PodcastGuidEntry => meta.podcast_guid = read_ilst_string_data(&mut b)?, BoxType::DescriptionEntry => meta.description = read_ilst_string_data(&mut b)?, BoxType::LongDescriptionEntry => meta.long_description = read_ilst_string_data(&mut b)?, BoxType::LyricsEntry => meta.lyrics = read_ilst_string_data(&mut b)?, BoxType::TVNetworkNameEntry => meta.tv_network_name = read_ilst_string_data(&mut b)?, BoxType::TVEpisodeNameEntry => meta.tv_episode_name = read_ilst_string_data(&mut b)?, BoxType::TVShowNameEntry => meta.tv_show_name = read_ilst_string_data(&mut b)?, BoxType::PurchaseDateEntry => meta.purchase_date = read_ilst_string_data(&mut b)?, BoxType::RatingEntry => meta.rating = read_ilst_string_data(&mut b)?, BoxType::OwnerEntry => meta.owner = read_ilst_string_data(&mut b)?, BoxType::HDVideoEntry => meta.hd_video = read_ilst_bool_data(&mut b)?, BoxType::SortNameEntry => meta.sort_name = read_ilst_string_data(&mut b)?, BoxType::SortArtistEntry => meta.sort_artist = read_ilst_string_data(&mut b)?, BoxType::SortAlbumEntry => meta.sort_album = read_ilst_string_data(&mut b)?, BoxType::SortAlbumArtistEntry => { meta.sort_album_artist = read_ilst_string_data(&mut b)? } BoxType::SortComposerEntry => meta.sort_composer = read_ilst_string_data(&mut b)?, BoxType::TrackNumberEntry => { if let Some(trkn) = read_ilst_u8_data(&mut b)? { meta.track_number = trkn.get(3).copied(); meta.total_tracks = trkn.get(5).copied(); }; } BoxType::DiskNumberEntry => { if let Some(disk) = read_ilst_u8_data(&mut b)? { meta.disc_number = disk.get(3).copied(); meta.total_discs = disk.get(5).copied(); }; } BoxType::TempoEntry => { meta.beats_per_minute = read_ilst_u8_data(&mut b)?.and_then(|tmpo| tmpo.get(1).copied()) } BoxType::CompilationEntry => meta.compilation = read_ilst_bool_data(&mut b)?, BoxType::AdvisoryEntry => { meta.advisory = read_ilst_u8_data(&mut b)?.and_then(|rtng| { Some(match rtng.first()? { 2 => AdvisoryRating::Clean, 0 => AdvisoryRating::Inoffensive, r => AdvisoryRating::Explicit(*r), }) }) } BoxType::MediaTypeEntry => { meta.media_type = read_ilst_u8_data(&mut b)?.and_then(|stik| { Some(match stik.first()? { 0 => MediaType::Movie, 1 => MediaType::Normal, 2 => MediaType::AudioBook, 5 => MediaType::WhackedBookmark, 6 => MediaType::MusicVideo, 9 => MediaType::ShortFilm, 10 => MediaType::TVShow, 11 => MediaType::Booklet, s => MediaType::Unknown(*s), }) }) } BoxType::PodcastEntry => meta.podcast = read_ilst_bool_data(&mut b)?, BoxType::TVSeasonNumberEntry => { meta.tv_season = read_ilst_u8_data(&mut b)?.and_then(|tvsn| tvsn.get(3).copied()) } BoxType::TVEpisodeNumberEntry => { meta.tv_episode_number = read_ilst_u8_data(&mut b)?.and_then(|tves| tves.get(3).copied()) } BoxType::GaplessPlaybackEntry => meta.gapless_playback = read_ilst_bool_data(&mut b)?, BoxType::CoverArtEntry => meta.cover_art = read_ilst_multiple_u8_data(&mut b).ok(), _ => skip_box_content(&mut b)?, }; check_parser_state!(b.content); } Ok(()) } fn read_ilst_bool_data(src: &mut BMFFBox) -> Result> { Ok(read_ilst_u8_data(src)?.and_then(|d| Some(d.first()? == &1))) } fn read_ilst_string_data(src: &mut BMFFBox) -> Result> { read_ilst_u8_data(src) } fn read_ilst_u8_data(src: &mut BMFFBox) -> Result>> { // For all non-covr atoms, there must only be one data atom. Ok(read_ilst_multiple_u8_data(src)?.pop()) } fn read_ilst_multiple_u8_data(src: &mut BMFFBox) -> Result>> { let mut iter = src.box_iter(); let mut data = TryVec::new(); while let Some(mut b) = iter.next_box()? { match b.head.name { BoxType::MetadataItemDataEntry => { data.push(read_ilst_data(&mut b)?)?; } _ => skip_box_content(&mut b)?, }; check_parser_state!(b.content); } Ok(data) } fn read_ilst_data(src: &mut BMFFBox) -> Result> { // Skip past the padding bytes skip(&mut src.content, src.head.offset)?; let size = src.content.limit(); read_buf(&mut src.content, size) } /// Skip a number of bytes that we don't care to parse. fn skip(src: &mut T, bytes: u64) -> Result<()> { std::io::copy(&mut src.take(bytes), &mut std::io::sink())?; Ok(()) } /// Read size bytes into a Vector or return error. fn read_buf(src: &mut T, size: u64) -> Result> { let buf = src.take(size).read_into_try_vec()?; if buf.len().to_u64() != size { return Status::ReadBufErr.into(); } Ok(buf) } fn be_i16(src: &mut T) -> Result { src.read_i16::().map_err(From::from) } fn be_i32(src: &mut T) -> Result { src.read_i32::().map_err(From::from) } fn be_i64(src: &mut T) -> Result { src.read_i64::().map_err(From::from) } fn be_u16(src: &mut T) -> Result { src.read_u16::().map_err(From::from) } fn be_u24(src: &mut T) -> Result { src.read_u24::().map_err(From::from) } fn be_u32(src: &mut T) -> Result { src.read_u32::().map_err(From::from) } fn be_u64(src: &mut T) -> Result { src.read_u64::().map_err(From::from) } fn write_be_u32(des: &mut T, num: u32) -> Result<()> { des.write_u32::(num) .map_err(From::from) } mp4parse-0.17.0/src/macros.rs000064400000000000000000000006721046102023000140720ustar 00000000000000// 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/. macro_rules! check_parser_state { ( $src:expr ) => { if $src.limit() > 0 { debug!("bad parser state: {} content bytes left", $src.limit()); return Status::CheckParserStateErr.into(); } }; } mp4parse-0.17.0/src/tests.rs000064400000000000000000001336021046102023000137500ustar 00000000000000//! Module for parsing ISO Base Media Format aka video/mp4 streams. //! Internal unit tests. // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. use super::read_mp4; use super::ParseStrictness; use super::{Error, Status}; use fallible_collections::TryRead as _; use std::convert::TryInto as _; use std::io::Cursor; use std::io::Read as _; use test_assembler::*; use crate::boxes::BoxType; enum BoxSize { Short(u32), Long(u64), UncheckedShort(u32), UncheckedLong(u64), Auto, } #[allow(clippy::trivially_copy_pass_by_ref)] // TODO: Consider reworking to a copy fn make_box(size: BoxSize, name: &[u8; 4], func: F) -> Cursor> where F: Fn(Section) -> Section, { let mut section = Section::new(); let box_size = Label::new(); section = match size { BoxSize::Short(size) | BoxSize::UncheckedShort(size) => section.B32(size), BoxSize::Long(_) | BoxSize::UncheckedLong(_) => section.B32(1), BoxSize::Auto => section.B32(&box_size), }; section = section.append_bytes(name); section = match size { // The spec allows the 32-bit size to be 0 to indicate unknown // length streams. It's not clear if this is valid when using a // 64-bit size, so prohibit it for now. BoxSize::Long(size) => { assert!(size > 0); section.B64(size) } BoxSize::UncheckedLong(size) => section.B64(size), _ => section, }; section = func(section); match size { BoxSize::Short(size) => { if size > 0 { assert_eq!(u64::from(size), section.size()) } } BoxSize::Long(size) => assert_eq!(size, section.size()), BoxSize::Auto => { assert!( section.size() <= u64::from(u32::max_value()), "Tried to use a long box with BoxSize::Auto" ); box_size.set_const(section.size()); } // Skip checking BoxSize::Unchecked* cases. _ => (), } Cursor::new(section.get_contents().unwrap()) } fn make_uuid_box(size: BoxSize, uuid: &[u8; 16], func: F) -> Cursor> where F: Fn(Section) -> Section, { make_box(size, b"uuid", |mut s| { for b in uuid { s = s.B8(*b); } func(s) }) } #[allow(clippy::trivially_copy_pass_by_ref)] // TODO: Consider reworking to a copy fn make_fullbox(size: BoxSize, name: &[u8; 4], version: u8, func: F) -> Cursor> where F: Fn(Section) -> Section, { make_box(size, name, |s| func(s.B8(version).B8(0).B8(0).B8(0))) } #[test] fn read_box_header_short() { let mut stream = make_box(BoxSize::Short(8), b"test", |s| s); let header = super::read_box_header(&mut stream).unwrap(); assert_eq!(header.name, BoxType::UnknownBox(0x7465_7374)); // "test" assert_eq!(header.size, 8); assert!(header.uuid.is_none()); } #[test] fn read_box_header_long() { let mut stream = make_box(BoxSize::Long(16), b"test", |s| s); let header = super::read_box_header(&mut stream).unwrap(); assert_eq!(header.name, BoxType::UnknownBox(0x7465_7374)); // "test" assert_eq!(header.size, 16); assert!(header.uuid.is_none()); } #[test] fn read_box_header_short_unknown_size() { let mut stream = make_box(BoxSize::Short(0), b"test", |s| s); match super::read_box_header(&mut stream) { Err(Error::Unsupported(s)) => assert_eq!(s, "unknown sized box"), _ => panic!("unexpected result reading box with unknown size"), }; } #[test] fn read_box_header_short_invalid_size() { let mut stream = make_box(BoxSize::UncheckedShort(2), b"test", |s| s); match super::read_box_header(&mut stream) { Err(Error::InvalidData(s)) => assert_eq!(s, Status::BoxBadSize), _ => panic!("unexpected result reading box with invalid size"), }; } #[test] fn read_box_header_long_invalid_size() { let mut stream = make_box(BoxSize::UncheckedLong(2), b"test", |s| s); match super::read_box_header(&mut stream) { Err(Error::InvalidData(s)) => assert_eq!(s, Status::BoxBadWideSize), _ => panic!("unexpected result reading box with invalid size"), }; } #[test] fn read_box_header_uuid() { const HEADER_UUID: [u8; 16] = [ 0x85, 0xc0, 0xb6, 0x87, 0x82, 0x0f, 0x11, 0xe0, 0x81, 0x11, 0xf4, 0xce, 0x46, 0x2b, 0x6a, 0x48, ]; let mut stream = make_uuid_box(BoxSize::Short(24), &HEADER_UUID, |s| s); let mut iter = super::BoxIter::new(&mut stream); let stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::UuidBox); assert_eq!(stream.head.size, 24); assert!(stream.head.uuid.is_some()); assert_eq!(stream.head.uuid.unwrap(), HEADER_UUID); } #[test] fn read_box_header_truncated_uuid() { const HEADER_UUID: [u8; 16] = [ 0x85, 0xc0, 0xb6, 0x87, 0x82, 0x0f, 0x11, 0xe0, 0x81, 0x11, 0xf4, 0xce, 0x46, 0x2b, 0x6a, 0x48, ]; let mut stream = make_uuid_box(BoxSize::UncheckedShort(23), &HEADER_UUID, |s| s); let mut iter = super::BoxIter::new(&mut stream); let stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::UuidBox); assert_eq!(stream.head.size, 23); assert!(stream.head.uuid.is_none()); } #[test] fn read_box_header_uuid_past_eof() { const HEADER_UUID: [u8; 20] = [ 0x00, 0x00, 0x00, 0x18, // size = 24 0x75, 0x75, 0x69, 0x64, // type = uuid 0x85, 0xc0, 0xb6, 0x87, 0x82, 0x0f, 0x11, 0xe0, 0x81, 0x11, 0xf4, 0xce, ]; let mut cursor = Cursor::new(HEADER_UUID); let mut iter = super::BoxIter::new(&mut cursor); match iter.next_box() { Ok(None) => (), Ok(_) => panic!("unexpected box read"), _ => panic!("unexpected error"), }; } #[test] fn read_ftyp() { let mut stream = make_box(BoxSize::Short(24), b"ftyp", |s| { s.append_bytes(b"mp42") .B32(0) // minor version .append_bytes(b"isom") .append_bytes(b"mp42") }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::FileTypeBox); assert_eq!(stream.head.size, 24); let parsed = super::read_ftyp(&mut stream).unwrap(); assert_eq!(parsed.major_brand, b"mp42"); // mp42 assert_eq!(parsed.minor_version, 0); assert_eq!(parsed.compatible_brands.len(), 2); assert_eq!(parsed.compatible_brands[0], b"isom"); // isom assert_eq!(parsed.compatible_brands[1], b"mp42"); // mp42 } #[test] fn read_truncated_ftyp() { // We declare a 24 byte box, but only write 20 bytes. let mut stream = make_box(BoxSize::UncheckedShort(24), b"ftyp", |s| { s.append_bytes(b"mp42") .B32(0) // minor version .append_bytes(b"isom") }); match read_mp4(&mut stream) { Err(Error::UnexpectedEOF) => (), Ok(_) => panic!("expected an error result"), _ => panic!("expected a different error result"), } } #[test] fn read_ftyp_case() { // Brands in BMFF are represented as a u32, so it would seem clear that // 0x6d703432 ("mp42") is not equal to 0x4d503432 ("MP42"), but some // demuxers treat these as case-insensitive strings, e.g. street.mp4's // major brand is "MP42". I haven't seen case-insensitive // compatible_brands (which we also test here), but it doesn't seem // unlikely given the major_brand behaviour. let mut stream = make_box(BoxSize::Auto, b"ftyp", |s| { s.append_bytes(b"MP42") .B32(0) // minor version .append_bytes(b"ISOM") .append_bytes(b"MP42") }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::FileTypeBox); assert_eq!(stream.head.size, 24); let parsed = super::read_ftyp(&mut stream).unwrap(); assert_eq!(parsed.major_brand, b"MP42"); assert_eq!(parsed.minor_version, 0); assert_eq!(parsed.compatible_brands.len(), 2); assert_eq!(parsed.compatible_brands[0], b"ISOM"); // ISOM assert_eq!(parsed.compatible_brands[1], b"MP42"); // MP42 } #[test] fn read_elst_v0() { let mut stream = make_fullbox(BoxSize::Short(28), b"elst", 0, |s| { s.B32(1) // list count // first entry .B32(1234) // duration .B32(5678) // time .B16(12) // rate integer .B16(34) // rate fraction }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::EditListBox); assert_eq!(stream.head.size, 28); let parsed = super::read_elst(&mut stream).unwrap(); assert_eq!(parsed.edits.len(), 1); assert_eq!(parsed.edits[0].segment_duration, 1234); assert_eq!(parsed.edits[0].media_time, 5678); assert_eq!(parsed.edits[0].media_rate_integer, 12); assert_eq!(parsed.edits[0].media_rate_fraction, 34); } #[test] fn read_elst_v1() { let mut stream = make_fullbox(BoxSize::Short(56), b"elst", 1, |s| { s.B32(2) // list count // first entry .B64(1234) // duration .B64(5678) // time .B16(12) // rate integer .B16(34) // rate fraction // second entry .B64(1234) // duration .B64(5678) // time .B16(12) // rate integer .B16(34) // rate fraction }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::EditListBox); assert_eq!(stream.head.size, 56); let parsed = super::read_elst(&mut stream).unwrap(); assert_eq!(parsed.edits.len(), 2); assert_eq!(parsed.edits[1].segment_duration, 1234); assert_eq!(parsed.edits[1].media_time, 5678); assert_eq!(parsed.edits[1].media_rate_integer, 12); assert_eq!(parsed.edits[1].media_rate_fraction, 34); } #[test] fn read_mdhd_v0() { let mut stream = make_fullbox(BoxSize::Short(32), b"mdhd", 0, |s| { s.B32(0) .B32(0) .B32(1234) // timescale .B32(5678) // duration .B32(0) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::MediaHeaderBox); assert_eq!(stream.head.size, 32); let parsed = super::read_mdhd(&mut stream).unwrap(); assert_eq!(parsed.timescale, 1234); assert_eq!(parsed.duration, 5678); } #[test] fn read_mdhd_v1() { let mut stream = make_fullbox(BoxSize::Short(44), b"mdhd", 1, |s| { s.B64(0) .B64(0) .B32(1234) // timescale .B64(5678) // duration .B32(0) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::MediaHeaderBox); assert_eq!(stream.head.size, 44); let parsed = super::read_mdhd(&mut stream).unwrap(); assert_eq!(parsed.timescale, 1234); assert_eq!(parsed.duration, 5678); } #[test] fn read_mdhd_unknown_duration() { let mut stream = make_fullbox(BoxSize::Short(32), b"mdhd", 0, |s| { s.B32(0) .B32(0) .B32(1234) // timescale .B32(::std::u32::MAX) // duration .B32(0) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::MediaHeaderBox); assert_eq!(stream.head.size, 32); let parsed = super::read_mdhd(&mut stream).unwrap(); assert_eq!(parsed.timescale, 1234); assert_eq!(parsed.duration, ::std::u64::MAX); } #[test] fn read_mdhd_invalid_timescale() { let mut stream = make_fullbox(BoxSize::Short(44), b"mdhd", 1, |s| { s.B64(0) .B64(0) .B32(0) // timescale .B64(5678) // duration .B32(0) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::MediaHeaderBox); assert_eq!(stream.head.size, 44); let r = super::parse_mdhd(&mut stream, &mut super::Track::new(0)); assert!(r.is_err()); } #[test] fn read_mvhd_v0() { let mut stream = make_fullbox(BoxSize::Short(108), b"mvhd", 0, |s| { s.B32(0).B32(0).B32(1234).B32(5678).append_repeated(0, 80) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::MovieHeaderBox); assert_eq!(stream.head.size, 108); let parsed = super::read_mvhd(&mut stream).unwrap(); assert_eq!(parsed.timescale, 1234); assert_eq!(parsed.duration, 5678); } #[test] fn read_mvhd_v1() { let mut stream = make_fullbox(BoxSize::Short(120), b"mvhd", 1, |s| { s.B64(0).B64(0).B32(1234).B64(5678).append_repeated(0, 80) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::MovieHeaderBox); assert_eq!(stream.head.size, 120); let parsed = super::read_mvhd(&mut stream).unwrap(); assert_eq!(parsed.timescale, 1234); assert_eq!(parsed.duration, 5678); } #[test] fn read_mvhd_invalid_timescale() { let mut stream = make_fullbox(BoxSize::Short(120), b"mvhd", 1, |s| { s.B64(0).B64(0).B32(0).B64(5678).append_repeated(0, 80) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::MovieHeaderBox); assert_eq!(stream.head.size, 120); let r = super::parse_mvhd(&mut stream); assert!(r.is_err()); } #[test] fn read_mvhd_unknown_duration() { let mut stream = make_fullbox(BoxSize::Short(108), b"mvhd", 0, |s| { s.B32(0) .B32(0) .B32(1234) .B32(::std::u32::MAX) .append_repeated(0, 80) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::MovieHeaderBox); assert_eq!(stream.head.size, 108); let parsed = super::read_mvhd(&mut stream).unwrap(); assert_eq!(parsed.timescale, 1234); assert_eq!(parsed.duration, ::std::u64::MAX); } #[test] fn read_vpcc_version_0() { let data_length = 12u16; let mut stream = make_fullbox(BoxSize::Auto, b"vpcC", 0, |s| { s.B8(2) .B8(0) .B8(0x82) .B8(0) .B16(data_length) .append_repeated(42, data_length as usize) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::VPCodecConfigurationBox); let r = super::read_vpcc(&mut stream); assert!(r.is_ok()); } // TODO: it'd be better to find a real sample here. #[test] #[allow(clippy::unusual_byte_groupings)] // Allow odd grouping for test readability. fn read_vpcc_version_1() { let data_length = 12u16; let mut stream = make_fullbox(BoxSize::Auto, b"vpcC", 1, |s| { s.B8(2) // profile .B8(0) // level .B8(0b1000_011_0) // bitdepth (4 bits), chroma (3 bits), video full range (1 bit) .B8(1) // color primaries .B8(1) // transfer characteristics .B8(1) // matrix .B16(data_length) .append_repeated(42, data_length as usize) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::VPCodecConfigurationBox); let r = super::read_vpcc(&mut stream); match r { Ok(vpcc) => { assert_eq!(vpcc.bit_depth, 8); assert_eq!(vpcc.chroma_subsampling, 3); assert!(!vpcc.video_full_range_flag); assert_eq!(vpcc.matrix_coefficients.unwrap(), 1); } _ => panic!("vpcc parsing error"), } } #[test] fn read_hdlr() { let mut stream = make_fullbox(BoxSize::Short(45), b"hdlr", 0, |s| { s.B32(0) .append_bytes(b"vide") .B32(0) .B32(0) .B32(0) .append_bytes(b"VideoHandler") .B8(0) // null-terminate string }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::HandlerBox); assert_eq!(stream.head.size, 45); let parsed = super::read_hdlr(&mut stream, ParseStrictness::Normal).unwrap(); assert_eq!(parsed.handler_type, b"vide"); } #[test] fn read_hdlr_multiple_nul_in_name() { let mut stream = make_fullbox(BoxSize::Short(45), b"hdlr", 0, |s| { s.B32(0) .append_bytes(b"vide") .B32(0) .B32(0) .B32(0) .append_bytes(b"Vide\0Handler") .B8(0) // null-terminate string }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::HandlerBox); assert_eq!(stream.head.size, 45); assert_eq!( super::Status::from(super::read_hdlr(&mut stream, ParseStrictness::Strict)), super::Status::Ok, ); } #[test] fn read_hdlr_short_name() { let mut stream = make_fullbox(BoxSize::Short(33), b"hdlr", 0, |s| { s.B32(0).append_bytes(b"vide").B32(0).B32(0).B32(0).B8(0) // null-terminate string }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::HandlerBox); assert_eq!(stream.head.size, 33); let parsed = super::read_hdlr(&mut stream, ParseStrictness::Normal).unwrap(); assert_eq!(parsed.handler_type, b"vide"); } #[test] fn read_hdlr_unsupported_version() { let mut stream = make_fullbox(BoxSize::Short(32), b"hdlr", 1, |s| { s.B32(0).append_bytes(b"vide").B32(0).B32(0).B32(0) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::HandlerBox); assert_eq!(stream.head.size, 32); assert_eq!( super::Status::from(super::read_hdlr(&mut stream, ParseStrictness::Normal)), super::Status::HdlrUnsupportedVersion, ); } #[test] fn read_hdlr_invalid_pre_defined_field() { let mut stream = make_fullbox(BoxSize::Short(32), b"hdlr", 0, |s| { s.B32(1).append_bytes(b"vide").B32(0).B32(0).B32(0) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::HandlerBox); assert_eq!(stream.head.size, 32); assert_eq!( super::Status::from(super::read_hdlr(&mut stream, ParseStrictness::Strict)), super::Status::HdlrPredefinedNonzero, ); } #[test] fn read_hdlr_invalid_reserved_field() { let mut stream = make_fullbox(BoxSize::Short(32), b"hdlr", 0, |s| { s.B32(0).append_bytes(b"vide").B32(0).B32(1).B32(0) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::HandlerBox); assert_eq!(stream.head.size, 32); assert_eq!( super::Status::from(super::read_hdlr(&mut stream, ParseStrictness::Strict)), super::Status::HdlrReservedNonzero, ); } #[test] fn read_hdlr_zero_length_name() { let mut stream = make_fullbox(BoxSize::Short(32), b"hdlr", 0, |s| { s.B32(0).append_bytes(b"vide").B32(0).B32(0).B32(0) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::HandlerBox); assert_eq!(stream.head.size, 32); assert_eq!( super::Status::from(super::read_hdlr(&mut stream, ParseStrictness::Normal)), super::Status::HdlrNameNoNul, ); } #[test] fn read_hdlr_zero_length_name_permissive() { let mut stream = make_fullbox(BoxSize::Short(32), b"hdlr", 0, |s| { s.B32(0).append_bytes(b"vide").B32(0).B32(0).B32(0) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::HandlerBox); assert_eq!(stream.head.size, 32); let parsed = super::read_hdlr(&mut stream, ParseStrictness::Permissive).unwrap(); assert_eq!(parsed.handler_type, b"vide"); } fn flac_streaminfo() -> Vec { vec![ 0x10, 0x00, 0x10, 0x00, 0x00, 0x0a, 0x11, 0x00, 0x38, 0x32, 0x0a, 0xc4, 0x42, 0xf0, 0x00, 0xc9, 0xdf, 0xae, 0xb5, 0x66, 0xfc, 0x02, 0x15, 0xa3, 0xb1, 0x54, 0x61, 0x47, 0x0f, 0xfb, 0x05, 0x00, 0x33, 0xad, ] } #[test] fn read_flac() { let mut stream = make_box(BoxSize::Auto, b"fLaC", |s| { s.append_repeated(0, 6) // reserved .B16(1) // data reference index .B32(0) // reserved .B32(0) // reserved .B16(2) // channel count .B16(16) // bits per sample .B16(0) // pre_defined .B16(0) // reserved .B32(44100 << 16) // Sample rate .append_bytes( &make_dfla( FlacBlockType::StreamInfo, true, &flac_streaminfo(), FlacBlockLength::Correct, ) .into_inner(), ) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); let r = super::read_audio_sample_entry(&mut stream); assert!(r.is_ok()); } #[derive(Clone, Copy)] enum FlacBlockType { StreamInfo = 0, _Padding = 1, _Application = 2, _Seektable = 3, _Comment = 4, _Cuesheet = 5, _Picture = 6, _Reserved, _Invalid = 127, } enum FlacBlockLength { Correct, Incorrect(usize), } fn make_dfla( block_type: FlacBlockType, last: bool, data: &[u8], data_length: FlacBlockLength, ) -> Cursor> { assert!(data.len() < 1 << 24); make_fullbox(BoxSize::Auto, b"dfLa", 0, |s| { let flag = u32::from(last); let size = match data_length { FlacBlockLength::Correct => (data.len() as u32) & 0x00ff_ffff, FlacBlockLength::Incorrect(size) => { assert!(size < 1 << 24); (size as u32) & 0x00ff_ffff } }; let block_type = (block_type as u32) & 0x7f; s.B32(flag << 31 | block_type << 24 | size) .append_bytes(data) }) } #[test] fn read_dfla() { let mut stream = make_dfla( FlacBlockType::StreamInfo, true, &flac_streaminfo(), FlacBlockLength::Correct, ); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::FLACSpecificBox); let dfla = super::read_dfla(&mut stream).unwrap(); assert_eq!(dfla.version, 0); } #[test] fn long_flac_metadata() { let streaminfo = flac_streaminfo(); let mut stream = make_dfla( FlacBlockType::StreamInfo, true, &streaminfo, FlacBlockLength::Incorrect(streaminfo.len() + 4), ); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::FLACSpecificBox); let r = super::read_dfla(&mut stream); assert!(r.is_err()); } #[test] fn read_opus() { let mut stream = make_box(BoxSize::Auto, b"Opus", |s| { s.append_repeated(0, 6) .B16(1) // data reference index .B32(0) .B32(0) .B16(2) // channel count .B16(16) // bits per sample .B16(0) .B16(0) .B32(48000 << 16) // Sample rate is always 48 kHz for Opus. .append_bytes(&make_dops().into_inner()) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); let r = super::read_audio_sample_entry(&mut stream); assert!(r.is_ok()); } fn make_dops() -> Cursor> { make_box(BoxSize::Auto, b"dOps", |s| { s.B8(0) // version .B8(2) // channel count .B16(348) // pre-skip .B32(44100) // original sample rate .B16(0) // gain .B8(0) // channel mapping }) } #[test] fn read_dops() { let mut stream = make_dops(); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); assert_eq!(stream.head.name, BoxType::OpusSpecificBox); let r = super::read_dops(&mut stream); assert!(r.is_ok()); } #[test] fn serialize_opus_header() { let opus = super::OpusSpecificBox { version: 0, output_channel_count: 1, pre_skip: 342, input_sample_rate: 24000, output_gain: 0, channel_mapping_family: 0, channel_mapping_table: None, }; let mut v = Vec::::new(); super::serialize_opus_header(&opus, &mut v).unwrap(); assert_eq!(v.len(), 19); assert_eq!( v, vec![ 0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, 0x01, 0x01, 0x56, 0x01, 0xc0, 0x5d, 0x00, 0x00, 0x00, 0x00, 0x00, ] ); let opus = super::OpusSpecificBox { version: 0, output_channel_count: 6, pre_skip: 152, input_sample_rate: 48000, output_gain: 0, channel_mapping_family: 1, channel_mapping_table: Some(super::ChannelMappingTable { stream_count: 4, coupled_count: 2, channel_mapping: vec![0, 4, 1, 2, 3, 5].into(), }), }; let mut v = Vec::::new(); super::serialize_opus_header(&opus, &mut v).unwrap(); assert_eq!(v.len(), 27); assert_eq!( v, vec![ 0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, 0x01, 0x06, 0x98, 0x00, 0x80, 0xbb, 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x02, 0x00, 0x04, 0x01, 0x02, 0x03, 0x05, ] ); } #[test] fn read_alac() { let mut stream = make_box(BoxSize::Auto, b"alac", |s| { s.append_repeated(0, 6) // reserved .B16(1) // data reference index .B32(0) // reserved .B32(0) // reserved .B16(2) // channel count .B16(16) // bits per sample .B16(0) // pre_defined .B16(0) // reserved .B32(44100 << 16) // Sample rate .append_bytes( &make_fullbox(BoxSize::Auto, b"alac", 0, |s| s.append_bytes(&[0xfa; 24])) .into_inner(), ) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); let r = super::read_audio_sample_entry(&mut stream); assert!(r.is_ok()); } #[test] fn esds_limit() { let mut stream = make_box(BoxSize::Auto, b"mp4a", |s| { s.append_repeated(0, 6) .B16(1) .B32(0) .B32(0) .B16(2) .B16(16) .B16(0) .B16(0) .B32(48000 << 16) .B32(8) .append_bytes(b"esds") .append_repeated(0, 4) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); match super::read_audio_sample_entry(&mut stream) { Err(Error::UnexpectedEOF) => (), Ok(_) => panic!("expected an error result"), _ => panic!("expected a different error result"), } } #[test] fn read_elst_zero_entries() { let mut stream = make_fullbox(BoxSize::Auto, b"elst", 0, |s| s.B32(0).B16(12).B16(34)); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); match super::read_elst(&mut stream) { Ok(elst) => assert_eq!(elst.edits.len(), 0), _ => panic!("expected no error"), } } fn make_elst() -> Cursor> { make_fullbox(BoxSize::Auto, b"elst", 1, |s| { s.B32(1) // first entry .B64(1234) // duration .B64(0xffff_ffff_ffff_ffff) // time .B16(12) // rate integer .B16(34) // rate fraction }) } #[test] fn read_edts_bogus() { // First edit list entry has a media_time of -1, so we expect a second // edit list entry to be present to provide a valid media_time. // Bogus edts are ignored. let mut stream = make_box(BoxSize::Auto, b"edts", |s| { s.append_bytes(&make_elst().into_inner()) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); let mut track = super::Track::new(0); match super::read_edts(&mut stream, &mut track) { Ok(_) => { assert_eq!(track.media_time, None); assert_eq!(track.empty_duration, None); } _ => panic!("expected no error"), } } #[test] fn skip_padding_in_boxes() { // Padding data could be added in the end of these boxes. Parser needs to skip // them instead of returning error. let box_names = vec![b"stts", b"stsc", b"stsz", b"stco", b"co64", b"stss"]; for name in box_names { let mut stream = make_fullbox(BoxSize::Auto, name, 1, |s| { s.append_repeated(0, 100) // add padding data }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); match name { b"stts" => { super::read_stts(&mut stream).expect("fail to skip padding: stts"); } b"stsc" => { super::read_stsc(&mut stream).expect("fail to skip padding: stsc"); } b"stsz" => { super::read_stsz(&mut stream).expect("fail to skip padding: stsz"); } b"stco" => { super::read_stco(&mut stream).expect("fail to skip padding: stco"); } b"co64" => { super::read_co64(&mut stream).expect("fail to skip padding: co64"); } b"stss" => { super::read_stss(&mut stream).expect("fail to skip padding: stss"); } _ => (), } } } #[test] fn skip_padding_in_stsd() { // Padding data could be added in the end of stsd boxes. Parser needs to skip // them instead of returning error. let avc = make_box(BoxSize::Auto, b"avc1", |s| { s.append_repeated(0, 6) .B16(1) .append_repeated(0, 16) .B16(320) .B16(240) .append_repeated(0, 14) .append_repeated(0, 32) .append_repeated(0, 4) .B32(0xffff_ffff) .append_bytes(b"avcC") .append_repeated(0, 100) }) .into_inner(); let mut stream = make_fullbox(BoxSize::Auto, b"stsd", 0, |s| { s.B32(1) .append_bytes(avc.as_slice()) .append_repeated(0, 100) // add padding data }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); super::read_stsd(&mut stream, &mut super::Track::new(0)).expect("fail to skip padding: stsd"); } #[test] fn read_qt_wave_atom() { let esds = make_fullbox(BoxSize::Auto, b"esds", 0, |s| { s.B8(0x03) // elementary stream descriptor tag .B8(0x12) // esds length .append_repeated(0, 2) .B8(0x00) // flags .B8(0x04) // decoder config descriptor tag .B8(0x0d) // dcds length .B8(0x6b) // mp3 .append_repeated(0, 12) }) .into_inner(); let chan = make_box(BoxSize::Auto, b"chan", |s| { s.append_repeated(0, 10) // we don't care its data. }) .into_inner(); let wave = make_box(BoxSize::Auto, b"wave", |s| s.append_bytes(esds.as_slice())).into_inner(); let mut stream = make_box(BoxSize::Auto, b"mp4a", |s| { s.append_repeated(0, 6) .B16(1) // data_reference_count .B16(1) // verion: qt -> 1 .append_repeated(0, 6) .B16(2) .B16(16) .append_repeated(0, 4) .B32(48000 << 16) .append_repeated(0, 16) .append_bytes(wave.as_slice()) .append_bytes(chan.as_slice()) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); let sample_entry = super::read_audio_sample_entry(&mut stream).expect("fail to read qt wave atom"); match sample_entry { super::SampleEntry::Audio(sample_entry) => { assert_eq!(sample_entry.codec_type, super::CodecType::MP3) } _ => panic!("fail to read audio sample enctry"), } } #[test] fn read_descriptor_80() { let aac_esds = vec![ 0x03, 0x80, 0x80, 0x80, 0x22, 0x00, 0x02, 0x00, 0x04, 0x80, 0x80, 0x80, 0x17, 0x40, 0x15, 0x00, 0x00, 0x00, 0x00, 0x03, 0x22, 0xBC, 0x00, 0x01, 0xF5, 0x83, 0x05, 0x80, 0x80, 0x80, 0x02, 0x11, 0x90, 0x06, 0x80, 0x80, 0x80, 0x01, 0x02, ]; let aac_dc_descriptor = &aac_esds[31..33]; let mut stream = make_box(BoxSize::Auto, b"esds", |s| { s.B32(0) // reserved .append_bytes(aac_esds.as_slice()) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); let es = super::read_esds(&mut stream).unwrap(); assert_eq!(es.audio_codec, super::CodecType::AAC); assert_eq!(es.audio_object_type, Some(2)); assert_eq!(es.extended_audio_object_type, None); assert_eq!(es.audio_sample_rate, Some(48000)); assert_eq!(es.audio_channel_count, Some(2)); assert_eq!(es.codec_esds, aac_esds); assert_eq!(es.decoder_specific_data, aac_dc_descriptor); } #[test] fn read_esds() { let aac_esds = vec![ 0x03, 0x24, 0x00, 0x00, 0x00, 0x04, 0x1c, 0x40, 0x15, 0x00, 0x12, 0x00, 0x00, 0x01, 0xf4, 0x00, 0x00, 0x01, 0xf4, 0x00, 0x05, 0x0d, 0x13, 0x00, 0x05, 0x88, 0x05, 0x00, 0x48, 0x21, 0x10, 0x00, 0x56, 0xe5, 0x98, 0x06, 0x01, 0x02, ]; let aac_dc_descriptor = &aac_esds[22..35]; let mut stream = make_box(BoxSize::Auto, b"esds", |s| { s.B32(0) // reserved .append_bytes(aac_esds.as_slice()) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); let es = super::read_esds(&mut stream).unwrap(); assert_eq!(es.audio_codec, super::CodecType::AAC); assert_eq!(es.audio_object_type, Some(2)); assert_eq!(es.extended_audio_object_type, None); assert_eq!(es.audio_sample_rate, Some(24000)); assert_eq!(es.audio_channel_count, Some(6)); assert_eq!(es.codec_esds, aac_esds); assert_eq!(es.decoder_specific_data, aac_dc_descriptor); } #[test] fn read_esds_aac_type5() { let aac_esds = vec![ 0x03, 0x80, 0x80, 0x80, 0x2F, 0x00, 0x00, 0x00, 0x04, 0x80, 0x80, 0x80, 0x21, 0x40, 0x15, 0x00, 0x15, 0x00, 0x00, 0x03, 0xED, 0xAA, 0x00, 0x03, 0x6B, 0x00, 0x05, 0x80, 0x80, 0x80, 0x0F, 0x2B, 0x01, 0x88, 0x02, 0xC4, 0x04, 0x90, 0x2C, 0x10, 0x8C, 0x80, 0x00, 0x00, 0xED, 0x40, 0x06, 0x80, 0x80, 0x80, 0x01, 0x02, ]; let aac_dc_descriptor = &aac_esds[31..46]; let mut stream = make_box(BoxSize::Auto, b"esds", |s| { s.B32(0) // reserved .append_bytes(aac_esds.as_slice()) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); let es = super::read_esds(&mut stream).unwrap(); assert_eq!(es.audio_codec, super::CodecType::AAC); assert_eq!(es.audio_object_type, Some(2)); assert_eq!(es.extended_audio_object_type, Some(5)); assert_eq!(es.audio_sample_rate, Some(24000)); assert_eq!(es.audio_channel_count, Some(8)); assert_eq!(es.codec_esds, aac_esds); assert_eq!(es.decoder_specific_data, aac_dc_descriptor); } #[test] fn read_esds_mpeg2_aac_lc() { // Recognize MPEG-2 AAC LC (ISO 13818-7) object type as AAC. // Extracted from BMO #1722497 sdasdasdasd_001.mp4 using Bento4. // "mp4extract --payload-only moov/trak[1]/mdia/minf/stbl/stsd/mp4a/esds sdasdasdasd_001.mp4 /dev/stdout | xxd -i -c 15" let aac_esds = vec![ 0x03, 0x19, 0x00, 0x00, 0x00, 0x04, 0x11, 0x67, 0x15, 0x00, 0x02, 0x38, 0x00, 0x01, 0x0f, 0xd0, 0x00, 0x00, 0xf5, 0x48, 0x05, 0x02, 0x13, 0x90, 0x06, 0x01, 0x02, ]; let aac_dc_descriptor = &aac_esds[22..24]; let mut stream = make_box(BoxSize::Auto, b"esds", |s| { s.B32(0) // reserved .append_bytes(aac_esds.as_slice()) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); let es = super::read_esds(&mut stream).unwrap(); assert_eq!(es.audio_codec, super::CodecType::AAC); assert_eq!(es.audio_object_type, Some(2)); assert_eq!(es.extended_audio_object_type, None); assert_eq!(es.audio_sample_rate, Some(22050)); assert_eq!(es.audio_channel_count, Some(2)); assert_eq!(es.codec_esds, aac_esds); assert_eq!(es.decoder_specific_data, aac_dc_descriptor); } #[test] fn read_stsd_mp4v() { let mp4v = vec![ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xd0, 0x01, 0xe0, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x18, 0xff, 0xff, 0x00, 0x00, 0x00, 0x4c, 0x65, 0x73, 0x64, 0x73, 0x00, 0x00, 0x00, 0x00, 0x03, 0x3e, 0x00, 0x00, 0x1f, 0x04, 0x36, 0x20, 0x11, 0x01, 0x77, 0x00, 0x00, 0x03, 0xe8, 0x00, 0x00, 0x03, 0xe8, 0x00, 0x05, 0x27, 0x00, 0x00, 0x01, 0xb0, 0x05, 0x00, 0x00, 0x01, 0xb5, 0x0e, 0xcf, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x20, 0x00, 0x86, 0xe0, 0x00, 0x2e, 0xa6, 0x60, 0x16, 0xf4, 0x01, 0xf4, 0x24, 0xc8, 0x01, 0xe5, 0x16, 0x84, 0x3c, 0x14, 0x63, 0x06, 0x01, 0x02, ]; #[cfg(not(feature = "mp4v"))] let esds_specific_data = &mp4v[90..]; #[cfg(feature = "mp4v")] let esds_specific_data = &mp4v[112..151]; println!("esds_specific_data {esds_specific_data:?}"); let mut stream = make_box(BoxSize::Auto, b"mp4v", |s| s.append_bytes(mp4v.as_slice())); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); let sample_entry = super::read_video_sample_entry(&mut stream).unwrap(); match sample_entry { super::SampleEntry::Video(v) => { assert_eq!(v.codec_type, super::CodecType::MP4V); assert_eq!(v.width, 720); assert_eq!(v.height, 480); match v.codec_specific { super::VideoCodecSpecific::ESDSConfig(esds_data) => { assert_eq!(esds_data.as_slice(), esds_specific_data); } _ => panic!("it should be ESDSConfig!"), } } _ => panic!("it should be a video sample entry!"), } } #[test] fn read_esds_one_byte_extension_descriptor() { let esds = vec![ 0x00, 0x03, 0x80, 0x1b, 0x00, 0x00, 0x00, 0x04, 0x80, 0x12, 0x40, 0x15, 0x00, 0x06, 0x00, 0x00, 0x01, 0xfe, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x05, 0x80, 0x02, 0x11, 0x90, 0x06, 0x01, 0x02, ]; let mut stream = make_box(BoxSize::Auto, b"esds", |s| { s.B32(0) // reserved .append_bytes(esds.as_slice()) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); let es = super::read_esds(&mut stream).unwrap(); assert_eq!(es.audio_codec, super::CodecType::AAC); assert_eq!(es.audio_object_type, Some(2)); assert_eq!(es.extended_audio_object_type, None); assert_eq!(es.audio_sample_rate, Some(48000)); assert_eq!(es.audio_channel_count, Some(2)); } #[test] fn read_esds_byte_extension_descriptor() { let mut stream = make_box(BoxSize::Auto, b"esds", |s| { s.B32(0) // reserved .B16(0x0003) .B16(0x8181) // extension byte length 0x81 .append_repeated(0, 0x81) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); match super::read_esds(&mut stream) { Ok(_) => (), _ => panic!("fail to parse descriptor extension byte length"), } } #[test] fn read_f4v_stsd() { let mut stream = make_box(BoxSize::Auto, b".mp3", |s| { s.append_repeated(0, 6) .B16(1) .B16(0) .append_repeated(0, 6) .B16(2) .B16(16) .append_repeated(0, 4) .B32(48000 << 16) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); let sample_entry = super::read_audio_sample_entry(&mut stream).expect("failed to read f4v stsd atom"); match sample_entry { super::SampleEntry::Audio(sample_entry) => { assert_eq!(sample_entry.codec_type, super::CodecType::MP3) } _ => panic!("fail to read audio sample enctry"), } } #[test] fn unknown_video_sample_entry() { let unknown_codec = make_box(BoxSize::Auto, b"yyyy", |s| s.append_repeated(0, 16)).into_inner(); let mut stream = make_box(BoxSize::Auto, b"xxxx", |s| { s.append_repeated(0, 6) .B16(1) .append_repeated(0, 16) .B16(0) .B16(0) .append_repeated(0, 14) .append_repeated(0, 32) .append_repeated(0, 4) .append_bytes(unknown_codec.as_slice()) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); match super::read_video_sample_entry(&mut stream) { Ok(super::SampleEntry::Unknown) => (), _ => panic!("expected a different error result"), } } #[test] fn unknown_audio_sample_entry() { let unknown_codec = make_box(BoxSize::Auto, b"yyyy", |s| s.append_repeated(0, 16)).into_inner(); let mut stream = make_box(BoxSize::Auto, b"xxxx", |s| { s.append_repeated(0, 6) .B16(1) .B32(0) .B32(0) .B16(2) .B16(16) .B16(0) .B16(0) .B32(48000 << 16) .append_bytes(unknown_codec.as_slice()) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); match super::read_audio_sample_entry(&mut stream) { Ok(super::SampleEntry::Unknown) => (), _ => panic!("expected a different error result"), } } #[test] fn read_esds_invalid_descriptor() { // tag 0x06, 0xff, 0x7f is incorrect. let esds = vec![ 0x03, 0x80, 0x80, 0x80, 0x22, 0x00, 0x00, 0x00, 0x04, 0x80, 0x80, 0x80, 0x14, 0x40, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x00, 0x00, 0xfa, 0x00, 0x05, 0x80, 0x80, 0x80, 0x02, 0xe8, 0x35, 0x06, 0xff, 0x7f, 0x00, 0x00, ]; let mut stream = make_box(BoxSize::Auto, b"esds", |s| { s.B32(0) // reserved .append_bytes(esds.as_slice()) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); match super::read_esds(&mut stream) { Err(Error::InvalidData(s)) => assert_eq!(s, Status::EsdsBadDescriptor), _ => panic!("unexpected result with invalid descriptor"), } } #[test] fn read_esds_redundant_descriptor() { // the '2' at the end is redundant data. let esds = vec![ 3, 25, 0, 1, 0, 4, 19, 64, 21, 0, 0, 0, 0, 0, 0, 0, 0, 1, 119, 0, 5, 2, 18, 16, 6, 1, 2, ]; let mut stream = make_box(BoxSize::Auto, b"esds", |s| { s.B32(0) // reserved .append_bytes(esds.as_slice()) }); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); match super::read_esds(&mut stream) { Ok(esds) => assert_eq!(esds.audio_codec, super::CodecType::AAC), _ => panic!("unexpected result with invalid descriptor"), } } #[test] fn read_stsd_lpcm() { // Extract from sample converted by ffmpeg. // "ffmpeg -i ./gizmo-short.mp4 -acodec pcm_s16le -ar 96000 -vcodec copy -f mov gizmo-short.mov" let lpcm = vec![ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x10, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x40, 0xf7, 0x70, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x63, 0x68, 0x61, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; let mut stream = make_box(BoxSize::Auto, b"lpcm", |s| s.append_bytes(lpcm.as_slice())); let mut iter = super::BoxIter::new(&mut stream); let mut stream = iter.next_box().unwrap().unwrap(); let sample_entry = super::read_audio_sample_entry(&mut stream).unwrap(); match sample_entry { #[allow(clippy::float_cmp)] // The float comparison below is valid and intended. super::SampleEntry::Audio(a) => { assert_eq!(a.codec_type, super::CodecType::LPCM); assert_eq!(a.samplerate, 96000.0); assert_eq!(a.channelcount, 1); match a.codec_specific { super::AudioCodecSpecific::LPCM => (), _ => panic!("it should be LPCM!"), } } _ => panic!("it should be a audio sample entry!"), } } #[test] fn read_to_end_() { let mut src = b"1234567890".take(5); let buf = src.read_into_try_vec().unwrap(); assert_eq!(buf.len(), 5); assert_eq!(buf, b"12345".as_ref()); } #[test] fn read_to_end_oom() { let mut src = b"1234567890".take(std::isize::MAX.try_into().expect("isize < u64")); assert!(src.read_into_try_vec().is_err()); } mp4parse-0.17.0/src/unstable.rs000064400000000000000000000442761046102023000144330ustar 00000000000000// 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 num_traits::{CheckedAdd, CheckedSub, PrimInt, Zero}; use std::ops::{Add, Neg, Sub}; use super::*; /// 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(pub T); impl From for CheckedInteger { fn from(i: T) -> Self { Self(i) } } // Orphan rules prevent a more general implementation, but this suffices impl From> for i64 { fn from(checked: CheckedInteger) -> i64 { checked.0 } } impl> Add for CheckedInteger where T: CheckedAdd, { type Output = Option; fn add(self, other: U) -> Self::Output { self.0.checked_add(&other.into()).map(Into::into) } } impl> Sub for CheckedInteger where T: CheckedSub, { type Output = Option; 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 { type Output = Option>; 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 PartialEq for CheckedInteger { fn eq(&self, other: &T) -> bool { self.0 == *other } } /// Provides the following information about a sample in the source file: /// sample data offset (start and end), composition time in microseconds /// (start and end) and whether it is a sync sample #[repr(C)] #[derive(Default, Debug, PartialEq, Eq)] pub struct Indice { /// The byte offset in the file where the indexed sample begins. pub start_offset: CheckedInteger, /// 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, /// The time in ticks when the indexed sample should be displayed. /// Analogous to the concept of presentation time stamp (pts). pub start_composition: CheckedInteger, /// The time in ticks 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, /// The time in ticks that the indexed sample should be decoded at. /// Analogous to the concept of decode time stamp (dts). pub start_decode: CheckedInteger, /// 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, } /// Create a vector of `Indice`s with the information about track samples. /// It uses `stsc`, `stco`, `stsz` and `stts` boxes to construct a list of /// every sample in the file and provides offsets which can be used to read /// raw sample data from the file. #[allow(clippy::reversed_empty_ranges)] pub fn create_sample_table( track: &Track, track_offset_time: CheckedInteger, ) -> Option> { let (stsc, stco, stsz, stts) = match (&track.stsc, &track.stco, &track.stsz, &track.stts) { (Some(a), Some(b), Some(c), Some(d)) => (a, b, c, d), _ => return None, }; // According to spec, no sync table means every sample is sync sample. let has_sync_table = matches!(track.stss, Some(_)); 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) .map(|(_, sample_counts)| sample_counts.to_usize()) .try_fold(0usize, usize::checked_add)?; 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(Indice { 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 = track.ctts.as_ref().map(|v| v.samples.as_slice().iter()); 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 spec: // 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::(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 = decode_time + ctts_offset; let end_composition = sum_delta + ctts_offset; let start_decode = decode_time; sample.start_composition = CheckedInteger(track_offset_time.0 + start_composition?.0); sample.end_composition = CheckedInteger(track_offset_time.0 + end_composition?.0); sample.start_decode = CheckedInteger(start_decode.0); } // 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) } // 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, cur_offset: i64, ctts_iter: Option>, track_id: usize, } impl<'a> Iterator for TimeOffsetIterator<'a> { type Item = i64; #[allow(clippy::reversed_empty_ranges)] fn next(&mut self) -> Option { 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 = TimeOffsetVersion::Version0(0); 0..0 } }; self.cur_offset = match offset_version { TimeOffsetVersion::Version0(i) => i64::from(i), 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 { match self.next() { Some(v) => TrackScaledTime::(v, self.track_id), _ => TrackScaledTime::(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, cur_sample_delta: u32, stts_iter: std::slice::Iter<'a, Sample>, track_id: usize, } impl<'a> Iterator for TimeToSampleIterator<'a> { type Item = u32; #[allow(clippy::reversed_empty_ranges)] fn next(&mut self) -> Option { 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 { match self.next() { Some(v) => TrackScaledTime::(i64::from(v), self.track_id), _ => TrackScaledTime::(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, stco_offsets: &'a TryVec, ) -> 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, sample_count: u32, stsc_peek_iter: std::iter::Peekable>, 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 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 { 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, }; } } } /// 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(numerator: T, denominator: T, scale2: S) -> Option 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, }) } #[derive(Debug, PartialEq, Eq)] pub struct Microseconds(pub T); /// Convert `time` in media's global (mvhd) timescale to microseconds, /// using provided `MediaTimeScale` pub fn media_time_to_us(time: MediaScaledTime, scale: MediaTimeScale) -> Option> { let microseconds_per_second = 1_000_000; rational_scale(time.0, scale.0, microseconds_per_second).map(Microseconds) } /// Convert `time` in track's local (mdhd) timescale to microseconds, /// using provided `TrackTimeScale` pub fn track_time_to_us( time: TrackScaledTime, scale: TrackTimeScale, ) -> Option> where T: PrimInt + Zero, { assert_eq!(time.1, scale.1); let microseconds_per_second = 1_000_000; rational_scale(time.0, scale.0, microseconds_per_second).map(Microseconds) } #[test] fn rational_scale_overflow() { assert_eq!(rational_scale::(17, 3, 1000), Some(5666)); let large = 0x4000_0000_0000_0000; assert_eq!(rational_scale::(large, 2, 2), Some(large)); assert_eq!(rational_scale::(large, 4, 4), Some(large)); assert_eq!(rational_scale::(large, 2, 8), None); assert_eq!(rational_scale::(large, 8, 4), Some(large / 2)); assert_eq!(rational_scale::(large + 1, 4, 4), Some(large + 1)); assert_eq!(rational_scale::(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(Microseconds(100_079_991_719_000_000u64)) ); } #[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(Microseconds(100_079_991_719_000_000u64)) ); } mp4parse-0.17.0/tests/amr_nb_1f.3gp000064400000000000000000000012751046102023000150520ustar 00000000000000ftyp3gp4isomiso23gp4free(mdat<fyqmoovlmvhd@trak\tkhd@$edtselst 2:mdia mdhd@U-hdlrsounSoundHandlerminfsmhd$dinfdref url stblEstsd5samr@damrFFMPsttsstscstsz stco,;udta3dscpvid:v0200f1d0000bpl97rtqg5b9ohvq6e80mp4parse-0.17.0/tests/amr_wb_1f.3gp000064400000000000000000000013111046102023000150520ustar 00000000000000ftyp3gp4isomiso23gp4freeEmdatD03γPB@*)HIR"x($Xt װ[˺X`moovlmvhd@trak\tkhd@$edtselstP)mdia mdhd>@U-hdlrsounSoundHandlerminfsmhd$dinfdref url stbl4stsd$sawb>stts@stscstsz=stco,;udta3dscpvid:v0200f1d0000bpl97rtqg5b9ohvq6e80mp4parse-0.17.0/tests/bbb_sunflower_QCIF_30fps_h263_noaudio_1f.3gp000064400000000000000000000030521046102023000225350ustar 00000000000000ftyp3gp4isomiso23gp4freemdat& !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1@@@Cb? !1gmoovlmvhd"@trak\tkhd"@$edtselst"mdia mdhd<U-hdlrvideVideoHandler3minfvmhd$dinfdref url stblstsds263HHd263FFMP fielpaspsttsstscstszstco,udtaCperfBlender Foundation 2008, Janus Bager Kristensen 20130titlBig Buck Bunny, Sunflower versiongnreAnimationPdscpCreative Commons Attribution 3.0 - http://bbb3d.renderfarming.netmp4parse-0.17.0/tests/clusterfuzz-testcase-minimized-mp4-6093954524250112000064400000000000000000000000701046102023000231670ustar 00000000000000Cimoovtrak&mdiaWminfmptvstbl1mcttsiooiaamp4parse-0.17.0/tests/overflow.rs000064400000000000000000000007611046102023000150230ustar 00000000000000/// Verify we're built with run-time integer overflow detection. // 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/. #[test] #[should_panic(expected = "attempt to add with overflow")] fn overflow_protection() { let edge = u32::max_value(); assert_eq!(0u32, edge + 1); let edge = u64::max_value(); assert_eq!(0u64, edge + 1); } mp4parse-0.17.0/tests/public.rs000064400000000000000000001752451046102023000144500ustar 00000000000000/// Check if needed fields are still public. // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. use mp4parse as mp4; use crate::mp4::{ParseStrictness, Status}; use std::convert::TryInto; use std::fs::File; use std::io::{Cursor, Read, Seek}; static MINI_MP4: &str = "tests/minimal.mp4"; static MINI_MP4_WITH_METADATA: &str = "tests/metadata.mp4"; static MINI_MP4_WITH_METADATA_STD_GENRE: &str = "tests/metadata_gnre.mp4"; static AUDIO_EME_CENC_MP4: &str = "tests/bipbop-cenc-audioinit.mp4"; static VIDEO_EME_CENC_MP4: &str = "tests/bipbop_480wp_1001kbps-cenc-video-key1-init.mp4"; // The cbcs files were created via shaka-packager from Firefox's test suite's bipbop.mp4 using: // packager-win.exe // in=bipbop.mp4,stream=audio,init_segment=bipbop_cbcs_audio_init.mp4,segment_template=bipbop_cbcs_audio_$Number$.m4s // in=bipbop.mp4,stream=video,init_segment=bipbop_cbcs_video_init.mp4,segment_template=bipbop_cbcs_video_$Number$.m4s // --protection_scheme cbcs --enable_raw_key_encryption // --keys label=:key_id=7e571d047e571d047e571d047e571d21:key=7e5744447e5744447e5744447e574421 // --iv 11223344556677889900112233445566 // --generate_static_mpd --mpd_output bipbop_cbcs.mpd // note: only the init files are needed for these tests static AUDIO_EME_CBCS_MP4: &str = "tests/bipbop_cbcs_audio_init.mp4"; static VIDEO_EME_CBCS_MP4: &str = "tests/bipbop_cbcs_video_init.mp4"; static VIDEO_AV1_MP4: &str = "tests/tiny_av1.mp4"; // This file contains invalid userdata in its copyright userdata. See // https://bugzilla.mozilla.org/show_bug.cgi?id=1687357 for more information. static VIDEO_INVALID_USERDATA: &str = "tests/invalid_userdata.mp4"; static IMAGE_AVIF: &str = "tests/valid.avif"; static IMAGE_AVIF_EXTENTS: &str = "tests/multiple-extents.avif"; static IMAGE_AVIF_ALPHA: &str = "tests/valid-alpha.avif"; static IMAGE_AVIF_ALPHA_PREMULTIPLIED: &str = "tests/1x1-black-alpha-50pct-premultiplied.avif"; static IMAGE_AVIF_CORRUPT: &str = "tests/corrupt/bug-1655846.avif"; static IMAGE_AVIF_CORRUPT_2: &str = "tests/corrupt/bug-1661347.avif"; static IMAGE_AVIF_IPMA_BAD_VERSION: &str = "tests/bad-ipma-version.avif"; static IMAGE_AVIF_IPMA_BAD_FLAGS: &str = "tests/bad-ipma-flags.avif"; static IMAGE_AVIF_IPMA_DUPLICATE_VERSION_AND_FLAGS: &str = "tests/corrupt/ipma-duplicate-version-and-flags.avif"; static IMAGE_AVIF_IPMA_DUPLICATE_ITEM_ID: &str = "tests/corrupt/ipma-duplicate-item_id.avif"; static IMAGE_AVIF_IPMA_INVALID_PROPERTY_INDEX: &str = "tests/corrupt/ipma-invalid-property-index.avif"; static IMAGE_AVIF_NO_HDLR: &str = "tests/corrupt/hdlr-not-first.avif"; static IMAGE_AVIF_HDLR_NOT_FIRST: &str = "tests/corrupt/no-hdlr.avif"; static IMAGE_AVIF_HDLR_NOT_PICT: &str = "tests/corrupt/hdlr-not-pict.avif"; static IMAGE_AVIF_HDLR_NONZERO_RESERVED: &str = "tests/hdlr-nonzero-reserved.avif"; static IMAGE_AVIF_HDLR_MULTIPLE_NUL: &str = "tests/invalid-avif-hdlr-name-multiple-nul.avif"; static IMAGE_AVIF_NO_MIF1: &str = "tests/no-mif1.avif"; static IMAGE_AVIF_NO_PITM: &str = "tests/corrupt/no-pitm.avif"; static IMAGE_AVIF_NO_PIXI: &str = "tests/corrupt/no-pixi.avif"; static IMAGE_AVIF_NO_AV1C: &str = "tests/corrupt/no-av1C.avif"; static IMAGE_AVIF_NO_ISPE: &str = "tests/corrupt/no-ispe.avif"; static IMAGE_AVIF_NO_ALPHA_ISPE: &str = "tests/corrupt/no-alpha-ispe.avif"; static IMAGE_AVIF_TRANSFORM_ORDER: &str = "tests/corrupt/invalid-transformation-order.avif"; static IMAGE_AVIF_TRANSFORM_BEFORE_ISPE: &str = "tests/corrupt/transformation-before-ispe.avif"; static IMAGE_AVIF_NO_ALPHA_AV1C: &str = "tests/corrupt/no-alpha-av1C.avif"; static IMAGE_AVIF_NO_ALPHA_PIXI: &str = "tests/corrupt/no-pixi-for-alpha.avif"; static IMAGE_AVIF_AV1C_MISSING_ESSENTIAL: &str = "tests/av1C-missing-essential.avif"; static IMAGE_AVIF_A1LX_MARKED_ESSENTIAL: &str = "tests/corrupt/a1lx-marked-essential.avif"; static IMAGE_AVIF_A1OP_MISSING_ESSENTIAL: &str = "tests/corrupt/a1op-missing-essential.avif"; static IMAGE_AVIF_IMIR_MISSING_ESSENTIAL: &str = "tests/imir-missing-essential.avif"; static IMAGE_AVIF_IROT_MISSING_ESSENTIAL: &str = "tests/irot-missing-essential.avif"; static IMAGE_AVIF_LSEL_MISSING_ESSENTIAL: &str = "tests/corrupt/lsel-missing-essential.avif"; static IMAGE_AVIF_CLAP_MISSING_ESSENTIAL: &str = "tests/clap-missing-essential.avif"; static IMAGE_AVIF_UNKNOWN_MDAT_SIZE: &str = "tests/unknown_mdat.avif"; static IMAGE_AVIF_UNKNOWN_MDAT_SIZE_IN_OVERSIZED_META: &str = "tests/unknown_mdat_in_oversized_meta.avif"; static IMAGE_AVIF_VALID_WITH_GARBAGE_OVERREAD_AT_END: &str = "tests/valid_with_garbage_overread.avif"; static IMAGE_AVIF_VALID_WITH_GARBAGE_BYTE_AT_END: &str = "tests/valid_with_garbage_byte.avif"; static IMAGE_AVIF_WIDE_BOX_SIZE_0: &str = "tests/wide_box_size_0.avif"; static AVIF_TEST_DIRS: &[&str] = &["tests", "av1-avif/testFiles", "link-u-avif-sample-images"]; // These files are // av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_a1op.avif // av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_a1lx.avif // av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_lsel.avif // respectively, but with https://github.com/AOMediaCodec/av1-avif/issues/174 fixed static AVIF_A1OP: &str = "tests/a1op.avif"; static AVIF_A1LX: &str = "tests/a1lx.avif"; static AVIF_LSEL: &str = "tests/lsel.avif"; static AVIF_CLAP: &str = "tests/clap-basic-1_3x3-to-1x1.avif"; static AVIF_GRID: &str = "av1-avif/testFiles/Microsoft/Summer_in_Tomsk_720p_5x4_grid.avif"; static AVIF_GRID_A1LX: &str = "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_grid_a1lx.avif"; static AVIF_AVIS_MAJOR_NO_PITM: &str = "av1-avif/testFiles/Netflix/avis/Chimera-AV1-10bit-480x270.avif"; /// This is av1-avif/testFiles/Netflix/avis/alpha_video.avif /// but with https://github.com/AOMediaCodec/av1-avif/issues/177 fixed static AVIF_AVIS_MAJOR_WITH_PITM_AND_ALPHA: &str = "tests/alpha_video_fixed.avif"; static AVIF_AVIS_WITH_NO_PITM_NO_ILOC: &str = "tests/avis_with_no_ptim_no_iloc.avif"; static AVIF_AVIS_WITH_PITM_NO_ILOC: &str = "tests/avis_with_pitm_no_iloc.avif"; static AVIF_AVIS_MAJOR_NO_MOOV: &str = "tests/corrupt/alpha_video_moov_is_moop.avif"; static AVIF_AVIS_NO_LOOP: &str = "tests/loop_none.avif"; static AVIF_AVIS_LOOP_FOREVER: &str = "tests/loop_forever.avif"; static AVIF_NO_PIXI_IMAGES: &[&str] = &[IMAGE_AVIF_NO_PIXI, IMAGE_AVIF_NO_ALPHA_PIXI]; static AVIF_UNSUPPORTED_IMAGES: &[&str] = &[ AVIF_A1LX, AVIF_A1OP, AVIF_CLAP, IMAGE_AVIF_CLAP_MISSING_ESSENTIAL, AVIF_GRID, AVIF_GRID_A1LX, AVIF_LSEL, "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_a1lx.avif", "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_a1op.avif", "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_a1op_lsel.avif", "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_lsel.avif", "av1-avif/testFiles/Apple/multilayer_examples/animals_00_multilayer_grid_lsel.avif", "av1-avif/testFiles/Link-U/kimono.crop.avif", "av1-avif/testFiles/Link-U/kimono.mirror-vertical.rotate270.crop.avif", "av1-avif/testFiles/Microsoft/Chimera_10bit_cropped_to_1920x1008.avif", "av1-avif/testFiles/Microsoft/Chimera_10bit_cropped_to_1920x1008_with_HDR_metadata.avif", "av1-avif/testFiles/Microsoft/Chimera_8bit_cropped_480x256.avif", "av1-avif/testFiles/Xiph/abandoned_filmgrain.avif", "av1-avif/testFiles/Xiph/fruits_2layer_thumbsize.avif", "av1-avif/testFiles/Xiph/quebec_3layer_op2.avif", "av1-avif/testFiles/Xiph/tiger_3layer_1res.avif", "av1-avif/testFiles/Xiph/tiger_3layer_3res.avif", "link-u-avif-sample-images/kimono.crop.avif", "link-u-avif-sample-images/kimono.mirror-vertical.rotate270.crop.avif", ]; /// See https://github.com/AOMediaCodec/av1-avif/issues/150 /// https://github.com/AOMediaCodec/av1-avif/issues/174 /// https://github.com/AOMediaCodec/av1-avif/issues/177 /// and https://github.com/AOMediaCodec/av1-avif/issues/178 // TODO: make this into a map of expected errors? static AV1_AVIF_CORRUPT_IMAGES: &[&str] = &[ IMAGE_AVIF_UNKNOWN_MDAT_SIZE_IN_OVERSIZED_META, IMAGE_AVIF_WIDE_BOX_SIZE_0, "av1-avif/testFiles/Link-U/kimono.crop.avif", "av1-avif/testFiles/Link-U/kimono.mirror-horizontal.avif", "av1-avif/testFiles/Link-U/kimono.mirror-vertical.avif", "av1-avif/testFiles/Link-U/kimono.mirror-vertical.rotate270.avif", "av1-avif/testFiles/Link-U/kimono.mirror-vertical.rotate270.crop.avif", "av1-avif/testFiles/Link-U/kimono.rotate90.avif", "av1-avif/testFiles/Link-U/kimono.rotate270.avif", "link-u-avif-sample-images/kimono.crop.avif", "link-u-avif-sample-images/kimono.mirror-horizontal.avif", "link-u-avif-sample-images/kimono.mirror-vertical.avif", "link-u-avif-sample-images/kimono.mirror-vertical.rotate270.avif", "link-u-avif-sample-images/kimono.mirror-vertical.rotate270.crop.avif", "link-u-avif-sample-images/kimono.rotate90.avif", "link-u-avif-sample-images/kimono.rotate270.avif", "link-u-avif-sample-images/plum-blossom-large.profile0.10bpc.yuv420.alpha-full.avif", "link-u-avif-sample-images/plum-blossom-large.profile0.10bpc.yuv420.alpha-full.monochrome.avif", "link-u-avif-sample-images/plum-blossom-large.profile0.10bpc.yuv420.alpha-limited.avif", "link-u-avif-sample-images/plum-blossom-large.profile0.10bpc.yuv420.alpha-limited.monochrome.avif", "link-u-avif-sample-images/plum-blossom-large.profile0.8bpc.yuv420.alpha-full.avif", "link-u-avif-sample-images/plum-blossom-large.profile0.8bpc.yuv420.alpha-full.monochrome.avif", "link-u-avif-sample-images/plum-blossom-large.profile0.8bpc.yuv420.alpha-limited.avif", "link-u-avif-sample-images/plum-blossom-large.profile0.8bpc.yuv420.alpha-limited.monochrome.avif", "link-u-avif-sample-images/plum-blossom-large.profile1.10bpc.yuv444.alpha-full.avif", "link-u-avif-sample-images/plum-blossom-large.profile1.10bpc.yuv444.alpha-limited.avif", "link-u-avif-sample-images/plum-blossom-large.profile1.8bpc.yuv444.alpha-full.avif", "link-u-avif-sample-images/plum-blossom-large.profile1.8bpc.yuv444.alpha-limited.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.10bpc.yuv422.alpha-full.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.10bpc.yuv422.alpha-full.monochrome.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.10bpc.yuv422.alpha-limited.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.10bpc.yuv422.alpha-limited.monochrome.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.12bpc.yuv420.alpha-full.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.12bpc.yuv420.alpha-full.monochrome.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.12bpc.yuv420.alpha-limited.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.12bpc.yuv420.alpha-limited.monochrome.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.12bpc.yuv422.alpha-full.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.12bpc.yuv422.alpha-full.monochrome.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.12bpc.yuv422.alpha-limited.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.12bpc.yuv422.alpha-limited.monochrome.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.12bpc.yuv444.alpha-full.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.12bpc.yuv444.alpha-full.monochrome.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.12bpc.yuv444.alpha-limited.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.12bpc.yuv444.alpha-limited.monochrome.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.8bpc.yuv422.alpha-full.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.8bpc.yuv422.alpha-full.monochrome.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.8bpc.yuv422.alpha-limited.avif", "link-u-avif-sample-images/plum-blossom-large.profile2.8bpc.yuv422.alpha-limited.monochrome.avif", "link-u-avif-sample-images/plum-blossom-small.profile0.10bpc.yuv420.alpha-full.avif", "link-u-avif-sample-images/plum-blossom-small.profile0.10bpc.yuv420.alpha-full.monochrome.avif", "link-u-avif-sample-images/plum-blossom-small.profile0.10bpc.yuv420.alpha-limited.avif", "link-u-avif-sample-images/plum-blossom-small.profile0.10bpc.yuv420.alpha-limited.monochrome.avif", "link-u-avif-sample-images/plum-blossom-small.profile0.8bpc.yuv420.alpha-full.avif", "link-u-avif-sample-images/plum-blossom-small.profile0.8bpc.yuv420.alpha-full.monochrome.avif", "link-u-avif-sample-images/plum-blossom-small.profile0.8bpc.yuv420.alpha-limited.avif", "link-u-avif-sample-images/plum-blossom-small.profile0.8bpc.yuv420.alpha-limited.monochrome.avif", "link-u-avif-sample-images/plum-blossom-small.profile1.10bpc.yuv444.alpha-full.avif", "link-u-avif-sample-images/plum-blossom-small.profile1.10bpc.yuv444.alpha-limited.avif", "link-u-avif-sample-images/plum-blossom-small.profile1.8bpc.yuv444.alpha-full.avif", "link-u-avif-sample-images/plum-blossom-small.profile1.8bpc.yuv444.alpha-limited.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.10bpc.yuv422.alpha-full.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.10bpc.yuv422.alpha-full.monochrome.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.10bpc.yuv422.alpha-limited.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.10bpc.yuv422.alpha-limited.monochrome.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.12bpc.yuv420.alpha-full.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.12bpc.yuv420.alpha-full.monochrome.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.12bpc.yuv420.alpha-limited.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.12bpc.yuv420.alpha-limited.monochrome.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.12bpc.yuv422.alpha-full.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.12bpc.yuv422.alpha-full.monochrome.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.12bpc.yuv422.alpha-limited.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.12bpc.yuv422.alpha-limited.monochrome.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.12bpc.yuv444.alpha-full.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.12bpc.yuv444.alpha-full.monochrome.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.12bpc.yuv444.alpha-limited.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.12bpc.yuv444.alpha-limited.monochrome.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.8bpc.yuv422.alpha-full.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.8bpc.yuv422.alpha-full.monochrome.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.8bpc.yuv422.alpha-limited.avif", "link-u-avif-sample-images/plum-blossom-small.profile2.8bpc.yuv422.alpha-limited.monochrome.avif", ]; static AVIF_CORRUPT_IMAGES_DIR: &str = "tests/corrupt"; // The 1 frame h263 3gp file can be generated by ffmpeg with command // "ffmpeg -i [input file] -f 3gp -vcodec h263 -vf scale=176x144 -frames:v 1 -an output.3gp" static VIDEO_H263_3GP: &str = "tests/bbb_sunflower_QCIF_30fps_h263_noaudio_1f.3gp"; // The 1 frame AMR-NB 3gp file can be generated by ffmpeg with command // "ffmpeg -i [input file] -f 3gp -acodec amr_nb -ar 8000 -ac 1 -frames:a 1 -vn output.3gp" #[cfg(feature = "3gpp")] static AUDIO_AMRNB_3GP: &str = "tests/amr_nb_1f.3gp"; // The 1 frame AMR-WB 3gp file can be generated by ffmpeg with command // "ffmpeg -i [input file] -f 3gp -acodec amr_wb -ar 16000 -ac 1 -frames:a 1 -vn output.3gp" #[cfg(feature = "3gpp")] static AUDIO_AMRWB_3GP: &str = "tests/amr_wb_1f.3gp"; #[cfg(feature = "mp4v")] // The 1 frame mp4v mp4 file can be generated by ffmpeg with command // "ffmpeg -i [input file] -f mp4 -c:v mpeg4 -vf scale=176x144 -frames:v 1 -an output.mp4" static VIDEO_MP4V_MP4: &str = "tests/bbb_sunflower_QCIF_30fps_mp4v_noaudio_1f.mp4"; // Adapted from https://github.com/GuillaumeGomez/audio-video-metadata/blob/9dff40f565af71d5502e03a2e78ae63df95cfd40/src/metadata.rs#L53 #[test] fn public_api() { let mut fd = File::open(MINI_MP4).expect("Unknown file"); let mut buf = Vec::new(); fd.read_to_end(&mut buf).expect("File error"); let mut c = Cursor::new(&buf); let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); assert_eq!(context.timescale, Some(mp4::MediaTimeScale(1000))); for track in context.tracks { match track.track_type { mp4::TrackType::Video => { // track part assert_eq!(track.duration, Some(mp4::TrackScaledTime(512, 0))); assert_eq!(track.empty_duration, Some(mp4::MediaScaledTime(0))); assert_eq!(track.media_time, Some(mp4::TrackScaledTime(0, 0))); assert_eq!(track.timescale, Some(mp4::TrackTimeScale(12800, 0))); // track.tkhd part let tkhd = track.tkhd.unwrap(); assert!(!tkhd.disabled); assert_eq!(tkhd.duration, 40); assert_eq!(tkhd.width, 20_971_520); assert_eq!(tkhd.height, 15_728_640); // track.stsd part let stsd = track.stsd.expect("expected an stsd"); let v = match stsd.descriptions.first().expect("expected a SampleEntry") { mp4::SampleEntry::Video(v) => v, _ => panic!("expected a VideoSampleEntry"), }; assert_eq!(v.width, 320); assert_eq!(v.height, 240); assert_eq!( match v.codec_specific { mp4::VideoCodecSpecific::AVCConfig(ref avc) => { assert!(!avc.is_empty()); "AVC" } mp4::VideoCodecSpecific::VPxConfig(ref vpx) => { // We don't enter in here, we just check if fields are public. assert!(vpx.bit_depth > 0); assert!(vpx.colour_primaries > 0); assert!(vpx.chroma_subsampling > 0); assert!(!vpx.codec_init.is_empty()); "VPx" } mp4::VideoCodecSpecific::ESDSConfig(ref mp4v) => { assert!(!mp4v.is_empty()); "MP4V" } mp4::VideoCodecSpecific::AV1Config(ref _av1c) => { "AV1" } mp4::VideoCodecSpecific::H263Config(ref _h263) => { "H263" } }, "AVC" ); } mp4::TrackType::Audio => { // track part assert_eq!(track.duration, Some(mp4::TrackScaledTime(2944, 1))); assert_eq!(track.empty_duration, Some(mp4::MediaScaledTime(0))); assert_eq!(track.media_time, Some(mp4::TrackScaledTime(1024, 1))); assert_eq!(track.timescale, Some(mp4::TrackTimeScale(48000, 1))); // track.tkhd part let tkhd = track.tkhd.unwrap(); assert!(!tkhd.disabled); assert_eq!(tkhd.duration, 62); assert_eq!(tkhd.width, 0); assert_eq!(tkhd.height, 0); // track.stsd part let stsd = track.stsd.expect("expected an stsd"); let a = match stsd.descriptions.first().expect("expected a SampleEntry") { mp4::SampleEntry::Audio(a) => a, _ => panic!("expected a AudioSampleEntry"), }; assert_eq!( match a.codec_specific { mp4::AudioCodecSpecific::ES_Descriptor(ref esds) => { assert_eq!(esds.audio_codec, mp4::CodecType::AAC); assert_eq!(esds.audio_sample_rate.unwrap(), 48000); assert_eq!(esds.audio_object_type.unwrap(), 2); "ES" } mp4::AudioCodecSpecific::FLACSpecificBox(ref flac) => { // STREAMINFO block must be present and first. assert!(!flac.blocks.is_empty()); assert_eq!(flac.blocks[0].block_type, 0); assert_eq!(flac.blocks[0].data.len(), 34); "FLAC" } mp4::AudioCodecSpecific::OpusSpecificBox(ref opus) => { // We don't enter in here, we just check if fields are public. assert!(opus.version > 0); "Opus" } mp4::AudioCodecSpecific::ALACSpecificBox(ref alac) => { assert!(alac.data.len() == 24 || alac.data.len() == 48); "ALAC" } mp4::AudioCodecSpecific::MP3 => { "MP3" } mp4::AudioCodecSpecific::LPCM => { "LPCM" } #[cfg(feature = "3gpp")] mp4::AudioCodecSpecific::AMRSpecificBox(_) => { "AMR" } }, "ES" ); assert!(a.samplesize > 0); assert!(a.samplerate > 0.0); } _ => {} } } } #[test] fn public_metadata() { let mut fd = File::open(MINI_MP4_WITH_METADATA).expect("Unknown file"); let mut buf = Vec::new(); fd.read_to_end(&mut buf).expect("File error"); let mut c = Cursor::new(&buf); let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); let udta = context .userdata .expect("didn't find udta") .expect("failed to parse udta"); let meta = udta.meta.expect("didn't find meta"); assert_eq!(meta.title.unwrap(), "Title"); assert_eq!(meta.artist.unwrap(), "Artist"); assert_eq!(meta.album_artist.unwrap(), "Album Artist"); assert_eq!(meta.comment.unwrap(), "Comments"); assert_eq!(meta.year.unwrap(), "2019"); assert_eq!( meta.genre.unwrap(), mp4::Genre::CustomGenre("Custom Genre".try_into().unwrap()) ); assert_eq!(meta.encoder.unwrap(), "Lavf56.40.101"); assert_eq!(meta.encoded_by.unwrap(), "Encoded-by"); assert_eq!(meta.copyright.unwrap(), "Copyright"); assert_eq!(meta.track_number.unwrap(), 3); assert_eq!(meta.total_tracks.unwrap(), 6); assert_eq!(meta.disc_number.unwrap(), 5); assert_eq!(meta.total_discs.unwrap(), 10); assert_eq!(meta.beats_per_minute.unwrap(), 128); assert_eq!(meta.composer.unwrap(), "Composer"); assert!(meta.compilation.unwrap()); assert!(!meta.gapless_playback.unwrap()); assert!(!meta.podcast.unwrap()); assert_eq!(meta.advisory.unwrap(), mp4::AdvisoryRating::Clean); assert_eq!(meta.media_type.unwrap(), mp4::MediaType::Normal); assert_eq!(meta.rating.unwrap(), "50"); assert_eq!(meta.grouping.unwrap(), "Grouping"); assert_eq!(meta.category.unwrap(), "Category"); assert_eq!(meta.keyword.unwrap(), "Keyword"); assert_eq!(meta.description.unwrap(), "Description"); assert_eq!(meta.lyrics.unwrap(), "Lyrics"); assert_eq!(meta.long_description.unwrap(), "Long Description"); assert_eq!(meta.tv_episode_name.unwrap(), "Episode Name"); assert_eq!(meta.tv_network_name.unwrap(), "Network Name"); assert_eq!(meta.tv_episode_number.unwrap(), 15); assert_eq!(meta.tv_season.unwrap(), 10); assert_eq!(meta.tv_show_name.unwrap(), "Show Name"); assert!(meta.hd_video.unwrap()); assert_eq!(meta.owner.unwrap(), "Owner"); assert_eq!(meta.sort_name.unwrap(), "Sort Name"); assert_eq!(meta.sort_album.unwrap(), "Sort Album"); assert_eq!(meta.sort_artist.unwrap(), "Sort Artist"); assert_eq!(meta.sort_album_artist.unwrap(), "Sort Album Artist"); assert_eq!(meta.sort_composer.unwrap(), "Sort Composer"); // Check for valid JPEG header let covers = meta.cover_art.unwrap(); let cover = &covers[0]; let mut bytes = [0u8; 4]; bytes[0] = cover[0]; bytes[1] = cover[1]; bytes[2] = cover[2]; assert_eq!(u32::from_le_bytes(bytes), 0x00ff_d8ff); } #[test] fn public_metadata_gnre() { let mut fd = File::open(MINI_MP4_WITH_METADATA_STD_GENRE).expect("Unknown file"); let mut buf = Vec::new(); fd.read_to_end(&mut buf).expect("File error"); let mut c = Cursor::new(&buf); let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); let udta = context .userdata .expect("didn't find udta") .expect("failed to parse udta"); let meta = udta.meta.expect("didn't find meta"); assert_eq!(meta.title.unwrap(), "Title"); assert_eq!(meta.artist.unwrap(), "Artist"); assert_eq!(meta.album_artist.unwrap(), "Album Artist"); assert_eq!(meta.comment.unwrap(), "Comments"); assert_eq!(meta.year.unwrap(), "2019"); assert_eq!(meta.genre.unwrap(), mp4::Genre::StandardGenre(3)); assert_eq!(meta.encoder.unwrap(), "Lavf56.40.101"); assert_eq!(meta.encoded_by.unwrap(), "Encoded-by"); assert_eq!(meta.copyright.unwrap(), "Copyright"); assert_eq!(meta.track_number.unwrap(), 3); assert_eq!(meta.total_tracks.unwrap(), 6); assert_eq!(meta.disc_number.unwrap(), 5); assert_eq!(meta.total_discs.unwrap(), 10); assert_eq!(meta.beats_per_minute.unwrap(), 128); assert_eq!(meta.composer.unwrap(), "Composer"); assert!(meta.compilation.unwrap()); assert!(!meta.gapless_playback.unwrap()); assert!(!meta.podcast.unwrap()); assert_eq!(meta.advisory.unwrap(), mp4::AdvisoryRating::Clean); assert_eq!(meta.media_type.unwrap(), mp4::MediaType::Normal); assert_eq!(meta.rating.unwrap(), "50"); assert_eq!(meta.grouping.unwrap(), "Grouping"); assert_eq!(meta.category.unwrap(), "Category"); assert_eq!(meta.keyword.unwrap(), "Keyword"); assert_eq!(meta.description.unwrap(), "Description"); assert_eq!(meta.lyrics.unwrap(), "Lyrics"); assert_eq!(meta.long_description.unwrap(), "Long Description"); assert_eq!(meta.tv_episode_name.unwrap(), "Episode Name"); assert_eq!(meta.tv_network_name.unwrap(), "Network Name"); assert_eq!(meta.tv_episode_number.unwrap(), 15); assert_eq!(meta.tv_season.unwrap(), 10); assert_eq!(meta.tv_show_name.unwrap(), "Show Name"); assert!(meta.hd_video.unwrap()); assert_eq!(meta.owner.unwrap(), "Owner"); assert_eq!(meta.sort_name.unwrap(), "Sort Name"); assert_eq!(meta.sort_album.unwrap(), "Sort Album"); assert_eq!(meta.sort_artist.unwrap(), "Sort Artist"); assert_eq!(meta.sort_album_artist.unwrap(), "Sort Album Artist"); assert_eq!(meta.sort_composer.unwrap(), "Sort Composer"); // Check for valid JPEG header let covers = meta.cover_art.unwrap(); let cover = &covers[0]; let mut bytes = [0u8; 4]; bytes[0] = cover[0]; bytes[1] = cover[1]; bytes[2] = cover[2]; assert_eq!(u32::from_le_bytes(bytes), 0x00ff_d8ff); } #[test] fn public_invalid_metadata() { // Test that reading userdata containing invalid metadata is not fatal to parsing and that // expected values are still found. let mut fd = File::open(VIDEO_INVALID_USERDATA).expect("Unknown file"); let mut buf = Vec::new(); fd.read_to_end(&mut buf).expect("File error"); let mut c = Cursor::new(&buf); let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); // Should have userdata. assert!(context.userdata.is_some()); // But it should contain an error. assert!(context.userdata.unwrap().is_err()); // Smoke test that other data has been parsed. Don't check everything, just make sure some // values are as expected. assert_eq!(context.tracks.len(), 2); for track in context.tracks { match track.track_type { mp4::TrackType::Video => { // Check some of the values in the video tkhd. let tkhd = track.tkhd.unwrap(); assert!(!tkhd.disabled); assert_eq!(tkhd.duration, 231232); assert_eq!(tkhd.width, 83_886_080); assert_eq!(tkhd.height, 47_185_920); } mp4::TrackType::Audio => { // Check some of the values in the audio tkhd. let tkhd = track.tkhd.unwrap(); assert!(!tkhd.disabled); assert_eq!(tkhd.duration, 231338); assert_eq!(tkhd.width, 0); assert_eq!(tkhd.height, 0); } _ => panic!("File should not contain other tracks."), } } } #[test] fn public_audio_tenc() { let kid = vec![ 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, ]; let mut fd = File::open(AUDIO_EME_CENC_MP4).expect("Unknown file"); let mut buf = Vec::new(); fd.read_to_end(&mut buf).expect("File error"); let mut c = Cursor::new(&buf); let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); for track in context.tracks { let stsd = track.stsd.expect("expected an stsd"); let a = match stsd.descriptions.first().expect("expected a SampleEntry") { mp4::SampleEntry::Audio(a) => a, _ => panic!("expected a AudioSampleEntry"), }; assert_eq!(a.codec_type, mp4::CodecType::EncryptedAudio); match a.protection_info.iter().find(|sinf| sinf.tenc.is_some()) { Some(p) => { assert_eq!(p.original_format, b"mp4a"); if let Some(ref schm) = p.scheme_type { assert_eq!(schm.scheme_type, b"cenc"); } else { panic!("Expected scheme type info"); } if let Some(ref tenc) = p.tenc { assert!(tenc.is_encrypted > 0); assert_eq!(tenc.iv_size, 16); assert_eq!(tenc.kid, kid); assert_eq!(tenc.crypt_byte_block_count, None); assert_eq!(tenc.skip_byte_block_count, None); assert_eq!(tenc.constant_iv, None); } else { panic!("Invalid test condition"); } } _ => { panic!("Invalid test condition"); } } } } #[test] fn public_video_cenc() { let system_id = vec![ 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b, ]; let kid = vec![ 0x7e, 0x57, 0x1d, 0x03, 0x7e, 0x57, 0x1d, 0x03, 0x7e, 0x57, 0x1d, 0x03, 0x7e, 0x57, 0x1d, 0x11, ]; let pssh_box = vec![ 0x00, 0x00, 0x00, 0x34, 0x70, 0x73, 0x73, 0x68, 0x01, 0x00, 0x00, 0x00, 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b, 0x00, 0x00, 0x00, 0x01, 0x7e, 0x57, 0x1d, 0x03, 0x7e, 0x57, 0x1d, 0x03, 0x7e, 0x57, 0x1d, 0x03, 0x7e, 0x57, 0x1d, 0x11, 0x00, 0x00, 0x00, 0x00, ]; let mut fd = File::open(VIDEO_EME_CENC_MP4).expect("Unknown file"); let mut buf = Vec::new(); fd.read_to_end(&mut buf).expect("File error"); let mut c = Cursor::new(&buf); let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); for track in context.tracks { let stsd = track.stsd.expect("expected an stsd"); let v = match stsd.descriptions.first().expect("expected a SampleEntry") { mp4::SampleEntry::Video(ref v) => v, _ => panic!("expected a VideoSampleEntry"), }; assert_eq!(v.codec_type, mp4::CodecType::EncryptedVideo); match v.protection_info.iter().find(|sinf| sinf.tenc.is_some()) { Some(p) => { assert_eq!(p.original_format, b"avc1"); if let Some(ref schm) = p.scheme_type { assert_eq!(schm.scheme_type, b"cenc"); } else { panic!("Expected scheme type info"); } if let Some(ref tenc) = p.tenc { assert!(tenc.is_encrypted > 0); assert_eq!(tenc.iv_size, 16); assert_eq!(tenc.kid, kid); assert_eq!(tenc.crypt_byte_block_count, None); assert_eq!(tenc.skip_byte_block_count, None); assert_eq!(tenc.constant_iv, None); } else { panic!("Invalid test condition"); } } _ => { panic!("Invalid test condition"); } } } for pssh in context.psshs { assert_eq!(pssh.system_id, system_id); for kid_id in pssh.kid { assert_eq!(kid_id, kid); } assert!(pssh.data.is_empty()); assert_eq!(pssh.box_content, pssh_box); } } #[test] fn public_audio_cbcs() { let system_id = vec![ 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b, ]; let kid = vec![ 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x21, ]; let default_iv = vec![ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, ]; let pssh_box = vec![ 0x00, 0x00, 0x00, 0x34, 0x70, 0x73, 0x73, 0x68, 0x01, 0x00, 0x00, 0x00, 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b, 0x00, 0x00, 0x00, 0x01, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x21, 0x00, 0x00, 0x00, 0x00, ]; let mut fd = File::open(AUDIO_EME_CBCS_MP4).expect("Unknown file"); let mut buf = Vec::new(); fd.read_to_end(&mut buf).expect("File error"); let mut c = Cursor::new(&buf); let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); for track in context.tracks { let stsd = track.stsd.expect("expected an stsd"); assert_eq!(stsd.descriptions.len(), 2); let mut found_encrypted_sample_description = false; for description in stsd.descriptions { match description { mp4::SampleEntry::Audio(ref a) => { if let Some(p) = a.protection_info.iter().find(|sinf| sinf.tenc.is_some()) { found_encrypted_sample_description = true; assert_eq!(p.original_format, b"mp4a"); if let Some(ref schm) = p.scheme_type { assert_eq!(schm.scheme_type, b"cbcs"); } else { panic!("Expected scheme type info"); } if let Some(ref tenc) = p.tenc { assert!(tenc.is_encrypted > 0); assert_eq!(tenc.iv_size, 0); assert_eq!(tenc.kid, kid); // Note: 0 for both crypt and skip seems odd but // that's what shaka-packager produced. It appears // to indicate full encryption. assert_eq!(tenc.crypt_byte_block_count, Some(0)); assert_eq!(tenc.skip_byte_block_count, Some(0)); assert_eq!(tenc.constant_iv, Some(default_iv.clone().into())); } else { panic!("Invalid test condition"); } } } _ => { panic!("expected a VideoSampleEntry"); } } } assert!( found_encrypted_sample_description, "Should have found an encrypted sample description" ); } for pssh in context.psshs { assert_eq!(pssh.system_id, system_id); for kid_id in pssh.kid { assert_eq!(kid_id, kid); } assert!(pssh.data.is_empty()); assert_eq!(pssh.box_content, pssh_box); } } #[test] fn public_video_cbcs() { let system_id = vec![ 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b, ]; let kid = vec![ 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x21, ]; let default_iv = vec![ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, ]; let pssh_box = vec![ 0x00, 0x00, 0x00, 0x34, 0x70, 0x73, 0x73, 0x68, 0x01, 0x00, 0x00, 0x00, 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b, 0x00, 0x00, 0x00, 0x01, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x04, 0x7e, 0x57, 0x1d, 0x21, 0x00, 0x00, 0x00, 0x00, ]; let mut fd = File::open(VIDEO_EME_CBCS_MP4).expect("Unknown file"); let mut buf = Vec::new(); fd.read_to_end(&mut buf).expect("File error"); let mut c = Cursor::new(&buf); let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); for track in context.tracks { let stsd = track.stsd.expect("expected an stsd"); assert_eq!(stsd.descriptions.len(), 2); let mut found_encrypted_sample_description = false; for description in stsd.descriptions { match description { mp4::SampleEntry::Video(ref v) => { assert_eq!(v.width, 400); assert_eq!(v.height, 300); if let Some(p) = v.protection_info.iter().find(|sinf| sinf.tenc.is_some()) { found_encrypted_sample_description = true; assert_eq!(p.original_format, b"avc1"); if let Some(ref schm) = p.scheme_type { assert_eq!(schm.scheme_type, b"cbcs"); } else { panic!("Expected scheme type info"); } if let Some(ref tenc) = p.tenc { assert!(tenc.is_encrypted > 0); assert_eq!(tenc.iv_size, 0); assert_eq!(tenc.kid, kid); assert_eq!(tenc.crypt_byte_block_count, Some(1)); assert_eq!(tenc.skip_byte_block_count, Some(9)); assert_eq!(tenc.constant_iv, Some(default_iv.clone().into())); } else { panic!("Invalid test condition"); } } } _ => { panic!("expected a VideoSampleEntry"); } } } assert!( found_encrypted_sample_description, "Should have found an encrypted sample description" ); } for pssh in context.psshs { assert_eq!(pssh.system_id, system_id); for kid_id in pssh.kid { assert_eq!(kid_id, kid); } assert!(pssh.data.is_empty()); assert_eq!(pssh.box_content, pssh_box); } } #[test] fn public_video_av1() { let mut fd = File::open(VIDEO_AV1_MP4).expect("Unknown file"); let mut buf = Vec::new(); fd.read_to_end(&mut buf).expect("File error"); let mut c = Cursor::new(&buf); let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); for track in context.tracks { // track part assert_eq!(track.duration, Some(mp4::TrackScaledTime(512, 0))); assert_eq!(track.empty_duration, Some(mp4::MediaScaledTime(0))); assert_eq!(track.media_time, Some(mp4::TrackScaledTime(0, 0))); assert_eq!(track.timescale, Some(mp4::TrackTimeScale(12288, 0))); // track.tkhd part let tkhd = track.tkhd.unwrap(); assert!(!tkhd.disabled); assert_eq!(tkhd.duration, 42); assert_eq!(tkhd.width, 4_194_304); assert_eq!(tkhd.height, 4_194_304); // track.stsd part let stsd = track.stsd.expect("expected an stsd"); let v = match stsd.descriptions.first().expect("expected a SampleEntry") { mp4::SampleEntry::Video(ref v) => v, _ => panic!("expected a VideoSampleEntry"), }; assert_eq!(v.codec_type, mp4::CodecType::AV1); assert_eq!(v.width, 64); assert_eq!(v.height, 64); match v.codec_specific { mp4::VideoCodecSpecific::AV1Config(ref av1c) => { // TODO: test av1c fields once ffmpeg is updated assert_eq!(av1c.profile, 0); assert_eq!(av1c.level, 0); assert_eq!(av1c.tier, 0); assert_eq!(av1c.bit_depth, 8); assert!(!av1c.monochrome); assert_eq!(av1c.chroma_subsampling_x, 1); assert_eq!(av1c.chroma_subsampling_y, 1); assert_eq!(av1c.chroma_sample_position, 0); assert!(!av1c.initial_presentation_delay_present); assert_eq!(av1c.initial_presentation_delay_minus_one, 0); } _ => panic!("Invalid test condition"), } } } #[test] fn public_mp4_bug_1185230() { let input = &mut File::open("tests/test_case_1185230.mp4").expect("Unknown file"); let context = mp4::read_mp4(input).expect("read_mp4 failed"); let number_video_tracks = context .tracks .iter() .filter(|t| t.track_type == mp4::TrackType::Video) .count(); let number_audio_tracks = context .tracks .iter() .filter(|t| t.track_type == mp4::TrackType::Audio) .count(); assert_eq!(number_video_tracks, 2); assert_eq!(number_audio_tracks, 2); } #[test] fn public_mp4_ctts_overflow() { let input = &mut File::open("tests/clusterfuzz-testcase-minimized-mp4-6093954524250112") .expect("Unknown file"); assert_eq!(Status::from(mp4::read_mp4(input)), Status::CttsBadSize); } #[test] fn public_avif_primary_item() { let input = &mut File::open(IMAGE_AVIF).expect("Unknown file"); let context = mp4::read_avif(input, ParseStrictness::Normal).expect("read_avif failed"); assert_eq!( context.primary_item_coded_data().unwrap(), [ 0x12, 0x00, 0x0A, 0x07, 0x38, 0x00, 0x06, 0x90, 0x20, 0x20, 0x69, 0x32, 0x0C, 0x16, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x79, 0x4C, 0xD2, 0x02 ] ); } #[test] fn public_avif_primary_item_split_extents() { let input = &mut File::open(IMAGE_AVIF_EXTENTS).expect("Unknown file"); let context = mp4::read_avif(input, ParseStrictness::Normal).expect("read_avif failed"); assert_eq!(context.primary_item_coded_data().unwrap().len(), 52); } #[test] fn public_avif_alpha_item() { for_strictness_result(IMAGE_AVIF_ALPHA, |_strictness, result| { assert!(result.is_ok()); }); } #[test] fn public_avif_alpha_non_premultiplied() { for_strictness_result(IMAGE_AVIF_ALPHA, |_strictness, result| { let context = result.expect("read_avif failed"); assert!(context.primary_item_coded_data().is_some()); assert!(context.alpha_item_coded_data().is_some()); assert!(!context.premultiplied_alpha); }); } #[test] fn public_avif_alpha_premultiplied() { for_strictness_result(IMAGE_AVIF_ALPHA_PREMULTIPLIED, |_strictness, result| { let context = result.expect("read_avif failed"); assert!(context.primary_item_coded_data().is_some()); assert!(context.alpha_item_coded_data().is_some()); assert!(context.premultiplied_alpha); }); } #[test] fn public_avif_unknown_mdat() { let input = &mut File::open(IMAGE_AVIF_UNKNOWN_MDAT_SIZE).expect("Unknown file"); let context = mp4::read_avif(input, ParseStrictness::Normal).expect("read_avif failed"); assert_eq!( context.primary_item_coded_data().unwrap(), [ 0x12, 0x00, 0x0A, 0x07, 0x38, 0x00, 0x06, 0x90, 0x20, 0x20, 0x69, 0x32, 0x0C, 0x16, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, 0x79, 0x4C, 0xD2, 0x02 ] ); } #[test] fn public_avif_unknown_mdat_in_oversized_meta() { let input = &mut File::open(IMAGE_AVIF_UNKNOWN_MDAT_SIZE_IN_OVERSIZED_META).expect("Unknown file"); assert_eq!( Status::from(mp4::read_avif(input, ParseStrictness::Normal)), Status::Unsupported ); } #[test] fn public_avif_bug_1655846() { let input = &mut File::open(IMAGE_AVIF_CORRUPT).expect("Unknown file"); assert!(mp4::read_avif(input, ParseStrictness::Normal).is_err()); } #[test] fn public_avif_bug_1661347() { let input = &mut File::open(IMAGE_AVIF_CORRUPT_2).expect("Unknown file"); assert!(mp4::read_avif(input, ParseStrictness::Normal).is_err()); } fn for_strictness_result( path: &str, check: impl Fn(ParseStrictness, mp4::Result), ) { let input = &mut File::open(path).expect("Unknown file"); for strictness in [ ParseStrictness::Permissive, ParseStrictness::Normal, ParseStrictness::Strict, ] { input.rewind().expect("rewind failed"); check(strictness, mp4::read_avif(input, strictness)); } } /// Check that input generates the expected error only in strict parsing mode fn assert_avif_should(path: &str, expected: Status) { for_strictness_result(path, |strictness, result| { if strictness == ParseStrictness::Strict { assert_eq!(expected, Status::from(result)); } else { assert!(result.is_ok()); } }) } /// Check that input generates the expected error unless in permissive parsing mode fn assert_avif_shall(path: &str, expected: Status) { for_strictness_result(path, |strictness, result| { if strictness == ParseStrictness::Permissive { assert!(result.is_ok()); } else { assert_eq!(expected, Status::from(result)); } }) } // Technically all transforms shall be essential, but this appears likely to change // so we only enforce it in strict parsing // See https://github.com/mozilla/mp4parse-rust/issues/284 #[test] fn public_avif_av1c_missing_essential() { assert_avif_should(IMAGE_AVIF_AV1C_MISSING_ESSENTIAL, Status::TxformNoEssential); } #[test] fn public_avif_clap_missing_essential() { for_strictness_result(IMAGE_AVIF_CLAP_MISSING_ESSENTIAL, |strictness, result| { if strictness == ParseStrictness::Strict { assert_eq!(Status::TxformNoEssential, Status::from(result)); } else { assert_unsupported_nonfatal(&result, mp4::Feature::Clap); } }) } #[test] fn public_avif_imir_missing_essential() { assert_avif_should(IMAGE_AVIF_IMIR_MISSING_ESSENTIAL, Status::TxformNoEssential); } #[test] fn public_avif_irot_missing_essential() { assert_avif_should(IMAGE_AVIF_IROT_MISSING_ESSENTIAL, Status::TxformNoEssential); } #[test] fn public_avif_ipma_bad_version() { assert_avif_should(IMAGE_AVIF_IPMA_BAD_VERSION, Status::IpmaBadVersion); } #[test] fn public_avif_ipma_bad_flags() { assert_avif_should(IMAGE_AVIF_IPMA_BAD_FLAGS, Status::IpmaFlagsNonzero); } #[test] fn public_avif_ipma_duplicate_version_and_flags() { assert_avif_shall( IMAGE_AVIF_IPMA_DUPLICATE_VERSION_AND_FLAGS, Status::IpmaBadQuantity, ); } #[test] // TODO: convert this to a `assert_avif_shall` test to cover all `ParseStrictness` modes // that would require crafting an input that validly uses multiple ipma boxes, // which is kind of annoying to make pass the "should" requirements on flags and version // as well as the "shall" requirement on duplicate version and flags fn public_avif_ipma_duplicate_item_id() { let input = &mut File::open(IMAGE_AVIF_IPMA_DUPLICATE_ITEM_ID).expect("Unknown file"); assert_eq!( Status::from(mp4::read_avif(input, ParseStrictness::Permissive)), Status::IpmaDuplicateItemId ) } #[test] fn public_avif_ipma_invalid_property_index() { assert_avif_shall(IMAGE_AVIF_IPMA_INVALID_PROPERTY_INDEX, Status::IpmaBadIndex); } #[test] fn public_avif_hdlr_first_in_meta() { assert_avif_shall(IMAGE_AVIF_NO_HDLR, Status::HdlrNotFirst); assert_avif_shall(IMAGE_AVIF_HDLR_NOT_FIRST, Status::HdlrNotFirst); } #[test] fn public_avif_hdlr_is_pict() { assert_avif_shall(IMAGE_AVIF_HDLR_NOT_PICT, Status::HdlrTypeNotPict); } #[test] fn public_avif_hdlr_nonzero_reserved() { // This is a "should" despite the spec indicating a (somewhat ambiguous) // requirement that this field is set to zero. // See comments in read_hdlr assert_avif_should( IMAGE_AVIF_HDLR_NONZERO_RESERVED, Status::HdlrReservedNonzero, ); } #[test] fn public_avif_hdlr_multiple_nul() { // This is a "should" despite the spec indicating a (somewhat ambiguous) // requirement about extra data in boxes // See comments in read_hdlr assert_avif_should(IMAGE_AVIF_HDLR_MULTIPLE_NUL, Status::Ok); } #[test] fn public_avif_no_mif1() { assert_avif_should(IMAGE_AVIF_NO_MIF1, Status::MissingMif1Brand); } #[test] fn public_avif_no_pitm() { assert_avif_shall(IMAGE_AVIF_NO_PITM, Status::PitmMissing); } #[test] fn public_avif_pixi_present_for_displayable_images() { let pixi_test = if cfg!(feature = "missing-pixi-permitted") { assert_avif_should } else { assert_avif_shall }; pixi_test(IMAGE_AVIF_NO_PIXI, Status::PixiMissing); pixi_test(IMAGE_AVIF_NO_ALPHA_PIXI, Status::PixiMissing); } #[test] fn public_avif_av1c_present_for_av01() { assert_avif_shall(IMAGE_AVIF_NO_AV1C, Status::Av1cMissing); assert_avif_shall(IMAGE_AVIF_NO_ALPHA_AV1C, Status::Av1cMissing); } #[test] fn public_avif_ispe_present() { assert_avif_shall(IMAGE_AVIF_NO_ISPE, Status::IspeMissing); assert_avif_shall(IMAGE_AVIF_NO_ALPHA_ISPE, Status::IspeMissing); } #[test] fn public_avif_transform_before_ispe() { assert_avif_shall(IMAGE_AVIF_TRANSFORM_BEFORE_ISPE, Status::TxformBeforeIspe); } #[test] fn public_avif_transform_order() { assert_avif_shall(IMAGE_AVIF_TRANSFORM_ORDER, Status::TxformOrder); } #[allow(clippy::uninlined_format_args)] fn assert_unsupported_nonfatal(result: &mp4::Result, feature: mp4::Feature) { match result { Ok(context) => { assert!( context.unsupported_features.contains(feature), "context.unsupported_features missing expected {:?}", feature ); } r => panic!( "Expected Ok with unsupported_features containing {:?}, found {:?}", feature, r ), } } // Assert that across all strictness levels the given feature is tracked as // being used, but unsupported. Additionally, if the feature is essential, // assert that the primary item is not processed unless using permissive mode. // TODO: Add similar tests for alpha fn assert_unsupported(path: &str, feature: mp4::Feature, essential: bool) { for_strictness_result(path, |strictness, result| { assert_unsupported_nonfatal(&result, feature); match result { Ok(context) if essential => assert_eq!( context.primary_item_coded_data().is_some(), strictness == ParseStrictness::Permissive ), Ok(context) if !essential => assert!(context.primary_item_coded_data().is_some()), _ => panic!("Expected Ok, got {:?}", result), } }); } fn assert_unsupported_nonessential(path: &str, feature: mp4::Feature) { assert_unsupported(path, feature, false); } fn assert_unsupported_essential(path: &str, feature: mp4::Feature) { assert_unsupported(path, feature, true); } #[test] fn public_avif_a1lx() { assert_unsupported_nonessential(AVIF_A1LX, mp4::Feature::A1lx); } #[test] fn public_avif_a1lx_marked_essential() { assert_avif_shall(IMAGE_AVIF_A1LX_MARKED_ESSENTIAL, Status::A1lxEssential); } #[test] fn public_avif_a1op() { assert_unsupported_essential(AVIF_A1OP, mp4::Feature::A1op); } #[test] fn public_avif_a1op_missing_essential() { assert_avif_shall(IMAGE_AVIF_A1OP_MISSING_ESSENTIAL, Status::A1opNoEssential); } #[test] fn public_avif_lsel() { assert_unsupported_essential(AVIF_LSEL, mp4::Feature::Lsel); } #[test] fn public_avif_lsel_missing_essential() { assert_avif_shall(IMAGE_AVIF_LSEL_MISSING_ESSENTIAL, Status::LselNoEssential); } #[test] fn public_avif_clap() { assert_unsupported_essential(AVIF_CLAP, mp4::Feature::Clap); } #[test] fn public_avif_grid() { for file in &[AVIF_GRID, AVIF_GRID_A1LX] { let input = &mut File::open(file).expect(file); assert_unsupported_nonfatal( &mp4::read_avif(input, ParseStrictness::Normal), mp4::Feature::Grid, ); } } #[test] fn public_avis_major_no_pitm() { let input = &mut File::open(AVIF_AVIS_MAJOR_NO_PITM).expect("Unknown file"); match mp4::read_avif(input, ParseStrictness::Normal) { Ok(context) => { assert_eq!(context.major_brand, mp4::AVIS_BRAND); assert!(context.primary_item_coded_data().is_none()); assert!(context.sequence.is_some()); } Err(e) => panic!("Expected Ok(_), found {:?}", e), } } #[test] fn public_avis_major_with_pitm_and_alpha() { let input = &mut File::open(AVIF_AVIS_MAJOR_WITH_PITM_AND_ALPHA).expect("Unknown file"); match mp4::read_avif(input, ParseStrictness::Normal) { Ok(context) => { assert_eq!(context.major_brand, mp4::AVIS_BRAND); assert!(context.primary_item_coded_data().is_some()); assert!(context.alpha_item_coded_data().is_some()); match context.sequence { Some(sequence) => { assert!(!sequence.tracks.is_empty()); assert_eq!(sequence.tracks[0].looped, None); } None => panic!("Expected sequence"), } } Err(e) => panic!("Expected Ok(_), found {:?}", e), } } #[test] fn public_avif_avis_major_no_moov() { assert_avif_shall(AVIF_AVIS_MAJOR_NO_MOOV, Status::MoovMissing); } #[test] fn public_avif_avis_with_no_pitm_no_iloc() { let input = &mut File::open(AVIF_AVIS_WITH_NO_PITM_NO_ILOC).expect("Unknown file"); match mp4::read_avif(input, ParseStrictness::Normal) { Ok(context) => { assert_eq!(context.major_brand, mp4::AVIS_BRAND); match context.sequence { Some(sequence) => { assert!(!sequence.tracks.is_empty()); assert_eq!(sequence.tracks[0].looped, Some(false)); } None => panic!("Expected sequence"), } } Err(e) => panic!("Expected Ok(_), found {:?}", e), } } #[test] fn public_avif_avis_with_pitm_no_iloc() { assert_avif_should(AVIF_AVIS_WITH_PITM_NO_ILOC, Status::PitmNotFound); } #[test] fn public_avif_valid_with_garbage_overread_at_end() { assert_avif_should( IMAGE_AVIF_VALID_WITH_GARBAGE_OVERREAD_AT_END, Status::CheckParserStateErr, ); } #[test] fn public_avif_valid_with_garbage_byte_at_end() { assert_avif_should(IMAGE_AVIF_VALID_WITH_GARBAGE_BYTE_AT_END, Status::Ok); } #[test] fn public_avif_bad_video_sample_entry() { let input = &mut File::open(IMAGE_AVIF_WIDE_BOX_SIZE_0).expect("Unknown file"); assert_eq!( Status::from(mp4::read_avif(input, ParseStrictness::Normal)), Status::BoxBadWideSize ); } fn public_avis_loop_impl(path: &str, looped: bool) { let input = &mut File::open(path).expect("Unknown file"); match mp4::read_avif(input, ParseStrictness::Normal) { Ok(context) => match context.sequence { Some(sequence) => { assert!(!sequence.tracks.is_empty()); assert_eq!(sequence.tracks[0].looped, Some(looped)); if looped { assert!(sequence.tracks[0].edited_duration.is_some()); } } None => panic!( "Expected sequence in {}", AVIF_AVIS_MAJOR_WITH_PITM_AND_ALPHA ), }, Err(e) => panic!("Expected Ok(_), found {:?}", e), } } #[test] fn public_avif_avis_no_loop() { public_avis_loop_impl(AVIF_AVIS_NO_LOOP, false); } #[test] fn public_avif_avis_loop_forever() { public_avis_loop_impl(AVIF_AVIS_LOOP_FOREVER, true); } #[test] fn public_avif_read_samples() { public_avif_read_samples_impl(ParseStrictness::Normal); } #[test] #[ignore] // See https://github.com/AOMediaCodec/av1-avif/issues/146 fn public_avif_read_samples_strict() { public_avif_read_samples_impl(ParseStrictness::Strict); } fn to_canonical_paths(strs: &[&str]) -> Vec { strs.iter() .map(std::fs::canonicalize) .map(Result::unwrap) .collect() } fn public_avif_read_samples_impl(strictness: ParseStrictness) { let corrupt_images = to_canonical_paths(AV1_AVIF_CORRUPT_IMAGES); let unsupported_images = to_canonical_paths(AVIF_UNSUPPORTED_IMAGES); let legal_no_pixi_images = if cfg!(feature = "missing-pixi-permitted") { to_canonical_paths(AVIF_NO_PIXI_IMAGES) } else { vec![] }; for dir in AVIF_TEST_DIRS { for entry in walkdir::WalkDir::new(dir) { let entry = entry.expect("AVIF entry"); let path = entry.path(); let extension = path.extension().unwrap_or_default(); if !path.is_file() || (extension != "avif" && extension != "avifs") { eprintln!("Skipping {path:?}"); continue; // Skip directories, ReadMe.txt, etc. } let corrupt = (path.canonicalize().unwrap().parent().unwrap() == std::fs::canonicalize(AVIF_CORRUPT_IMAGES_DIR).unwrap() || corrupt_images.contains(&path.canonicalize().unwrap())) && !legal_no_pixi_images.contains(&path.canonicalize().unwrap()); let unsupported = unsupported_images.contains(&path.canonicalize().unwrap()); println!( "parsing {}{}{:?}", if corrupt { "(corrupt) " } else { "" }, if unsupported { "(unsupported) " } else { "" }, path, ); let input = &mut File::open(path).expect("Unknow file"); match mp4::read_avif(input, strictness) { Ok(c) if unsupported || corrupt => { if unsupported { assert!(!c.unsupported_features.is_empty()); } else { panic!("Expected error parsing {:?}, found:\n{:?}", path, c) } } Ok(c) => { assert!( c.unsupported_features.is_empty(), "{:?}", c.unsupported_features ); eprintln!("Successfully parsed {path:?}") } Err(e) if corrupt => { eprintln!("Expected error parsing corrupt input {path:?}: {e:?}") } Err(e) => panic!("Unexpected error parsing {:?}: {:?}", path, e), } } } } #[test] fn public_video_h263() { let mut fd = File::open(VIDEO_H263_3GP).expect("Unknown file"); let mut buf = Vec::new(); fd.read_to_end(&mut buf).expect("File error"); let mut c = Cursor::new(&buf); let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); for track in context.tracks { let stsd = track.stsd.expect("expected an stsd"); let v = match stsd.descriptions.first().expect("expected a SampleEntry") { mp4::SampleEntry::Video(ref v) => v, _ => panic!("expected a VideoSampleEntry"), }; assert_eq!(v.codec_type, mp4::CodecType::H263); assert_eq!(v.width, 176); assert_eq!(v.height, 144); let _codec_specific = match v.codec_specific { mp4::VideoCodecSpecific::H263Config(_) => true, _ => { panic!("expected a H263Config",); } }; } } #[test] #[cfg(feature = "3gpp")] fn public_audio_amrnb() { let mut fd = File::open(AUDIO_AMRNB_3GP).expect("Unknown file"); let mut buf = Vec::new(); fd.read_to_end(&mut buf).expect("File error"); let mut c = Cursor::new(&buf); let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); for track in context.tracks { let stsd = track.stsd.expect("expected an stsd"); let a = match stsd.descriptions.first().expect("expected a SampleEntry") { mp4::SampleEntry::Audio(ref v) => v, _ => panic!("expected a AudioSampleEntry"), }; assert!(a.codec_type == mp4::CodecType::AMRNB); let _codec_specific = match a.codec_specific { mp4::AudioCodecSpecific::AMRSpecificBox(_) => true, _ => { panic!("expected a AMRSpecificBox",); } }; } } #[test] #[cfg(feature = "3gpp")] fn public_audio_amrwb() { let mut fd = File::open(AUDIO_AMRWB_3GP).expect("Unknown file"); let mut buf = Vec::new(); fd.read_to_end(&mut buf).expect("File error"); let mut c = Cursor::new(&buf); let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); for track in context.tracks { let stsd = track.stsd.expect("expected an stsd"); let a = match stsd.descriptions.first().expect("expected a SampleEntry") { mp4::SampleEntry::Audio(ref v) => v, _ => panic!("expected a AudioSampleEntry"), }; assert!(a.codec_type == mp4::CodecType::AMRWB); let _codec_specific = match a.codec_specific { mp4::AudioCodecSpecific::AMRSpecificBox(_) => true, _ => { panic!("expected a AMRSpecificBox",); } }; } } #[test] #[cfg(feature = "mp4v")] fn public_video_mp4v() { let mut fd = File::open(VIDEO_MP4V_MP4).expect("Unknown file"); let mut buf = Vec::new(); fd.read_to_end(&mut buf).expect("File error"); let mut c = Cursor::new(&buf); let context = mp4::read_mp4(&mut c).expect("read_mp4 failed"); for track in context.tracks { let stsd = track.stsd.expect("expected an stsd"); let v = match stsd.descriptions.first().expect("expected a SampleEntry") { mp4::SampleEntry::Video(ref v) => v, _ => panic!("expected a VideoSampleEntry"), }; assert_eq!(v.codec_type, mp4::CodecType::MP4V); assert_eq!(v.width, 176); assert_eq!(v.height, 144); let _codec_specific = match v.codec_specific { mp4::VideoCodecSpecific::ESDSConfig(_) => true, _ => { panic!("expected a ESDSConfig",); } }; } }