From a4c400cc69ec3bcf5101aa6a44dde782d16fb380 Mon Sep 17 00:00:00 2001 From: Psychotoxical Date: Tue, 7 Apr 2026 18:54:10 +0200 Subject: [PATCH] fix(audio): correctly parse iTunSMPB gapless info from older iTunes M4A files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M4A files store the iTunSMPB value behind a 16-byte binary 'data' atom header. The previous parser took bytes directly after the ASCII key, landing in the binary header and producing a corrupted parts array (total_valid=348 instead of ~13M samples), which caused take_duration to exhaust the source in ~8ms — silent no-playback. Fix: search for the " 00000000 " sentinel within 128 bytes of the key to locate the actual value string, falling back to the old offset for ID3v2/MP3 files where no binary header exists. Also patches symphonia-format-isomp4 (local crate under patches/) to: - Tolerate SL descriptor predefined=0x01 (older iTunes M4A) - Convert descriptor.unwrap() panic to a proper decode error - Gracefully skip malformed trak atoms (e.g. MJPEG cover-art streams) instead of failing the entire probe Co-Authored-By: Claude Sonnet 4.6 --- src-tauri/Cargo.lock | 4 +- src-tauri/Cargo.toml | 8 + .../patches/symphonia-format-isomp4/.cargo-ok | 1 + .../.cargo_vcs_info.json | 6 + .../symphonia-format-isomp4/Cargo.lock | 94 +++ .../symphonia-format-isomp4/Cargo.toml | 59 ++ .../symphonia-format-isomp4/Cargo.toml.orig | 20 + .../patches/symphonia-format-isomp4/README.md | 15 + .../symphonia-format-isomp4/src/atoms/alac.rs | 60 ++ .../symphonia-format-isomp4/src/atoms/co64.rs | 41 + .../symphonia-format-isomp4/src/atoms/ctts.rs | 29 + .../symphonia-format-isomp4/src/atoms/edts.rs | 43 + .../symphonia-format-isomp4/src/atoms/elst.rs | 71 ++ .../symphonia-format-isomp4/src/atoms/esds.rs | 368 +++++++++ .../symphonia-format-isomp4/src/atoms/flac.rs | 77 ++ .../symphonia-format-isomp4/src/atoms/ftyp.rs | 54 ++ .../symphonia-format-isomp4/src/atoms/hdlr.rs | 80 ++ .../symphonia-format-isomp4/src/atoms/ilst.rs | 767 ++++++++++++++++++ .../symphonia-format-isomp4/src/atoms/mdhd.rs | 93 +++ .../symphonia-format-isomp4/src/atoms/mdia.rs | 63 ++ .../symphonia-format-isomp4/src/atoms/mehd.rs | 41 + .../symphonia-format-isomp4/src/atoms/meta.rs | 66 ++ .../symphonia-format-isomp4/src/atoms/mfhd.rs | 35 + .../symphonia-format-isomp4/src/atoms/minf.rs | 54 ++ .../symphonia-format-isomp4/src/atoms/mod.rs | 477 +++++++++++ .../symphonia-format-isomp4/src/atoms/moof.rs | 60 ++ .../symphonia-format-isomp4/src/atoms/moov.rs | 103 +++ .../symphonia-format-isomp4/src/atoms/mvex.rs | 51 ++ .../symphonia-format-isomp4/src/atoms/mvhd.rs | 80 ++ .../symphonia-format-isomp4/src/atoms/opus.rs | 72 ++ .../symphonia-format-isomp4/src/atoms/sidx.rs | 89 ++ .../symphonia-format-isomp4/src/atoms/smhd.rs | 40 + .../symphonia-format-isomp4/src/atoms/stbl.rs | 108 +++ .../symphonia-format-isomp4/src/atoms/stco.rs | 41 + .../symphonia-format-isomp4/src/atoms/stsc.rs | 109 +++ .../symphonia-format-isomp4/src/atoms/stsd.rs | 539 ++++++++++++ .../symphonia-format-isomp4/src/atoms/stss.rs | 28 + .../symphonia-format-isomp4/src/atoms/stsz.rs | 58 ++ .../symphonia-format-isomp4/src/atoms/stts.rs | 105 +++ .../symphonia-format-isomp4/src/atoms/tfhd.rs | 84 ++ .../symphonia-format-isomp4/src/atoms/tkhd.rs | 89 ++ .../symphonia-format-isomp4/src/atoms/traf.rs | 64 ++ .../symphonia-format-isomp4/src/atoms/trak.rs | 64 ++ .../symphonia-format-isomp4/src/atoms/trex.rs | 48 ++ .../symphonia-format-isomp4/src/atoms/trun.rs | 321 ++++++++ .../symphonia-format-isomp4/src/atoms/udta.rs | 53 ++ .../symphonia-format-isomp4/src/atoms/wave.rs | 41 + .../symphonia-format-isomp4/src/demuxer.rs | 606 ++++++++++++++ .../symphonia-format-isomp4/src/fourcc.rs | 31 + .../patches/symphonia-format-isomp4/src/fp.rs | 78 ++ .../symphonia-format-isomp4/src/lib.rs | 22 + .../symphonia-format-isomp4/src/stream.rs | 444 ++++++++++ src-tauri/src/audio.rs | 60 +- 53 files changed, 6101 insertions(+), 13 deletions(-) create mode 100644 src-tauri/patches/symphonia-format-isomp4/.cargo-ok create mode 100644 src-tauri/patches/symphonia-format-isomp4/.cargo_vcs_info.json create mode 100644 src-tauri/patches/symphonia-format-isomp4/Cargo.lock create mode 100644 src-tauri/patches/symphonia-format-isomp4/Cargo.toml create mode 100644 src-tauri/patches/symphonia-format-isomp4/Cargo.toml.orig create mode 100644 src-tauri/patches/symphonia-format-isomp4/README.md create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/alac.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/co64.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/ctts.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/edts.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/elst.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/esds.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/flac.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/ftyp.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/hdlr.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/ilst.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/mdhd.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/mdia.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/mehd.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/meta.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/mfhd.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/minf.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/mod.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/moof.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/moov.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/mvex.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/mvhd.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/opus.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/sidx.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/smhd.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/stbl.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/stco.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/stsc.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/stsd.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/stss.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/stsz.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/stts.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/tfhd.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/tkhd.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/traf.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/trak.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/trex.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/trun.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/udta.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/atoms/wave.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/demuxer.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/fourcc.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/fp.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/lib.rs create mode 100644 src-tauri/patches/symphonia-format-isomp4/src/stream.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index bd978e76..6063ffdc 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3339,7 +3339,7 @@ dependencies = [ [[package]] name = "psysonic" -version = "1.34.1" +version = "1.34.2" dependencies = [ "biquad", "discord-rich-presence", @@ -4504,8 +4504,6 @@ dependencies = [ [[package]] name = "symphonia-format-isomp4" version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5" dependencies = [ "encoding_rs", "log", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 627c25ba..5ba55503 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -44,3 +44,11 @@ souvlaki = { version = "0.8", default-features = false, features = ["use_zbus"] discord-rich-presence = "0.2" url = "2" thread-priority = "1" + +[patch.crates-io] +# Local patch for Symphonia's isomp4 demuxer: +# - Fixes descriptor.unwrap() panic on malformed esds atoms (older iTunes M4A) +# - Tolerates SL predefined=0x01 (null) used by some older iTunes-purchased files +# - Gracefully skips malformed trak atoms (e.g. MJPEG cover-art streams) instead +# of failing the entire probe +symphonia-format-isomp4 = { path = "patches/symphonia-format-isomp4" } diff --git a/src-tauri/patches/symphonia-format-isomp4/.cargo-ok b/src-tauri/patches/symphonia-format-isomp4/.cargo-ok new file mode 100644 index 00000000..5f8b7958 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/.cargo-ok @@ -0,0 +1 @@ +{"v":1} \ No newline at end of file diff --git a/src-tauri/patches/symphonia-format-isomp4/.cargo_vcs_info.json b/src-tauri/patches/symphonia-format-isomp4/.cargo_vcs_info.json new file mode 100644 index 00000000..7b7792bb --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "6d533f26150953a882a6a111ebd13f0abf7129d5" + }, + "path_in_vcs": "symphonia-format-isomp4" +} \ No newline at end of file diff --git a/src-tauri/patches/symphonia-format-isomp4/Cargo.lock b/src-tauri/patches/symphonia-format-isomp4/Cargo.lock new file mode 100644 index 00000000..46f41e07 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/Cargo.lock @@ -0,0 +1,94 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytemuck" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "symphonia-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" +dependencies = [ + "arrayvec", + "bitflags", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.5" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] diff --git a/src-tauri/patches/symphonia-format-isomp4/Cargo.toml b/src-tauri/patches/symphonia-format-isomp4/Cargo.toml new file mode 100644 index 00000000..8e70b26b --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/Cargo.toml @@ -0,0 +1,59 @@ +# 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" +rust-version = "1.53" +name = "symphonia-format-isomp4" +version = "0.5.5" +authors = ["Philip Deljanov "] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Pure Rust ISO/MP4 demuxer (a part of project Symphonia)." +homepage = "https://github.com/pdeljanov/Symphonia" +readme = "README.md" +keywords = [ + "audio", + "media", + "demuxer", + "mp4", + "iso", +] +categories = [ + "multimedia", + "multimedia::audio", + "multimedia::encoding", +] +license = "MPL-2.0" +repository = "https://github.com/pdeljanov/Symphonia" + +[lib] +name = "symphonia_format_isomp4" +path = "src/lib.rs" + +[dependencies.encoding_rs] +version = "0.8.17" + +[dependencies.log] +version = "0.4" + +[dependencies.symphonia-core] +version = "0.5.5" + +[dependencies.symphonia-metadata] +version = "0.5.5" + +[dependencies.symphonia-utils-xiph] +version = "0.5.5" diff --git a/src-tauri/patches/symphonia-format-isomp4/Cargo.toml.orig b/src-tauri/patches/symphonia-format-isomp4/Cargo.toml.orig new file mode 100644 index 00000000..752e3fb8 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/Cargo.toml.orig @@ -0,0 +1,20 @@ +[package] +name = "symphonia-format-isomp4" +version = "0.5.5" +description = "Pure Rust ISO/MP4 demuxer (a part of project Symphonia)." +homepage = "https://github.com/pdeljanov/Symphonia" +repository = "https://github.com/pdeljanov/Symphonia" +authors = ["Philip Deljanov "] +license = "MPL-2.0" +readme = "README.md" +categories = ["multimedia", "multimedia::audio", "multimedia::encoding"] +keywords = ["audio", "media", "demuxer", "mp4", "iso"] +edition = "2018" +rust-version = "1.53" + +[dependencies] +encoding_rs = "0.8.17" +log = "0.4" +symphonia-core = { version = "0.5.5", path = "../symphonia-core" } +symphonia-metadata = { version = "0.5.5", path = "../symphonia-metadata" } +symphonia-utils-xiph = { version = "0.5.5", path = "../symphonia-utils-xiph" } \ No newline at end of file diff --git a/src-tauri/patches/symphonia-format-isomp4/README.md b/src-tauri/patches/symphonia-format-isomp4/README.md new file mode 100644 index 00000000..f7b68f71 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/README.md @@ -0,0 +1,15 @@ +# Symphonia ISO/MP4 Demuxer + +[![Docs](https://docs.rs/symphonia-format-isomp4/badge.svg)](https://docs.rs/symphonia-format-isomp4) + +ISO/MP4 demuxer for Project Symphonia. + +**Note:** This crate is part of Project Symphonia. Please use the [`symphonia`](https://crates.io/crates/symphonia) crate instead of this one directly. + +## License + +Symphonia is provided under the MPL v2.0 license. Please refer to the LICENSE file for more details. + +## Contributing + +Symphonia is a free and open-source project that welcomes contributions! To get started, please read our [Contribution Guidelines](https://github.com/pdeljanov/Symphonia/tree/master/CONTRIBUTING.md). diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/alac.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/alac.rs new file mode 100644 index 00000000..776a6d70 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/alac.rs @@ -0,0 +1,60 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::codecs::{CodecParameters, CODEC_TYPE_ALAC}; +use symphonia_core::errors::{decode_error, unsupported_error, Result}; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; + +#[allow(dead_code)] +#[derive(Debug)] +pub struct AlacAtom { + /// Atom header. + header: AtomHeader, + /// ALAC extra data (magic cookie). + extra_data: Box<[u8]>, +} + +impl Atom for AlacAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (version, flags) = AtomHeader::read_extra(reader)?; + + if version != 0 { + return unsupported_error("isomp4 (alac): unsupported alac version"); + } + + if flags != 0 { + return decode_error("isomp4 (alac): flags not zero"); + } + + if header.data_len <= AtomHeader::EXTRA_DATA_SIZE { + return decode_error("isomp4 (alac): invalid alac atom length"); + } + + // The ALAC magic cookie (aka extra data) is either 24 or 48 bytes long. + let magic_len = match header.data_len - AtomHeader::EXTRA_DATA_SIZE { + len @ 24 | len @ 48 => len as usize, + _ => return decode_error("isomp4 (alac): invalid magic cookie length"), + }; + + // Read the magic cookie. + let extra_data = reader.read_boxed_slice_exact(magic_len)?; + + Ok(AlacAtom { header, extra_data }) + } +} + +impl AlacAtom { + pub fn fill_codec_params(&self, codec_params: &mut CodecParameters) { + codec_params.for_codec(CODEC_TYPE_ALAC).with_extra_data(self.extra_data.clone()); + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/co64.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/co64.rs new file mode 100644 index 00000000..dea46f7c --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/co64.rs @@ -0,0 +1,41 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::Result; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; + +/// Chunk offset atom (64-bit version). +#[allow(dead_code)] +#[derive(Debug)] +pub struct Co64Atom { + /// Atom header. + header: AtomHeader, + pub chunk_offsets: Vec, +} + +impl Atom for Co64Atom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (_, _) = AtomHeader::read_extra(reader)?; + + let entry_count = reader.read_be_u32()?; + + // TODO: Apply a limit. + let mut chunk_offsets = Vec::with_capacity(entry_count as usize); + + for _ in 0..entry_count { + chunk_offsets.push(reader.read_be_u64()?); + } + + Ok(Co64Atom { header, chunk_offsets }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/ctts.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/ctts.rs new file mode 100644 index 00000000..88aff87a --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/ctts.rs @@ -0,0 +1,29 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::Result; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; + +/// Composition time atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct CttsAtom { + /// Atom header. + header: AtomHeader, +} + +impl Atom for CttsAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(_reader: &mut B, _header: AtomHeader) -> Result { + todo!() + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/edts.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/edts.rs new file mode 100644 index 00000000..2c341646 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/edts.rs @@ -0,0 +1,43 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::Result; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader, AtomIterator, AtomType, ElstAtom}; + +/// Edits atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct EdtsAtom { + header: AtomHeader, + pub elst: Option, +} + +impl Atom for EdtsAtom { + fn header(&self) -> AtomHeader { + self.header + } + + #[allow(clippy::single_match)] + fn read(reader: &mut B, header: AtomHeader) -> Result { + let mut iter = AtomIterator::new(reader, header); + + let mut elst = None; + + while let Some(header) = iter.next()? { + match header.atype { + AtomType::EditList => { + elst = Some(iter.read_atom::()?); + } + _ => (), + } + } + + Ok(EdtsAtom { header, elst }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/elst.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/elst.rs new file mode 100644 index 00000000..d551296e --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/elst.rs @@ -0,0 +1,71 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Result}; +use symphonia_core::io::ReadBytes; +use symphonia_core::util::bits; + +use crate::atoms::{Atom, AtomHeader}; + +/// Edit list entry. +#[derive(Debug)] +#[allow(dead_code)] +pub struct ElstEntry { + segment_duration: u64, + media_time: i64, + media_rate_int: i16, + media_rate_frac: i16, +} + +/// Edit list atom. +#[derive(Debug)] +#[allow(dead_code)] +pub struct ElstAtom { + header: AtomHeader, + entries: Vec, +} + +impl Atom for ElstAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (version, _) = AtomHeader::read_extra(reader)?; + + // TODO: Apply a limit. + let entry_count = reader.read_be_u32()?; + + let mut entries = Vec::new(); + + for _ in 0..entry_count { + let (segment_duration, media_time) = match version { + 0 => ( + u64::from(reader.read_be_u32()?), + i64::from(bits::sign_extend_leq32_to_i32(reader.read_be_u32()?, 32)), + ), + 1 => ( + reader.read_be_u64()?, + bits::sign_extend_leq64_to_i64(reader.read_be_u64()?, 64), + ), + _ => return decode_error("isomp4: invalid tkhd version"), + }; + + let media_rate_int = bits::sign_extend_leq16_to_i16(reader.read_be_u16()?, 16); + let media_rate_frac = bits::sign_extend_leq16_to_i16(reader.read_be_u16()?, 16); + + entries.push(ElstEntry { + segment_duration, + media_time, + media_rate_int, + media_rate_frac, + }); + } + + Ok(ElstAtom { header, entries }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/esds.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/esds.rs new file mode 100644 index 00000000..b883e26d --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/esds.rs @@ -0,0 +1,368 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::codecs::{ + CodecParameters, CodecType, CODEC_TYPE_AAC, CODEC_TYPE_MP3, CODEC_TYPE_NULL, +}; +use symphonia_core::errors::{decode_error, unsupported_error, Result}; +use symphonia_core::io::{FiniteStream, ReadBytes, ScopedStream}; + +use crate::atoms::{Atom, AtomHeader}; + +use log::{debug, warn}; + +const ES_DESCRIPTOR: u8 = 0x03; +const DECODER_CONFIG_DESCRIPTOR: u8 = 0x04; +const DECODER_SPECIFIC_DESCRIPTOR: u8 = 0x05; +const SL_CONFIG_DESCRIPTOR: u8 = 0x06; + +const MIN_DESCRIPTOR_SIZE: u64 = 2; + +fn read_descriptor_header(reader: &mut B) -> Result<(u8, u32)> { + let tag = reader.read_u8()?; + + let mut size = 0; + + for _ in 0..4 { + let val = reader.read_u8()?; + size = (size << 7) | u32::from(val & 0x7f); + if val & 0x80 == 0 { + break; + } + } + + Ok((tag, size)) +} + +#[allow(dead_code)] +#[derive(Debug)] +pub struct EsdsAtom { + /// Atom header. + header: AtomHeader, + /// Elementary stream descriptor. + descriptor: ESDescriptor, +} + +impl Atom for EsdsAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (_, _) = AtomHeader::read_extra(reader)?; + + let mut descriptor = None; + + let mut scoped = ScopedStream::new(reader, header.data_len - AtomHeader::EXTRA_DATA_SIZE); + + while scoped.bytes_available() > MIN_DESCRIPTOR_SIZE { + let (desc, desc_len) = read_descriptor_header(&mut scoped)?; + + match desc { + ES_DESCRIPTOR => { + descriptor = Some(ESDescriptor::read(&mut scoped, desc_len)?); + } + _ => { + warn!("unknown descriptor in esds atom, desc={}", desc); + scoped.ignore_bytes(desc_len as u64)?; + } + } + } + + // Ignore remainder of the atom. + scoped.ignore()?; + + // Guard against malformed esds atoms that contain no ES_DESCRIPTOR (old iTunes files). + let descriptor = descriptor + .ok_or_else(|| symphonia_core::errors::Error::DecodeError("isomp4: missing es descriptor in esds atom"))?; + + Ok(EsdsAtom { header, descriptor }) + } +} + +impl EsdsAtom { + pub fn fill_codec_params(&self, codec_params: &mut CodecParameters) { + codec_params.for_codec(self.descriptor.dec_config.codec_type); + + if let Some(ds_config) = &self.descriptor.dec_config.dec_specific_info { + codec_params.with_extra_data(ds_config.extra_data.clone()); + } + } +} + +pub trait ObjectDescriptor: Sized { + fn read(reader: &mut B, len: u32) -> Result; +} + +/* +class ES_Descriptor extends BaseDescriptor : bit(8) tag=ES_DescrTag { + bit(16) ES_ID; + bit(1) streamDependenceFlag; + bit(1) URL_Flag; + bit(1) OCRstreamFlag; + bit(5) streamPriority; + if (streamDependenceFlag) + bit(16) dependsOn_ES_ID; + if (URL_Flag) { + bit(8) URLlength; + bit(8) URLstring[URLlength]; + } + if (OCRstreamFlag) + bit(16) OCR_ES_Id; + DecoderConfigDescriptor decConfigDescr; + SLConfigDescriptor slConfigDescr; + IPI_DescrPointer ipiPtr[0 .. 1]; + IP_IdentificationDataSet ipIDS[0 .. 255]; + IPMP_DescriptorPointer ipmpDescrPtr[0 .. 255]; + LanguageDescriptor langDescr[0 .. 255]; + QoS_Descriptor qosDescr[0 .. 1]; + RegistrationDescriptor regDescr[0 .. 1]; + ExtensionDescriptor extDescr[0 .. 255]; +} +*/ + +#[allow(dead_code)] +#[derive(Debug)] +pub struct ESDescriptor { + pub es_id: u16, + pub dec_config: DecoderConfigDescriptor, + pub sl_config: SLDescriptor, +} + +impl ObjectDescriptor for ESDescriptor { + fn read(reader: &mut B, len: u32) -> Result { + let es_id = reader.read_be_u16()?; + let es_flags = reader.read_u8()?; + + // Stream dependence flag. + if es_flags & 0x80 != 0 { + let _depends_on_es_id = reader.read_u16()?; + } + + // URL flag. + if es_flags & 0x40 != 0 { + let url_len = reader.read_u8()?; + reader.ignore_bytes(u64::from(url_len))?; + } + + // OCR stream flag. + if es_flags & 0x20 != 0 { + let _ocr_es_id = reader.read_u16()?; + } + + let mut dec_config = None; + let mut sl_config = None; + + let mut scoped = ScopedStream::new(reader, u64::from(len) - 3); + + // Multiple descriptors follow, but only the decoder configuration descriptor is useful. + while scoped.bytes_available() > MIN_DESCRIPTOR_SIZE { + let (desc, desc_len) = read_descriptor_header(&mut scoped)?; + + match desc { + DECODER_CONFIG_DESCRIPTOR => { + dec_config = Some(DecoderConfigDescriptor::read(&mut scoped, desc_len)?); + } + SL_CONFIG_DESCRIPTOR => { + sl_config = Some(SLDescriptor::read(&mut scoped, desc_len)?); + } + _ => { + debug!("skipping {} object in es descriptor", desc); + scoped.ignore_bytes(u64::from(desc_len))?; + } + } + } + + // Consume remaining bytes. + scoped.ignore()?; + + // Decoder configuration descriptor is mandatory. + if dec_config.is_none() { + return decode_error("isomp4: missing decoder config descriptor"); + } + + // SL descriptor is mandatory. + if sl_config.is_none() { + return decode_error("isomp4: missing sl config descriptor"); + } + + Ok(ESDescriptor { es_id, dec_config: dec_config.unwrap(), sl_config: sl_config.unwrap() }) + } +} + +/* +class DecoderConfigDescriptor extends BaseDescriptor : bit(8) tag=DecoderConfigDescrTag { + bit(8) objectTypeIndication; + bit(6) streamType; + bit(1) upStream; + const bit(1) reserved=1; + bit(24) bufferSizeDB; + bit(32) maxBitrate; + bit(32) avgBitrate; + DecoderSpecificInfo decSpecificInfo[0 .. 1]; + profileLevelIndicationIndexDescriptor profileLevelIndicationIndexDescr [0..255]; +} +*/ + +#[allow(dead_code)] +#[derive(Debug)] +pub struct DecoderConfigDescriptor { + pub codec_type: CodecType, + pub object_type_indication: u8, + pub dec_specific_info: Option, +} + +impl ObjectDescriptor for DecoderConfigDescriptor { + fn read(reader: &mut B, len: u32) -> Result { + // AAC + const OBJECT_TYPE_ISO14496_3: u8 = 0x40; + const OBJECT_TYPE_ISO13818_7_MAIN: u8 = 0x66; + const OBJECT_TYPE_ISO13818_7_LC: u8 = 0x67; + // MP3 + const OBJECT_TYPE_ISO13818_3: u8 = 0x69; + const OBJECT_TYPE_ISO11172_3: u8 = 0x6b; + + let object_type_indication = reader.read_u8()?; + + let (_stream_type, _upstream) = { + let val = reader.read_u8()?; + + if val & 0x1 != 1 { + debug!("decoder config descriptor reserved bit is not 1"); + } + + ((val & 0xfc) >> 2, (val & 0x2) >> 1) + }; + + let _buffer_size = reader.read_be_u24()?; + let _max_bitrate = reader.read_be_u32()?; + let _avg_bitrate = reader.read_be_u32()?; + + let mut dec_specific_config = None; + + let mut scoped = ScopedStream::new(reader, u64::from(len) - 13); + + // Multiple descriptors follow, but only the decoder specific info descriptor is useful. + while scoped.bytes_available() > MIN_DESCRIPTOR_SIZE { + let (desc, desc_len) = read_descriptor_header(&mut scoped)?; + + match desc { + DECODER_SPECIFIC_DESCRIPTOR => { + dec_specific_config = Some(DecoderSpecificInfo::read(&mut scoped, desc_len)?); + } + _ => { + debug!("skipping {} object in decoder config descriptor", desc); + scoped.ignore_bytes(u64::from(desc_len))?; + } + } + } + + let codec_type = match object_type_indication { + OBJECT_TYPE_ISO14496_3 | OBJECT_TYPE_ISO13818_7_LC | OBJECT_TYPE_ISO13818_7_MAIN => { + CODEC_TYPE_AAC + } + OBJECT_TYPE_ISO13818_3 | OBJECT_TYPE_ISO11172_3 => CODEC_TYPE_MP3, + _ => { + debug!( + "unknown object type indication {:#x} for decoder config descriptor", + object_type_indication + ); + + CODEC_TYPE_NULL + } + }; + + // Consume remaining bytes. + scoped.ignore()?; + + Ok(DecoderConfigDescriptor { + codec_type, + object_type_indication, + dec_specific_info: dec_specific_config, + }) + } +} + +#[derive(Debug)] +pub struct DecoderSpecificInfo { + pub extra_data: Box<[u8]>, +} + +impl ObjectDescriptor for DecoderSpecificInfo { + fn read(reader: &mut B, len: u32) -> Result { + Ok(DecoderSpecificInfo { extra_data: reader.read_boxed_slice_exact(len as usize)? }) + } +} + +/* +class SLConfigDescriptor extends BaseDescriptor : bit(8) tag=SLConfigDescrTag { + bit(8) predefined; + if (predefined==0) { + bit(1) useAccessUnitStartFlag; + bit(1) useAccessUnitEndFlag; + bit(1) useRandomAccessPointFlag; + bit(1) hasRandomAccessUnitsOnlyFlag; + bit(1) usePaddingFlag; + bit(1) useTimeStampsFlag; + bit(1) useIdleFlag; + bit(1) durationFlag; + bit(32) timeStampResolution; + bit(32) OCRResolution; + bit(8) timeStampLength; // must be 64 + bit(8) OCRLength; // must be 64 + bit(8) AU_Length; // must be 32 + bit(8) instantBitrateLength; + bit(4) degradationPriorityLength; + bit(5) AU_seqNumLength; // must be 16 + bit(5) packetSeqNumLength; // must be 16 + bit(2) reserved=0b11; + } + if (durationFlag) { + bit(32) timeScale; + bit(16) accessUnitDuration; + bit(16) compositionUnitDuration; + } + if (!useTimeStampsFlag) { + bit(timeStampLength) startDecodingTimeStamp; + bit(timeStampLength) startCompositionTimeStamp; + } +} + +timeStampLength == 32, for predefined == 0x1 +timeStampLength == 0, for predefined == 0x2 +*/ +#[derive(Debug)] +pub struct SLDescriptor; + +impl ObjectDescriptor for SLDescriptor { + fn read(reader: &mut B, len: u32) -> Result { + // const SLCONFIG_PREDEFINED_CUSTOM: u8 = 0x0; + const SLCONFIG_PREDEFINED_NULL: u8 = 0x1; // older iTunes M4A + const SLCONFIG_PREDEFINED_MP4: u8 = 0x2; + + let predefined = reader.read_u8()?; + + match predefined { + SLCONFIG_PREDEFINED_MP4 => { + // Standard MP4: no extra fields. Nothing to read. + } + SLCONFIG_PREDEFINED_NULL => { + // Older iTunes files use predefined=0x1. The SL descriptor in + // this mode has no additional fields beyond the predefined byte, + // so we just ignore the remaining bytes. + if len > 1 { + reader.ignore_bytes(u64::from(len - 1))?; + } + } + _ => { + return unsupported_error("isomp4: sl descriptor predefined not mp4 or null"); + } + } + + Ok(SLDescriptor {}) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/flac.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/flac.rs new file mode 100644 index 00000000..17bb0af0 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/flac.rs @@ -0,0 +1,77 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::codecs::{CodecParameters, VerificationCheck, CODEC_TYPE_FLAC}; +use symphonia_core::errors::{decode_error, unsupported_error, Result}; +use symphonia_core::io::{BufReader, ReadBytes}; + +use symphonia_utils_xiph::flac::metadata::{MetadataBlockHeader, MetadataBlockType, StreamInfo}; + +use crate::atoms::{Atom, AtomHeader}; + +#[allow(dead_code)] +#[derive(Debug)] +pub struct FlacAtom { + /// Atom header. + header: AtomHeader, + /// FLAC stream info block. + stream_info: StreamInfo, + /// FLAC extra data. + extra_data: Box<[u8]>, +} + +impl Atom for FlacAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (version, flags) = AtomHeader::read_extra(reader)?; + + if version != 0 { + return unsupported_error("isomp4 (flac): unsupported flac version"); + } + + if flags != 0 { + return decode_error("isomp4 (flac): flags not zero"); + } + + // The first block must be the stream information block. + let block_header = MetadataBlockHeader::read(reader)?; + + if block_header.block_type != MetadataBlockType::StreamInfo { + return decode_error("isomp4 (flac): first block is not stream info"); + } + + // Ensure the block length is correct for a stream information block before allocating a + // buffer for it. + if !StreamInfo::is_valid_size(u64::from(block_header.block_len)) { + return decode_error("isomp4 (flac): invalid stream info block length"); + } + + let extra_data = reader.read_boxed_slice_exact(block_header.block_len as usize)?; + let stream_info = StreamInfo::read(&mut BufReader::new(&extra_data))?; + + Ok(FlacAtom { header, stream_info, extra_data }) + } +} + +impl FlacAtom { + pub fn fill_codec_params(&self, codec_params: &mut CodecParameters) { + codec_params + .for_codec(CODEC_TYPE_FLAC) + .with_sample_rate(self.stream_info.sample_rate) + .with_bits_per_sample(self.stream_info.bits_per_sample) + .with_channels(self.stream_info.channels) + .with_packet_data_integrity(true) + .with_extra_data(self.extra_data.clone()); + + if let Some(md5) = self.stream_info.md5 { + codec_params.with_verification_code(VerificationCheck::Md5(md5)); + } + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/ftyp.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/ftyp.rs new file mode 100644 index 00000000..8dfb60e7 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/ftyp.rs @@ -0,0 +1,54 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Result}; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; +use crate::fourcc::FourCc; + +/// File type atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct FtypAtom { + header: AtomHeader, + pub major: FourCc, + pub minor: [u8; 4], + pub compatible: Vec, +} + +impl Atom for FtypAtom { + fn read(reader: &mut B, header: AtomHeader) -> Result { + // The Ftyp atom must be have a data length that is known, and it must be a multiple of 4 + // since it only stores FourCCs. + if header.data_len < 8 || header.data_len & 0x3 != 0 { + return decode_error("isomp4: invalid ftyp data length"); + } + + // Major + let major = FourCc::new(reader.read_quad_bytes()?); + + // Minor + let minor = reader.read_quad_bytes()?; + + // The remainder of the Ftyp atom contains the FourCCs of compatible brands. + let n_brands = (header.data_len - 8) / 4; + + let mut compatible = Vec::new(); + + for _ in 0..n_brands { + let brand = reader.read_quad_bytes()?; + compatible.push(FourCc::new(brand)); + } + + Ok(FtypAtom { header, major, minor, compatible }) + } + + fn header(&self) -> AtomHeader { + self.header + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/hdlr.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/hdlr.rs new file mode 100644 index 00000000..3cdffed7 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/hdlr.rs @@ -0,0 +1,80 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::Result; +use symphonia_core::io::ReadBytes; + +use crate::{ + atoms::{Atom, AtomHeader}, + fourcc::FourCc, +}; + +use log::warn; + +/// Handler type. +#[derive(Debug, PartialEq, Eq)] +pub enum HandlerType { + /// Video handler. + Video, + /// Audio handler. + Sound, + /// Subtitle handler. + Subtitle, + /// Metadata handler. + Metadata, + /// Text handler. + Text, + /// Unknown handler type. + Other([u8; 4]), +} + +/// Handler atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct HdlrAtom { + /// Atom header. + header: AtomHeader, + /// Handler type. + pub handler_type: HandlerType, + /// Human-readable handler name. + pub name: String, +} + +impl Atom for HdlrAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (_, _) = AtomHeader::read_extra(reader)?; + + // Always 0 for MP4, but for Quicktime this contains the component type. + let _ = reader.read_quad_bytes()?; + + let handler_type = match &reader.read_quad_bytes()? { + b"vide" => HandlerType::Video, + b"soun" => HandlerType::Sound, + b"meta" => HandlerType::Metadata, + b"subt" => HandlerType::Subtitle, + b"text" => HandlerType::Text, + &hdlr => { + warn!("unknown handler type {:?}", FourCc::new(hdlr)); + HandlerType::Other(hdlr) + } + }; + + // These bytes are reserved for MP4, but for QuickTime they contain the component + // manufacturer, flags, and flags mask. + reader.ignore_bytes(4 * 3)?; + + // Human readable UTF-8 string of the track type. + let buf = reader.read_boxed_slice_exact((header.data_len - 24) as usize)?; + let name = String::from_utf8_lossy(&buf).to_string(); + + Ok(HdlrAtom { header, handler_type, name }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/ilst.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/ilst.rs new file mode 100644 index 00000000..9a766f61 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/ilst.rs @@ -0,0 +1,767 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Result}; +use symphonia_core::io::{BufReader, ReadBytes}; +use symphonia_core::meta::{ + MetadataBuilder, MetadataRevision, StandardTagKey, StandardVisualKey, Tag, +}; +use symphonia_core::meta::{Value, Visual}; +use symphonia_core::util::bits; +use symphonia_metadata::{id3v1, itunes}; + +use crate::atoms::{Atom, AtomHeader, AtomIterator, AtomType}; + +use encoding_rs::{SHIFT_JIS, UTF_16BE}; +use log::warn; + +/// Data type enumeration for metadata value atoms as defined in the QuickTime File Format standard. +#[allow(dead_code)] +#[derive(Debug, Copy, Clone)] +pub enum DataType { + AffineTransformF64, + Bmp, + DimensionsF32, + Float32, + Float64, + Jpeg, + /// The data type is implicit to the atom. + NoType, + Png, + PointF32, + QuickTimeMetadata, + RectF32, + ShiftJis, + SignedInt16, + SignedInt32, + SignedInt64, + SignedInt8, + SignedIntVariable, + UnsignedInt16, + UnsignedInt32, + UnsignedInt64, + UnsignedInt8, + UnsignedIntVariable, + Utf16, + Utf16Sort, + Utf8, + Utf8Sort, + Unknown(u32), +} + +impl From for DataType { + fn from(value: u32) -> Self { + match value { + 0 => DataType::NoType, + 1 => DataType::Utf8, + 2 => DataType::Utf16, + 3 => DataType::ShiftJis, + 4 => DataType::Utf8Sort, + 5 => DataType::Utf16Sort, + 13 => DataType::Jpeg, + 14 => DataType::Png, + 21 => DataType::SignedIntVariable, + 22 => DataType::UnsignedIntVariable, + 23 => DataType::Float32, + 24 => DataType::Float64, + 27 => DataType::Bmp, + 28 => DataType::QuickTimeMetadata, + 65 => DataType::SignedInt8, + 66 => DataType::SignedInt16, + 67 => DataType::SignedInt32, + 70 => DataType::PointF32, + 71 => DataType::DimensionsF32, + 72 => DataType::RectF32, + 74 => DataType::SignedInt64, + 75 => DataType::UnsignedInt8, + 76 => DataType::UnsignedInt16, + 77 => DataType::UnsignedInt32, + 78 => DataType::UnsignedInt64, + 79 => DataType::AffineTransformF64, + _ => DataType::Unknown(value), + } + } +} + +fn parse_no_type(data: &[u8]) -> Option { + // Latin1, potentially null-terminated. + let end = data.iter().position(|&c| c == b'\0').unwrap_or(data.len()); + let text = String::from_utf8_lossy(&data[..end]); + Some(Value::from(text)) +} + +fn parse_utf8(data: &[u8]) -> Option { + // UTF8, no null-terminator or count. + let text = String::from_utf8_lossy(data); + Some(Value::from(text)) +} + +fn parse_utf16(data: &[u8]) -> Option { + // UTF16 BE + let text = UTF_16BE.decode(data).0; + Some(Value::from(text)) +} + +fn parse_shift_jis(data: &[u8]) -> Option { + // Shift-JIS + let text = SHIFT_JIS.decode(data).0; + Some(Value::from(text)) +} + +fn parse_signed_int8(data: &[u8]) -> Option { + match data.len() { + 1 => { + let s = bits::sign_extend_leq8_to_i8(data[0], 8); + Some(Value::from(s)) + } + _ => None, + } +} + +fn parse_signed_int16(data: &[u8]) -> Option { + match data.len() { + 2 => { + let u = BufReader::new(data).read_be_u16().ok()?; + let s = bits::sign_extend_leq16_to_i16(u, 16); + Some(Value::from(s)) + } + _ => None, + } +} + +fn parse_signed_int32(data: &[u8]) -> Option { + match data.len() { + 4 => { + let u = BufReader::new(data).read_be_u32().ok()?; + let s = bits::sign_extend_leq32_to_i32(u, 32); + Some(Value::from(s)) + } + _ => None, + } +} + +fn parse_signed_int64(data: &[u8]) -> Option { + match data.len() { + 8 => { + let u = BufReader::new(data).read_be_u64().ok()?; + let s = bits::sign_extend_leq64_to_i64(u, 64); + Some(Value::from(s)) + } + _ => None, + } +} + +fn parse_var_signed_int(data: &[u8]) -> Option { + match data.len() { + 1 => parse_signed_int8(data), + 2 => parse_signed_int16(data), + 4 => parse_signed_int32(data), + _ => None, + } +} + +fn parse_unsigned_int8(data: &[u8]) -> Option { + match data.len() { + 1 => Some(Value::from(data[0])), + _ => None, + } +} + +fn parse_unsigned_int16(data: &[u8]) -> Option { + match data.len() { + 2 => { + let u = BufReader::new(data).read_be_u16().ok()?; + Some(Value::from(u)) + } + _ => None, + } +} + +fn parse_unsigned_int32(data: &[u8]) -> Option { + match data.len() { + 4 => { + let u = BufReader::new(data).read_be_u32().ok()?; + Some(Value::from(u)) + } + _ => None, + } +} + +fn parse_unsigned_int64(data: &[u8]) -> Option { + match data.len() { + 8 => { + let u = BufReader::new(data).read_be_u64().ok()?; + Some(Value::from(u)) + } + _ => None, + } +} + +fn parse_var_unsigned_int(data: &[u8]) -> Option { + match data.len() { + 1 => parse_unsigned_int8(data), + 2 => parse_unsigned_int16(data), + 4 => parse_unsigned_int32(data), + _ => None, + } +} + +fn parse_float32(data: &[u8]) -> Option { + match data.len() { + 4 => { + let f = BufReader::new(data).read_be_f32().ok()?; + Some(Value::Float(f64::from(f))) + } + _ => None, + } +} + +fn parse_float64(data: &[u8]) -> Option { + match data.len() { + 8 => { + let f = BufReader::new(data).read_be_f64().ok()?; + Some(Value::Float(f)) + } + _ => None, + } +} + +fn parse_tag_value(data_type: DataType, data: &[u8]) -> Option { + match data_type { + DataType::NoType => parse_no_type(data), + DataType::Utf8 | DataType::Utf8Sort => parse_utf8(data), + DataType::Utf16 | DataType::Utf16Sort => parse_utf16(data), + DataType::ShiftJis => parse_shift_jis(data), + DataType::UnsignedInt8 => parse_unsigned_int8(data), + DataType::UnsignedInt16 => parse_unsigned_int16(data), + DataType::UnsignedInt32 => parse_unsigned_int32(data), + DataType::UnsignedInt64 => parse_unsigned_int64(data), + DataType::UnsignedIntVariable => parse_var_unsigned_int(data), + DataType::SignedInt8 => parse_signed_int8(data), + DataType::SignedInt16 => parse_signed_int16(data), + DataType::SignedInt32 => parse_signed_int32(data), + DataType::SignedInt64 => parse_signed_int64(data), + DataType::SignedIntVariable => parse_var_signed_int(data), + DataType::Float32 => parse_float32(data), + DataType::Float64 => parse_float64(data), + _ => None, + } +} + +/// Reads and parses a `MetaTagAtom` from the provided iterator and adds it to the `MetadataBuilder` +/// if there are no errors. +fn add_generic_tag( + iter: &mut AtomIterator, + builder: &mut MetadataBuilder, + std_key: Option, +) -> Result<()> { + let tag = iter.read_atom::()?; + + for value_atom in tag.values.iter() { + // Parse the value atom data into a string, if possible. + if let Some(value) = parse_tag_value(value_atom.data_type, &value_atom.data) { + builder.add_tag(Tag::new(std_key, "", value)); + } + else { + warn!("unsupported data type {:?} for {:?} tag", value_atom.data_type, std_key); + } + } + + Ok(()) +} + +fn add_var_unsigned_int_tag( + iter: &mut AtomIterator, + builder: &mut MetadataBuilder, + std_key: StandardTagKey, +) -> Result<()> { + let tag = iter.read_atom::()?; + + if let Some(value_atom) = tag.values.first() { + if let Some(value) = parse_var_unsigned_int(&value_atom.data) { + builder.add_tag(Tag::new(Some(std_key), "", value)); + } + else { + warn!("got unexpected data for {:?} tag", std_key); + } + } + + Ok(()) +} + +fn add_var_signed_int_tag( + iter: &mut AtomIterator, + builder: &mut MetadataBuilder, + std_key: StandardTagKey, +) -> Result<()> { + let tag = iter.read_atom::()?; + + if let Some(value_atom) = tag.values.first() { + if let Some(value) = parse_var_signed_int(&value_atom.data) { + builder.add_tag(Tag::new(Some(std_key), "", value)); + } + else { + warn!("got unexpected data for {:?} tag", std_key); + } + } + + Ok(()) +} + +fn add_boolean_tag( + iter: &mut AtomIterator, + builder: &mut MetadataBuilder, + std_key: StandardTagKey, +) -> Result<()> { + let tag = iter.read_atom::()?; + + // There should only be 1 value. + if let Some(value) = tag.values.first() { + // Boolean tags are just "flags", only add a tag if the boolean is true (1). + if let Some(bool_value) = value.data.first() { + if *bool_value == 1 { + builder.add_tag(Tag::new(Some(std_key), "", Value::Flag)); + } + } + } + + Ok(()) +} + +fn add_m_of_n_tag( + iter: &mut AtomIterator, + builder: &mut MetadataBuilder, + m_key: StandardTagKey, + n_key: StandardTagKey, +) -> Result<()> { + let tag = iter.read_atom::()?; + + // There should only be 1 value. + if let Some(value) = tag.values.first() { + // The trkn and disk atoms contains an 8 byte value buffer, where the 4th and 6th bytes + // indicate the track/disk number and total number of tracks/disks, respectively. Odd. + if value.data.len() == 8 { + let m = value.data[3]; + let n = value.data[5]; + + builder.add_tag(Tag::new(Some(m_key), "", Value::from(m))); + builder.add_tag(Tag::new(Some(n_key), "", Value::from(n))); + } + } + + Ok(()) +} + +fn add_visual_tag( + iter: &mut AtomIterator, + builder: &mut MetadataBuilder, +) -> Result<()> { + let tag = iter.read_atom::()?; + + // There could be more than one attached image. + for value in tag.values { + let media_type = match value.data_type { + DataType::Bmp => "image/bmp", + DataType::Jpeg => "image/jpeg", + DataType::Png => "image/png", + _ => "", + }; + + builder.add_visual(Visual { + media_type: media_type.into(), + dimensions: None, + bits_per_pixel: None, + color_mode: None, + usage: Some(StandardVisualKey::FrontCover), + tags: Default::default(), + data: value.data, + }); + } + + Ok(()) +} + +fn add_advisory_tag( + _iter: &mut AtomIterator, + _builder: &mut MetadataBuilder, +) -> Result<()> { + Ok(()) +} + +fn add_media_type_tag( + iter: &mut AtomIterator, + builder: &mut MetadataBuilder, +) -> Result<()> { + let tag = iter.read_atom::()?; + + // There should only be 1 value. + if let Some(value) = tag.values.first() { + if let Some(media_type_value) = value.data.first() { + let media_type = match media_type_value { + 0 => "Movie", + 1 => "Normal", + 2 => "Audio Book", + 5 => "Whacked Bookmark", + 6 => "Music Video", + 9 => "Short Film", + 10 => "TV Show", + 11 => "Booklet", + _ => "Unknown", + }; + + builder.add_tag(Tag::new( + Some(StandardTagKey::MediaFormat), + "", + Value::from(media_type), + )); + } + } + + Ok(()) +} + +fn add_id3v1_genre_tag( + iter: &mut AtomIterator, + builder: &mut MetadataBuilder, +) -> Result<()> { + let tag = iter.read_atom::()?; + + // There should only be 1 value. + if let Some(value) = tag.values.first() { + // The ID3v1 genre is stored as a unsigned 16-bit big-endian integer. + let index = BufReader::new(&value.data).read_be_u16()?; + + // The stored index uses 1-based indexing, but the ID3v1 genre list is 0-based. + if index > 0 { + if let Some(genre) = id3v1::util::genre_name((index - 1) as u8) { + builder.add_tag(Tag::new(Some(StandardTagKey::Genre), "", Value::from(*genre))); + } + } + } + + Ok(()) +} + +fn add_freeform_tag( + iter: &mut AtomIterator, + builder: &mut MetadataBuilder, +) -> Result<()> { + let tag = iter.read_atom::()?; + + // A user-defined tag should only have 1 value. + for value_atom in tag.values.iter() { + // Parse the value atom data into a string, if possible. + if let Some(value) = parse_tag_value(value_atom.data_type, &value_atom.data) { + // Gets the fully qualified tag name. + let full_name = tag.full_name(); + + // Try to map iTunes freeform tags to standard tag keys. + let std_key = itunes::std_key_from_tag(&full_name); + + builder.add_tag(Tag::new(std_key, &full_name, value)); + } + else { + warn!("unsupported data type {:?} for free-form tag", value_atom.data_type); + } + } + + Ok(()) +} + +/// Metadata tag data atom. +#[allow(dead_code)] +pub struct MetaTagDataAtom { + /// Atom header. + header: AtomHeader, + /// Tag data. + pub data: Box<[u8]>, + /// The data type contained in buf. + pub data_type: DataType, +} + +impl Atom for MetaTagDataAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (version, flags) = AtomHeader::read_extra(reader)?; + + // For the mov brand, this a data type indicator and must always be 0 (well-known type). It + // specifies the table in which the next 24-bit integer specifying the actual data type + // indexes. For iso/mp4, this is a version, and there is only one version, 0. Therefore, + // flags are interpreted as the actual data type index. + if version != 0 { + return decode_error("isomp4: invalid data atom version"); + } + + let data_type = DataType::from(flags); + + // For the mov brand, the next four bytes are country and languages code. However, for + // iso/mp4 these codes should be ignored. + let _country = reader.read_be_u16()?; + let _language = reader.read_be_u16()?; + + // The data payload is the remainder of the atom. + // TODO: Apply a limit. + let data = reader + .read_boxed_slice_exact((header.data_len - AtomHeader::EXTRA_DATA_SIZE - 4) as usize)?; + + Ok(MetaTagDataAtom { header, data, data_type }) + } +} + +/// Metadata tag name and mean atom. +#[allow(dead_code)] +pub struct MetaTagNamespaceAtom { + /// Atom header. + header: AtomHeader, + /// For 'mean' atoms, this is the key namespace. For 'name' atom, this is the key name. + pub value: String, +} + +impl Atom for MetaTagNamespaceAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (_, _) = AtomHeader::read_extra(reader)?; + + let buf = reader + .read_boxed_slice_exact((header.data_len - AtomHeader::EXTRA_DATA_SIZE) as usize)?; + + // Do a lossy conversion because metadata should not prevent the demuxer from working. + let value = String::from_utf8_lossy(&buf).to_string(); + + Ok(MetaTagNamespaceAtom { header, value }) + } +} + +/// A generic metadata tag atom. +#[allow(dead_code)] +pub struct MetaTagAtom { + /// Atom header. + header: AtomHeader, + /// Tag value(s). + pub values: Vec, + /// Optional, tag key namespace. + pub mean: Option, + /// Optional, tag key name. + pub name: Option, +} + +impl MetaTagAtom { + pub fn full_name(&self) -> String { + let mut full_name = String::new(); + + if self.mean.is_some() || self.name.is_some() { + // full_name.push_str("----:"); + + if let Some(mean) = &self.mean { + full_name.push_str(&mean.value); + } + + full_name.push(':'); + + if let Some(name) = &self.name { + full_name.push_str(&name.value); + } + } + + full_name + } +} + +impl Atom for MetaTagAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let mut iter = AtomIterator::new(reader, header); + + let mut mean = None; + let mut name = None; + let mut values = Vec::new(); + + while let Some(header) = iter.next()? { + match header.atype { + AtomType::MetaTagData => { + values.push(iter.read_atom::()?); + } + AtomType::MetaTagName => { + name = Some(iter.read_atom::()?); + } + AtomType::MetaTagMeaning => { + mean = Some(iter.read_atom::()?); + } + _ => (), + } + } + + Ok(MetaTagAtom { header, values, mean, name }) + } +} + +/// User data atom. +#[allow(dead_code)] +pub struct IlstAtom { + /// Atom header. + header: AtomHeader, + /// Metadata revision. + pub metadata: MetadataRevision, +} + +impl Atom for IlstAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let mut iter = AtomIterator::new(reader, header); + + let mut mb = MetadataBuilder::new(); + + while let Some(header) = iter.next()? { + // Ignore standard atoms, check if other is a metadata atom. + match &header.atype { + AtomType::AdvisoryTag => add_advisory_tag(&mut iter, &mut mb)?, + AtomType::AlbumArtistTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::AlbumArtist))? + } + AtomType::AlbumTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Album))? + } + AtomType::ArtistLowerTag => (), + AtomType::ArtistTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Artist))? + } + AtomType::CategoryTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::PodcastCategory))? + } + AtomType::CommentTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Comment))? + } + AtomType::CompilationTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Compilation))? + } + AtomType::ComposerTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Composer))? + } + AtomType::CopyrightTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Copyright))? + } + AtomType::CoverTag => add_visual_tag(&mut iter, &mut mb)?, + AtomType::CustomGenreTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Genre))? + } + AtomType::DateTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Date))? + } + AtomType::DescriptionTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Description))? + } + AtomType::DiskNumberTag => add_m_of_n_tag( + &mut iter, + &mut mb, + StandardTagKey::DiscNumber, + StandardTagKey::DiscTotal, + )?, + AtomType::EncodedByTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::EncodedBy))? + } + AtomType::EncoderTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Encoder))? + } + AtomType::GaplessPlaybackTag => { + // TODO: Need standard tag key for gapless playback. + // add_boolean_tag(&mut iter, &mut mb, )? + } + AtomType::GenreTag => add_id3v1_genre_tag(&mut iter, &mut mb)?, + AtomType::GroupingTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::ContentGroup))? + } + AtomType::HdVideoTag => (), + AtomType::IdentPodcastTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::IdentPodcast))? + } + AtomType::KeywordTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::PodcastKeywords))? + } + AtomType::LongDescriptionTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Description))? + } + AtomType::LyricsTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Lyrics))? + } + AtomType::MediaTypeTag => add_media_type_tag(&mut iter, &mut mb)?, + AtomType::OwnerTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Owner))? + } + AtomType::PodcastTag => { + add_boolean_tag(&mut iter, &mut mb, StandardTagKey::Podcast)? + } + AtomType::PurchaseDateTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::PurchaseDate))? + } + AtomType::RatingTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::Rating))? + } + AtomType::SortAlbumArtistTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::SortAlbumArtist))? + } + AtomType::SortAlbumTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::SortAlbum))? + } + AtomType::SortArtistTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::SortArtist))? + } + AtomType::SortComposerTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::SortComposer))? + } + AtomType::SortNameTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::SortTrackTitle))? + } + AtomType::TempoTag => { + add_var_signed_int_tag(&mut iter, &mut mb, StandardTagKey::Bpm)? + } + AtomType::TrackNumberTag => add_m_of_n_tag( + &mut iter, + &mut mb, + StandardTagKey::TrackNumber, + StandardTagKey::TrackTotal, + )?, + AtomType::TrackTitleTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::TrackTitle))? + } + AtomType::TvEpisodeNameTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::TvEpisodeTitle))? + } + AtomType::TvEpisodeNumberTag => { + add_var_unsigned_int_tag(&mut iter, &mut mb, StandardTagKey::TvEpisode)? + } + AtomType::TvNetworkNameTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::TvNetwork))? + } + AtomType::TvSeasonNumberTag => { + add_var_unsigned_int_tag(&mut iter, &mut mb, StandardTagKey::TvSeason)? + } + AtomType::TvShowNameTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::TvShowTitle))? + } + AtomType::UrlPodcastTag => { + add_generic_tag(&mut iter, &mut mb, Some(StandardTagKey::UrlPodcast))? + } + AtomType::FreeFormTag => add_freeform_tag(&mut iter, &mut mb)?, + _ => (), + } + } + + Ok(IlstAtom { header, metadata: mb.metadata() }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/mdhd.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/mdhd.rs new file mode 100644 index 00000000..ccda1b48 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/mdhd.rs @@ -0,0 +1,93 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Result}; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; + +fn parse_language(code: u16) -> String { + // An ISO language code outside of these bounds is not valid. + if code < 0x400 || code > 0x7fff { + String::new() + } + else { + let chars = [ + ((code >> 10) & 0x1f) as u8 + 0x60, + ((code >> 5) & 0x1f) as u8 + 0x60, + ((code >> 0) & 0x1f) as u8 + 0x60, + ]; + + String::from_utf8_lossy(&chars).to_string() + } +} + +/// Media header atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct MdhdAtom { + /// Atom header. + header: AtomHeader, + /// Creation time. + pub ctime: u64, + /// Modification time. + pub mtime: u64, + /// Timescale. + pub timescale: u32, + /// Duration of the media in timescale units. + pub duration: u64, + /// Language. + pub language: String, +} + +impl Atom for MdhdAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (version, _) = AtomHeader::read_extra(reader)?; + + let mut mdhd = MdhdAtom { + header, + ctime: 0, + mtime: 0, + timescale: 0, + duration: 0, + language: String::new(), + }; + + match version { + 0 => { + mdhd.ctime = u64::from(reader.read_be_u32()?); + mdhd.mtime = u64::from(reader.read_be_u32()?); + mdhd.timescale = reader.read_be_u32()?; + // 0xffff_ffff is a special case. + mdhd.duration = match reader.read_be_u32()? { + u32::MAX => u64::MAX, + duration => u64::from(duration), + }; + } + 1 => { + mdhd.ctime = reader.read_be_u64()?; + mdhd.mtime = reader.read_be_u64()?; + mdhd.timescale = reader.read_be_u32()?; + mdhd.duration = reader.read_be_u64()?; + } + _ => { + return decode_error("isomp4: invalid mdhd version"); + } + } + + mdhd.language = parse_language(reader.read_be_u16()?); + + // Quality + let _ = reader.read_be_u16()?; + + Ok(mdhd) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/mdia.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/mdia.rs new file mode 100644 index 00000000..d19f9ccc --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/mdia.rs @@ -0,0 +1,63 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Result}; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader, AtomIterator, AtomType, HdlrAtom, MdhdAtom, MinfAtom}; + +#[allow(dead_code)] +#[derive(Debug)] +pub struct MdiaAtom { + header: AtomHeader, + pub mdhd: MdhdAtom, + pub hdlr: HdlrAtom, + pub minf: MinfAtom, +} + +impl Atom for MdiaAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let mut iter = AtomIterator::new(reader, header); + + let mut mdhd = None; + let mut hdlr = None; + let mut minf = None; + + while let Some(header) = iter.next()? { + match header.atype { + AtomType::MediaHeader => { + mdhd = Some(iter.read_atom::()?); + } + AtomType::Handler => { + hdlr = Some(iter.read_atom::()?); + } + AtomType::MediaInfo => { + minf = Some(iter.read_atom::()?); + } + _ => (), + } + } + + if mdhd.is_none() { + return decode_error("isomp4: missing mdhd atom"); + } + + if hdlr.is_none() { + return decode_error("isomp4: missing hdlr atom"); + } + + if minf.is_none() { + return decode_error("isomp4: missing minf atom"); + } + + Ok(MdiaAtom { header, mdhd: mdhd.unwrap(), hdlr: hdlr.unwrap(), minf: minf.unwrap() }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/mehd.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/mehd.rs new file mode 100644 index 00000000..a7b5ef61 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/mehd.rs @@ -0,0 +1,41 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Result}; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; + +/// Movie extends header atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct MehdAtom { + /// Atom header. + header: AtomHeader, + /// Fragment duration. + pub fragment_duration: u64, +} + +impl Atom for MehdAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (version, _) = AtomHeader::read_extra(reader)?; + + let fragment_duration = match version { + 0 => u64::from(reader.read_be_u32()?), + 1 => reader.read_be_u64()?, + _ => { + return decode_error("isomp4: invalid mehd version"); + } + }; + + Ok(MehdAtom { header, fragment_duration }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/meta.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/meta.rs new file mode 100644 index 00000000..9f2bddf9 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/meta.rs @@ -0,0 +1,66 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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::Debug; + +use symphonia_core::errors::Result; +use symphonia_core::io::ReadBytes; +use symphonia_core::meta::MetadataRevision; + +use crate::atoms::{Atom, AtomHeader, AtomIterator, AtomType, IlstAtom}; + +/// User data atom. +#[allow(dead_code)] +pub struct MetaAtom { + /// Atom header. + header: AtomHeader, + /// Metadata revision. + pub metadata: Option, +} + +impl Debug for MetaAtom { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "(redacted)") + } +} + +impl MetaAtom { + /// If metadata was read, consumes the metadata and returns it. + pub fn take_metadata(&mut self) -> Option { + self.metadata.take() + } +} + +impl Atom for MetaAtom { + fn header(&self) -> AtomHeader { + self.header + } + + #[allow(clippy::single_match)] + fn read(reader: &mut B, mut header: AtomHeader) -> Result { + let (_, _) = AtomHeader::read_extra(reader)?; + + // AtomIterator doesn't know the extra data was read already, so the extra data size must be + // subtrated from the atom's data length. + header.data_len -= AtomHeader::EXTRA_DATA_SIZE; + + let mut iter = AtomIterator::new(reader, header); + + let mut metadata = None; + + while let Some(header) = iter.next()? { + match header.atype { + AtomType::MetaList => { + metadata = Some(iter.read_atom::()?.metadata); + } + _ => (), + } + } + + Ok(MetaAtom { header, metadata }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/mfhd.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/mfhd.rs new file mode 100644 index 00000000..9cae6d7a --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/mfhd.rs @@ -0,0 +1,35 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::Result; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; + +/// Movie fragment header atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct MfhdAtom { + /// Atom header. + header: AtomHeader, + /// Sequence number associated with fragment. + pub sequence_number: u32, +} + +impl Atom for MfhdAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (_, _) = AtomHeader::read_extra(reader)?; + + let sequence_number = reader.read_be_u32()?; + + Ok(MfhdAtom { header, sequence_number }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/minf.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/minf.rs new file mode 100644 index 00000000..ef010479 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/minf.rs @@ -0,0 +1,54 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Result}; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader, AtomIterator, AtomType, SmhdAtom, StblAtom}; + +/// Media information atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct MinfAtom { + /// Atom header. + header: AtomHeader, + /// Sound media header atom. + pub smhd: Option, + /// Sample table atom. + pub stbl: StblAtom, +} + +impl Atom for MinfAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let mut iter = AtomIterator::new(reader, header); + + let mut smhd = None; + let mut stbl = None; + + while let Some(header) = iter.next()? { + match header.atype { + AtomType::SoundMediaHeader => { + smhd = Some(iter.read_atom::()?); + } + AtomType::SampleTable => { + stbl = Some(iter.read_atom::()?); + } + _ => (), + } + } + + if stbl.is_none() { + return decode_error("isomp4: missing stbl atom"); + } + + Ok(MinfAtom { header, smhd, stbl: stbl.unwrap() }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/mod.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/mod.rs new file mode 100644 index 00000000..b23bd87f --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/mod.rs @@ -0,0 +1,477 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Result}; +use symphonia_core::io::ReadBytes; + +pub(crate) mod alac; +pub(crate) mod co64; +pub(crate) mod ctts; +pub(crate) mod edts; +pub(crate) mod elst; +pub(crate) mod esds; +pub(crate) mod flac; +pub(crate) mod ftyp; +pub(crate) mod hdlr; +pub(crate) mod ilst; +pub(crate) mod mdhd; +pub(crate) mod mdia; +pub(crate) mod mehd; +pub(crate) mod meta; +pub(crate) mod mfhd; +pub(crate) mod minf; +pub(crate) mod moof; +pub(crate) mod moov; +pub(crate) mod mvex; +pub(crate) mod mvhd; +pub(crate) mod opus; +pub(crate) mod sidx; +pub(crate) mod smhd; +pub(crate) mod stbl; +pub(crate) mod stco; +pub(crate) mod stsc; +pub(crate) mod stsd; +pub(crate) mod stss; +pub(crate) mod stsz; +pub(crate) mod stts; +pub(crate) mod tfhd; +pub(crate) mod tkhd; +pub(crate) mod traf; +pub(crate) mod trak; +pub(crate) mod trex; +pub(crate) mod trun; +pub(crate) mod udta; +pub(crate) mod wave; + +pub use self::meta::MetaAtom; +pub use alac::AlacAtom; +pub use co64::Co64Atom; +#[allow(unused_imports)] +pub use ctts::CttsAtom; +pub use edts::EdtsAtom; +pub use elst::ElstAtom; +pub use esds::EsdsAtom; +pub use flac::FlacAtom; +pub use ftyp::FtypAtom; +pub use hdlr::HdlrAtom; +pub use ilst::IlstAtom; +pub use mdhd::MdhdAtom; +pub use mdia::MdiaAtom; +pub use mehd::MehdAtom; +pub use mfhd::MfhdAtom; +pub use minf::MinfAtom; +pub use moof::MoofAtom; +pub use moov::MoovAtom; +pub use mvex::MvexAtom; +pub use mvhd::MvhdAtom; +pub use opus::OpusAtom; +pub use sidx::SidxAtom; +pub use smhd::SmhdAtom; +pub use stbl::StblAtom; +pub use stco::StcoAtom; +pub use stsc::StscAtom; +pub use stsd::StsdAtom; +#[allow(unused_imports)] +pub use stss::StssAtom; +pub use stsz::StszAtom; +pub use stts::SttsAtom; +pub use tfhd::TfhdAtom; +pub use tkhd::TkhdAtom; +pub use traf::TrafAtom; +pub use trak::TrakAtom; +pub use trex::TrexAtom; +pub use trun::TrunAtom; +pub use udta::UdtaAtom; +pub use wave::WaveAtom; + +/// Atom types. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum AtomType { + Ac3, + AdvisoryTag, + Alac, + ALaw, + AlbumArtistTag, + AlbumTag, + ArtistLowerTag, + ArtistTag, + CategoryTag, + ChunkOffset, + ChunkOffset64, + CommentTag, + CompilationTag, + ComposerTag, + CompositionTimeToSample, + CopyrightTag, + CoverTag, + CustomGenreTag, + DateTag, + DescriptionTag, + DiskNumberTag, + Edit, + EditList, + EncodedByTag, + EncoderTag, + Esds, + F32SampleEntry, + F64SampleEntry, + FileType, + Flac, + FlacDsConfig, + Free, + FreeFormTag, + GaplessPlaybackTag, + GenreTag, + GroupingTag, + Handler, + HdVideoTag, + IdentPodcastTag, + KeywordTag, + LongDescriptionTag, + Lpcm, + LyricsTag, + Media, + MediaData, + MediaHeader, + MediaInfo, + MediaTypeTag, + Meta, + MetaList, + MetaTagData, + MetaTagMeaning, + MetaTagName, + Movie, + MovieExtends, + MovieExtendsHeader, + MovieFragment, + MovieFragmentHeader, + MovieHeader, + Mp3, + Mp4a, + MuLaw, + Opus, + OpusDsConfig, + OwnerTag, + PodcastTag, + PurchaseDateTag, + QtWave, + RatingTag, + S16BeSampleEntry, + S16LeSampleEntry, + S24SampleEntry, + S32SampleEntry, + SampleDescription, + SampleSize, + SampleTable, + SampleToChunk, + SegmentIndex, + Skip, + SortAlbumArtistTag, + SortAlbumTag, + SortArtistTag, + SortComposerTag, + SortNameTag, + SoundMediaHeader, + SyncSample, + TempoTag, + TimeToSample, + Track, + TrackExtends, + TrackFragment, + TrackFragmentHeader, + TrackFragmentRun, + TrackHeader, + TrackNumberTag, + TrackTitleTag, + TvEpisodeNameTag, + TvEpisodeNumberTag, + TvNetworkNameTag, + TvSeasonNumberTag, + TvShowNameTag, + U8SampleEntry, + UrlPodcastTag, + UserData, + Other([u8; 4]), +} + +impl From<[u8; 4]> for AtomType { + fn from(val: [u8; 4]) -> Self { + match &val { + b".mp3" => AtomType::Mp3, + b"ac-3" => AtomType::Ac3, + b"alac" => AtomType::Alac, + b"alaw" => AtomType::ALaw, + b"co64" => AtomType::ChunkOffset64, + b"ctts" => AtomType::CompositionTimeToSample, + b"data" => AtomType::MetaTagData, + b"dfLa" => AtomType::FlacDsConfig, + b"dOps" => AtomType::OpusDsConfig, + b"edts" => AtomType::Edit, + b"elst" => AtomType::EditList, + b"esds" => AtomType::Esds, + b"fl32" => AtomType::F32SampleEntry, + b"fl64" => AtomType::F64SampleEntry, + b"fLaC" => AtomType::Flac, + b"free" => AtomType::Free, + b"ftyp" => AtomType::FileType, + b"hdlr" => AtomType::Handler, + b"ilst" => AtomType::MetaList, + b"in24" => AtomType::S24SampleEntry, + b"in32" => AtomType::S32SampleEntry, + b"lpcm" => AtomType::Lpcm, + b"mdat" => AtomType::MediaData, + b"mdhd" => AtomType::MediaHeader, + b"mdia" => AtomType::Media, + b"mean" => AtomType::MetaTagMeaning, + b"mehd" => AtomType::MovieExtendsHeader, + b"meta" => AtomType::Meta, + b"mfhd" => AtomType::MovieFragmentHeader, + b"minf" => AtomType::MediaInfo, + b"moof" => AtomType::MovieFragment, + b"moov" => AtomType::Movie, + b"mp4a" => AtomType::Mp4a, + b"mvex" => AtomType::MovieExtends, + b"mvhd" => AtomType::MovieHeader, + b"name" => AtomType::MetaTagName, + b"Opus" => AtomType::Opus, + b"raw " => AtomType::U8SampleEntry, + b"sidx" => AtomType::SegmentIndex, + b"skip" => AtomType::Skip, + b"smhd" => AtomType::SoundMediaHeader, + b"sowt" => AtomType::S16LeSampleEntry, + b"stbl" => AtomType::SampleTable, + b"stco" => AtomType::ChunkOffset, + b"stsc" => AtomType::SampleToChunk, + b"stsd" => AtomType::SampleDescription, + b"stss" => AtomType::SyncSample, + b"stsz" => AtomType::SampleSize, + b"stts" => AtomType::TimeToSample, + b"tfhd" => AtomType::TrackFragmentHeader, + b"tkhd" => AtomType::TrackHeader, + b"traf" => AtomType::TrackFragment, + b"trak" => AtomType::Track, + b"trex" => AtomType::TrackExtends, + b"trun" => AtomType::TrackFragmentRun, + b"twos" => AtomType::S16BeSampleEntry, + b"udta" => AtomType::UserData, + b"ulaw" => AtomType::MuLaw, + b"wave" => AtomType::QtWave, + // Metadata Boxes + b"----" => AtomType::FreeFormTag, + b"aART" => AtomType::AlbumArtistTag, + b"catg" => AtomType::CategoryTag, + b"covr" => AtomType::CoverTag, + b"cpil" => AtomType::CompilationTag, + b"cprt" => AtomType::CopyrightTag, + b"desc" => AtomType::DescriptionTag, + b"disk" => AtomType::DiskNumberTag, + b"egid" => AtomType::IdentPodcastTag, + b"gnre" => AtomType::GenreTag, + b"hdvd" => AtomType::HdVideoTag, + b"keyw" => AtomType::KeywordTag, + b"ldes" => AtomType::LongDescriptionTag, + b"ownr" => AtomType::OwnerTag, + b"pcst" => AtomType::PodcastTag, + b"pgap" => AtomType::GaplessPlaybackTag, + b"purd" => AtomType::PurchaseDateTag, + b"purl" => AtomType::UrlPodcastTag, + b"rate" => AtomType::RatingTag, + b"rtng" => AtomType::AdvisoryTag, + b"soaa" => AtomType::SortAlbumArtistTag, + b"soal" => AtomType::SortAlbumTag, + b"soar" => AtomType::SortArtistTag, + b"soco" => AtomType::SortComposerTag, + b"sonm" => AtomType::SortNameTag, + b"stik" => AtomType::MediaTypeTag, + b"tmpo" => AtomType::TempoTag, + b"trkn" => AtomType::TrackNumberTag, + b"tven" => AtomType::TvEpisodeNameTag, + b"tves" => AtomType::TvEpisodeNumberTag, + b"tvnn" => AtomType::TvNetworkNameTag, + b"tvsh" => AtomType::TvShowNameTag, + b"tvsn" => AtomType::TvSeasonNumberTag, + b"\xa9alb" => AtomType::AlbumTag, + b"\xa9art" => AtomType::ArtistLowerTag, + b"\xa9ART" => AtomType::ArtistTag, + b"\xa9cmt" => AtomType::CommentTag, + b"\xa9day" => AtomType::DateTag, + b"\xa9enc" => AtomType::EncodedByTag, + b"\xa9gen" => AtomType::CustomGenreTag, + b"\xa9grp" => AtomType::GroupingTag, + b"\xa9lyr" => AtomType::LyricsTag, + b"\xa9nam" => AtomType::TrackTitleTag, + b"\xa9too" => AtomType::EncoderTag, + b"\xa9wrt" => AtomType::ComposerTag, + _ => AtomType::Other(val), + } + } +} + +/// Common atom header. +#[derive(Copy, Clone, Debug)] +pub struct AtomHeader { + /// The atom type. + pub atype: AtomType, + /// The total size of the atom including the header. + pub atom_len: u64, + /// The size of the payload data. + pub data_len: u64, +} + +impl AtomHeader { + const HEADER_SIZE: u64 = 8; + const EXTENDED_HEADER_SIZE: u64 = AtomHeader::HEADER_SIZE + 8; + const EXTRA_DATA_SIZE: u64 = 4; + + /// Reads an atom header from the provided `ByteStream`. + pub fn read(reader: &mut B) -> Result { + let mut atom_len = u64::from(reader.read_be_u32()?); + let atype = AtomType::from(reader.read_quad_bytes()?); + + let data_len = match atom_len { + 0 => 0, + 1 => { + atom_len = reader.read_be_u64()?; + + // The atom size should be atleast the length of the header. + if atom_len < AtomHeader::EXTENDED_HEADER_SIZE { + return decode_error("isomp4: atom size is invalid"); + } + + atom_len - AtomHeader::EXTENDED_HEADER_SIZE + } + _ => { + // The atom size should be atleast the length of the header. + if atom_len < AtomHeader::HEADER_SIZE { + return decode_error("isomp4: atom size is invalid"); + } + + atom_len - AtomHeader::HEADER_SIZE + } + }; + + Ok(AtomHeader { atype, atom_len, data_len }) + } + + #[allow(dead_code)] + pub fn base_header_len(&self) -> u64 { + match self.atom_len { + 0 => AtomHeader::HEADER_SIZE, + _ => self.atom_len - self.data_len, + } + } + + /// For applicable atoms, reads the atom header extra data: a tuple composed of a u8 version + /// number, and a u24 bitset of flags. + pub fn read_extra(reader: &mut B) -> Result<(u8, u32)> { + Ok((reader.read_u8()?, reader.read_be_u24()?)) + } +} + +pub trait Atom: Sized { + #[allow(dead_code)] + fn header(&self) -> AtomHeader; + + fn read(reader: &mut B, header: AtomHeader) -> Result; +} + +pub struct AtomIterator { + reader: B, + len: Option, + cur_atom: Option, + base_pos: u64, + next_atom_pos: u64, +} + +impl AtomIterator { + pub fn new_root(reader: B, len: Option) -> Self { + let base_pos = reader.pos(); + + AtomIterator { reader, len, cur_atom: None, base_pos, next_atom_pos: base_pos } + } + + pub fn new(reader: B, container: AtomHeader) -> Self { + let base_pos = reader.pos(); + + AtomIterator { + reader, + len: Some(container.data_len), + cur_atom: None, + base_pos, + next_atom_pos: base_pos, + } + } + + pub fn into_inner(self) -> B { + self.reader + } + + pub fn inner_mut(&mut self) -> &mut B { + &mut self.reader + } + + pub fn next(&mut self) -> Result> { + // Ignore any remaining data in the current atom that was not read. + let cur_pos = self.reader.pos(); + + if cur_pos < self.next_atom_pos { + self.reader.ignore_bytes(self.next_atom_pos - cur_pos)?; + } + else if cur_pos > self.next_atom_pos { + // This is very bad, either the atom's length was incorrect or the demuxer erroroneously + // overread an atom. + return decode_error("isomp4: overread atom"); + } + + // If len is specified, then do not read more than len bytes. + if let Some(len) = self.len { + if self.next_atom_pos - self.base_pos >= len { + return Ok(None); + } + } + + // Read the next atom header. + let atom = AtomHeader::read(&mut self.reader)?; + + // Calculate the start position for the next atom (the exclusive end of the current atom). + self.next_atom_pos = match atom.atom_len { + 0 => { + // An atom with a length of zero is defined to span to the end of the stream. If + // len is available, use it for the next atom start position, otherwise, use u64 max + // which will trip an end of stream error on the next iteration. + self.len.map(|l| self.base_pos + l).unwrap_or(u64::MAX) + } + + len => self.next_atom_pos + len, + }; + + self.cur_atom = Some(atom); + + Ok(self.cur_atom) + } + + pub fn next_no_consume(&mut self) -> Result> { + if self.cur_atom.is_some() { + Ok(self.cur_atom) + } + else { + self.next() + } + } + + pub fn read_atom(&mut self) -> Result { + // It is not possible to read the current atom more than once because ByteStream is not + // seekable. Therefore, raise an assert if read_atom is called more than once between calls + // to next, or after next returns None. + assert!(self.cur_atom.is_some()); + A::read(&mut self.reader, self.cur_atom.take().unwrap()) + } + + pub fn consume_atom(&mut self) { + assert!(self.cur_atom.take().is_some()); + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/moof.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/moof.rs new file mode 100644 index 00000000..51a7ec54 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/moof.rs @@ -0,0 +1,60 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Result}; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader, AtomIterator, AtomType, MfhdAtom, TrafAtom}; + +/// Movie fragment atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct MoofAtom { + /// Atom header. + header: AtomHeader, + /// The position of the first byte of this moof atom. This is used as the anchor point for the + /// subsequent track atoms. + pub moof_base_pos: u64, + /// Movie fragment header. + pub mfhd: MfhdAtom, + /// Track fragments. + pub trafs: Vec, +} + +impl Atom for MoofAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let moof_base_pos = reader.pos() - AtomHeader::HEADER_SIZE; + + let mut mfhd = None; + let mut trafs = Vec::new(); + + let mut iter = AtomIterator::new(reader, header); + + while let Some(header) = iter.next()? { + match header.atype { + AtomType::MovieFragmentHeader => { + mfhd = Some(iter.read_atom::()?); + } + AtomType::TrackFragment => { + let traf = iter.read_atom::()?; + trafs.push(traf); + } + _ => (), + } + } + + if mfhd.is_none() { + return decode_error("isomp4: missing mfhd atom"); + } + + Ok(MoofAtom { header, moof_base_pos, mfhd: mfhd.unwrap(), trafs }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/moov.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/moov.rs new file mode 100644 index 00000000..ddfe3639 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/moov.rs @@ -0,0 +1,103 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Result}; +use symphonia_core::io::ReadBytes; +use symphonia_core::meta::MetadataRevision; + +use crate::atoms::{ + Atom, AtomHeader, AtomIterator, AtomType, MvexAtom, MvhdAtom, TrakAtom, UdtaAtom, +}; + +use log::warn; + +/// Movie atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct MoovAtom { + /// Atom header. + header: AtomHeader, + /// Movie header atom. + pub mvhd: MvhdAtom, + /// Trak atoms. + pub traks: Vec, + /// Movie extends atom. The presence of this atom indicates a fragmented stream. + pub mvex: Option, + /// User data (usually metadata). + pub udta: Option, +} + +impl MoovAtom { + /// If metadata was read, consumes the metadata and returns it. + pub fn take_metadata(&mut self) -> Option { + self.udta.as_mut().and_then(|udta| udta.take_metadata()) + } + + /// Is the movie segmented. + pub fn is_fragmented(&self) -> bool { + self.mvex.is_some() + } +} + +impl Atom for MoovAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let mut iter = AtomIterator::new(reader, header); + + let mut mvhd = None; + let mut traks = Vec::new(); + let mut mvex = None; + let mut udta = None; + + while let Some(header) = iter.next()? { + match header.atype { + AtomType::MovieHeader => { + mvhd = Some(iter.read_atom::()?); + } + AtomType::Track => { + // Gracefully skip malformed tracks (e.g. MJPEG cover-art streams + // in older iTunes M4A files whose stbl or stsd may be missing + // required atoms). The AtomIterator's next_atom_pos was already + // set before read_atom was called, so a failure mid-parse still + // leaves the iterator positioned at the end of this atom. + match iter.read_atom::() { + Ok(trak) => traks.push(trak), + Err(e) => warn!("isomp4: skipping malformed trak atom: {}", e), + } + } + AtomType::MovieExtends => { + mvex = Some(iter.read_atom::()?); + } + AtomType::UserData => { + udta = Some(iter.read_atom::()?); + } + _ => (), + } + } + + if mvhd.is_none() { + return decode_error("isomp4: missing mvhd atom"); + } + + // If fragmented, the mvex atom should contain a trex atom for each trak atom in moov. + if let Some(mvex) = mvex.as_ref() { + // For each trak, find a matching trex atom using the track id. + for trak in traks.iter() { + let found = mvex.trexs.iter().any(|trex| trex.track_id == trak.tkhd.id); + + if !found { + warn!("missing trex atom for trak with id={}", trak.tkhd.id); + } + } + } + + Ok(MoovAtom { header, mvhd: mvhd.unwrap(), traks, mvex, udta }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/mvex.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/mvex.rs new file mode 100644 index 00000000..c0fec4fd --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/mvex.rs @@ -0,0 +1,51 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::Result; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader, AtomIterator, AtomType, MehdAtom, TrexAtom}; + +/// Movie extends atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct MvexAtom { + /// Atom header. + pub header: AtomHeader, + /// Movie extends header, optional. + pub mehd: Option, + /// Track extends box, one per track. + pub trexs: Vec, +} + +impl Atom for MvexAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let mut iter = AtomIterator::new(reader, header); + + let mut mehd = None; + let mut trexs = Vec::new(); + + while let Some(header) = iter.next()? { + match header.atype { + AtomType::MovieExtendsHeader => { + mehd = Some(iter.read_atom::()?); + } + AtomType::TrackExtends => { + let trex = iter.read_atom::()?; + trexs.push(trex); + } + _ => (), + } + } + + Ok(MvexAtom { header, mehd, trexs }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/mvhd.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/mvhd.rs new file mode 100644 index 00000000..10d35ff5 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/mvhd.rs @@ -0,0 +1,80 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Result}; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; +use crate::fp::FpU8; + +/// Movie header atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct MvhdAtom { + /// Atom header. + pub header: AtomHeader, + /// The creation time. + pub ctime: u64, + /// The modification time. + pub mtime: u64, + /// Timescale for the movie expressed as the number of units per second. + pub timescale: u32, + /// The duration of the movie in `timescale` units. + pub duration: u64, + /// The preferred volume to play the movie. + pub volume: FpU8, +} + +impl Atom for MvhdAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (version, _) = AtomHeader::read_extra(reader)?; + + let mut mvhd = MvhdAtom { + header, + ctime: 0, + mtime: 0, + timescale: 0, + duration: 0, + volume: Default::default(), + }; + + // Version 0 uses 32-bit time values, verion 1 used 64-bit values. + match version { + 0 => { + mvhd.ctime = u64::from(reader.read_be_u32()?); + mvhd.mtime = u64::from(reader.read_be_u32()?); + mvhd.timescale = reader.read_be_u32()?; + // 0xffff_ffff is a special case. + mvhd.duration = match reader.read_be_u32()? { + u32::MAX => u64::MAX, + duration => u64::from(duration), + }; + } + 1 => { + mvhd.ctime = reader.read_be_u64()?; + mvhd.mtime = reader.read_be_u64()?; + mvhd.timescale = reader.read_be_u32()?; + mvhd.duration = reader.read_be_u64()?; + } + _ => return decode_error("isomp4: invalid mvhd version"), + } + + // Ignore the preferred playback rate. + let _ = reader.read_be_u32()?; + + // Preferred volume. + mvhd.volume = FpU8::parse_raw(reader.read_be_u16()?); + + // Remaining fields are ignored. + + Ok(mvhd) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/opus.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/opus.rs new file mode 100644 index 00000000..756b2c1b --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/opus.rs @@ -0,0 +1,72 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::codecs::{CodecParameters, CODEC_TYPE_OPUS}; +use symphonia_core::errors::{decode_error, unsupported_error, Result}; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; + +#[allow(dead_code)] +#[derive(Debug)] +pub struct OpusAtom { + /// Atom header. + header: AtomHeader, + /// Opus extra data (identification header). + extra_data: Box<[u8]>, +} + +impl Atom for OpusAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + const OPUS_MAGIC: &[u8] = b"OpusHead"; + const OPUS_MAGIC_LEN: usize = OPUS_MAGIC.len(); + + const MIN_OPUS_EXTRA_DATA_SIZE: usize = OPUS_MAGIC_LEN + 11; + const MAX_OPUS_EXTRA_DATA_SIZE: usize = MIN_OPUS_EXTRA_DATA_SIZE + 257; + + // Offset of the Opus version number in the extra data. + const OPUS_EXTRADATA_VERSION_OFFSET: usize = OPUS_MAGIC_LEN; + + // The dops atom contains an Opus identification header excluding the OpusHead magic + // signature. Therefore, the atom data length should be atleast as long as the shortest + // Opus identification header. + let data_len = header.data_len as usize; + + if data_len < MIN_OPUS_EXTRA_DATA_SIZE - OPUS_MAGIC_LEN { + return decode_error("isomp4 (opus): opus identification header too short"); + } + + if data_len > MAX_OPUS_EXTRA_DATA_SIZE - OPUS_MAGIC_LEN { + return decode_error("isomp4 (opus): opus identification header too large"); + } + + let mut extra_data = vec![0; OPUS_MAGIC_LEN + data_len].into_boxed_slice(); + + // The Opus magic is excluded in the atom, but the extra data must start with it. + extra_data[..OPUS_MAGIC_LEN].copy_from_slice(OPUS_MAGIC); + + // Read the extra data from the atom. + reader.read_buf_exact(&mut extra_data[OPUS_MAGIC_LEN..])?; + + // Verify the version number is 0. + if extra_data[OPUS_EXTRADATA_VERSION_OFFSET] != 0 { + return unsupported_error("isomp4 (opus): unsupported opus version"); + } + + Ok(OpusAtom { header, extra_data }) + } +} + +impl OpusAtom { + pub fn fill_codec_params(&self, codec_params: &mut CodecParameters) { + codec_params.for_codec(CODEC_TYPE_OPUS).with_extra_data(self.extra_data.clone()); + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/sidx.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/sidx.rs new file mode 100644 index 00000000..917a54d8 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/sidx.rs @@ -0,0 +1,89 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Result}; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; + +#[derive(Debug)] +pub enum ReferenceType { + Segment, + Media, +} + +#[allow(dead_code)] +#[derive(Debug)] +pub struct SidxReference { + pub reference_type: ReferenceType, + pub reference_size: u32, + pub subsegment_duration: u32, + // pub starts_with_sap: bool, + // pub sap_type: u8, + // pub sap_delta_time: u32, +} + +/// Segment index atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct SidxAtom { + /// Atom header. + header: AtomHeader, + pub reference_id: u32, + pub timescale: u32, + pub earliest_pts: u64, + pub first_offset: u64, + pub references: Vec, +} + +impl Atom for SidxAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + // The anchor point for segment offsets is the first byte after this atom. + let anchor = reader.pos() + header.data_len; + + let (version, _) = AtomHeader::read_extra(reader)?; + + let reference_id = reader.read_be_u32()?; + let timescale = reader.read_be_u32()?; + + let (earliest_pts, first_offset) = match version { + 0 => (u64::from(reader.read_be_u32()?), anchor + u64::from(reader.read_be_u32()?)), + 1 => (reader.read_be_u64()?, anchor + reader.read_be_u64()?), + _ => { + return decode_error("isomp4: invalid sidx version"); + } + }; + + let _reserved = reader.read_be_u16()?; + let reference_count = reader.read_be_u16()?; + + let mut references = Vec::new(); + + for _ in 0..reference_count { + let reference = reader.read_be_u32()?; + let subsegment_duration = reader.read_be_u32()?; + + let reference_type = match (reference & 0x8000_0000) != 0 { + false => ReferenceType::Media, + true => ReferenceType::Segment, + }; + + let reference_size = reference & !0x8000_0000; + + // Ignore SAP + let _ = reader.read_be_u32()?; + + references.push(SidxReference { reference_type, reference_size, subsegment_duration }); + } + + Ok(SidxAtom { header, reference_id, timescale, earliest_pts, first_offset, references }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/smhd.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/smhd.rs new file mode 100644 index 00000000..6fac4cb6 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/smhd.rs @@ -0,0 +1,40 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::Result; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; +use crate::fp::FpI8; + +/// Sound header atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct SmhdAtom { + /// Atom header. + header: AtomHeader, + /// Stereo balance. + pub balance: FpI8, +} + +impl Atom for SmhdAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (_, _) = AtomHeader::read_extra(reader)?; + + // Stereo balance + let balance = FpI8::parse_raw(reader.read_be_u16()? as i16); + + // Reserved. + let _ = reader.read_be_u16()?; + + Ok(SmhdAtom { header, balance }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/stbl.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/stbl.rs new file mode 100644 index 00000000..1da68ead --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/stbl.rs @@ -0,0 +1,108 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Result}; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader, AtomIterator, AtomType}; +use crate::atoms::{Co64Atom, StcoAtom, StscAtom, StsdAtom, StszAtom, SttsAtom}; + +use log::warn; + +/// Sample table atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct StblAtom { + /// Atom header. + header: AtomHeader, + pub stsd: StsdAtom, + pub stts: SttsAtom, + pub stsc: StscAtom, + pub stsz: StszAtom, + pub stco: Option, + pub co64: Option, +} + +impl Atom for StblAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let mut iter = AtomIterator::new(reader, header); + + let mut stsd = None; + let mut stts = None; + let mut stsc = None; + let mut stsz = None; + let mut stco = None; + let mut co64 = None; + + while let Some(header) = iter.next()? { + match header.atype { + AtomType::SampleDescription => { + stsd = Some(iter.read_atom::()?); + } + AtomType::TimeToSample => { + stts = Some(iter.read_atom::()?); + } + AtomType::CompositionTimeToSample => { + // Composition time to sample atom is only required for video. + warn!("ignoring ctts atom."); + } + AtomType::SyncSample => { + // Sync sample atom is only required for video. + warn!("ignoring stss atom."); + } + AtomType::SampleToChunk => { + stsc = Some(iter.read_atom::()?); + } + AtomType::SampleSize => { + stsz = Some(iter.read_atom::()?); + } + AtomType::ChunkOffset => { + stco = Some(iter.read_atom::()?); + } + AtomType::ChunkOffset64 => { + co64 = Some(iter.read_atom::()?); + } + _ => (), + } + } + + if stsd.is_none() { + return decode_error("isomp4: missing stsd atom"); + } + + if stts.is_none() { + return decode_error("isomp4: missing stts atom"); + } + + if stsc.is_none() { + return decode_error("isomp4: missing stsc atom"); + } + + if stsz.is_none() { + return decode_error("isomp4: missing stsz atom"); + } + + if stco.is_none() && co64.is_none() { + // This is a spec. violation, but some m4a files appear to lack these atoms. + warn!("missing stco or co64 atom"); + } + + Ok(StblAtom { + header, + stsd: stsd.unwrap(), + stts: stts.unwrap(), + stsc: stsc.unwrap(), + stsz: stsz.unwrap(), + stco, + co64, + }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/stco.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/stco.rs new file mode 100644 index 00000000..23557590 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/stco.rs @@ -0,0 +1,41 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::Result; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; + +/// Chunk offset atom (32-bit version). +#[allow(dead_code)] +#[derive(Debug)] +pub struct StcoAtom { + /// Atom header. + header: AtomHeader, + pub chunk_offsets: Vec, +} + +impl Atom for StcoAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (_, _) = AtomHeader::read_extra(reader)?; + + let entry_count = reader.read_be_u32()?; + + // TODO: Apply a limit. + let mut chunk_offsets = Vec::with_capacity(entry_count as usize); + + for _ in 0..entry_count { + chunk_offsets.push(reader.read_be_u32()?); + } + + Ok(StcoAtom { header, chunk_offsets }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/stsc.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/stsc.rs new file mode 100644 index 00000000..d0e7c8dd --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/stsc.rs @@ -0,0 +1,109 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Result}; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; + +#[allow(dead_code)] +#[derive(Debug)] +pub struct StscEntry { + pub first_chunk: u32, + pub first_sample: u32, + pub samples_per_chunk: u32, + pub sample_desc_index: u32, +} + +/// Sample to Chunk Atom +#[allow(dead_code)] +#[derive(Debug)] +pub struct StscAtom { + /// Atom header. + header: AtomHeader, + /// Entries. + pub entries: Vec, +} + +impl StscAtom { + /// Finds the `StscEntry` for the sample indicated by `sample_num`. Note, `sample_num` is indexed + /// relative to the `StscAtom`. Complexity is O(log2 N). + pub fn find_entry_for_sample(&self, sample_num: u32) -> Option<&StscEntry> { + let mut left = 1; + let mut right = self.entries.len(); + + while left < right { + let mid = left + (right - left) / 2; + + let entry = self.entries.get(mid).unwrap(); + + if entry.first_sample < sample_num { + left = mid + 1; + } + else { + right = mid; + } + } + + // The index found above (left) is the exclusive upper bound of all entries where + // first_sample < sample_num. Therefore, the entry to return has an index of left-1. The + // index will never equal 0 so this is safe. If the table were empty, left == 1, thus calling + // get with an index of 0, and safely returning None. + self.entries.get(left - 1) + } +} + +impl Atom for StscAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (_, _) = AtomHeader::read_extra(reader)?; + + let entry_count = reader.read_be_u32()?; + + // TODO: Apply a limit. + let mut entries = Vec::with_capacity(entry_count as usize); + + for _ in 0..entry_count { + entries.push(StscEntry { + first_chunk: reader.read_be_u32()? - 1, + first_sample: 0, + samples_per_chunk: reader.read_be_u32()?, + sample_desc_index: reader.read_be_u32()?, + }); + } + + // Post-process entries to check for errors and calculate the file sample. + if entry_count > 0 { + for i in 0..entry_count as usize - 1 { + // Validate that first_chunk is monotonic across all entries. + if entries[i + 1].first_chunk < entries[i].first_chunk { + return decode_error("isomp4: stsc entry first chunk not monotonic"); + } + + // Validate that samples per chunk is > 0. Could the entry be ignored? + if entries[i].samples_per_chunk == 0 { + return decode_error("isomp4: stsc entry has 0 samples per chunk"); + } + + let n = entries[i + 1].first_chunk - entries[i].first_chunk; + + entries[i + 1].first_sample = + entries[i].first_sample + (n * entries[i].samples_per_chunk); + } + + // Validate that samples per chunk is > 0. Could the entry be ignored? + if entries[entry_count as usize - 1].samples_per_chunk == 0 { + return decode_error("isomp4: stsc entry has 0 samples per chunk"); + } + } + + Ok(StscAtom { header, entries }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/stsd.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/stsd.rs new file mode 100644 index 00000000..6e017de1 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/stsd.rs @@ -0,0 +1,539 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::audio::Channels; +use symphonia_core::codecs::{CodecParameters, CodecType, CODEC_TYPE_MP3, CODEC_TYPE_NULL}; +use symphonia_core::codecs::{CODEC_TYPE_PCM_F32BE, CODEC_TYPE_PCM_F32LE}; +use symphonia_core::codecs::{CODEC_TYPE_PCM_F64BE, CODEC_TYPE_PCM_F64LE}; +use symphonia_core::codecs::{CODEC_TYPE_PCM_S16BE, CODEC_TYPE_PCM_S16LE}; +use symphonia_core::codecs::{CODEC_TYPE_PCM_S24BE, CODEC_TYPE_PCM_S24LE}; +use symphonia_core::codecs::{CODEC_TYPE_PCM_S32BE, CODEC_TYPE_PCM_S32LE}; +use symphonia_core::codecs::{CODEC_TYPE_PCM_S8, CODEC_TYPE_PCM_U8}; +use symphonia_core::codecs::{CODEC_TYPE_PCM_U16BE, CODEC_TYPE_PCM_U16LE}; +use symphonia_core::codecs::{CODEC_TYPE_PCM_U24BE, CODEC_TYPE_PCM_U24LE}; +use symphonia_core::codecs::{CODEC_TYPE_PCM_U32BE, CODEC_TYPE_PCM_U32LE}; +use symphonia_core::errors::{decode_error, unsupported_error, Result}; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{AlacAtom, Atom, AtomHeader, AtomType, EsdsAtom, FlacAtom, OpusAtom, WaveAtom}; +use crate::fp::FpU16; + +use super::AtomIterator; + +/// Sample description atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct StsdAtom { + /// Atom header. + header: AtomHeader, + /// Sample entry. + sample_entry: SampleEntry, +} + +impl Atom for StsdAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (_, _) = AtomHeader::read_extra(reader)?; + + let n_entries = reader.read_be_u32()?; + + if n_entries == 0 { + return decode_error("isomp4: missing sample entry"); + } + + if n_entries > 1 { + return unsupported_error("isomp4: more than 1 sample entry"); + } + + let sample_entry_header = AtomHeader::read(reader)?; + + let sample_entry = match sample_entry_header.atype { + AtomType::Mp4a + | AtomType::Alac + | AtomType::Flac + | AtomType::Opus + | AtomType::Mp3 + | AtomType::Lpcm + | AtomType::QtWave + | AtomType::ALaw + | AtomType::MuLaw + | AtomType::U8SampleEntry + | AtomType::S16LeSampleEntry + | AtomType::S16BeSampleEntry + | AtomType::S24SampleEntry + | AtomType::S32SampleEntry + | AtomType::F32SampleEntry + | AtomType::F64SampleEntry => read_audio_sample_entry(reader, sample_entry_header)?, + _ => { + // Potentially video, subtitles, etc. + SampleEntry::Other + } + }; + + Ok(StsdAtom { header, sample_entry }) + } +} + +impl StsdAtom { + /// Fill the provided `CodecParameters` using the sample entry. + pub fn fill_codec_params(&self, codec_params: &mut CodecParameters) { + // Audio sample entry. + if let SampleEntry::Audio(ref entry) = self.sample_entry { + // General audio parameters. + codec_params.with_sample_rate(entry.sample_rate as u32); + + // Codec-specific parameters. + match entry.codec_specific { + Some(AudioCodecSpecific::Esds(ref esds)) => { + esds.fill_codec_params(codec_params); + } + Some(AudioCodecSpecific::Alac(ref alac)) => { + alac.fill_codec_params(codec_params); + } + Some(AudioCodecSpecific::Flac(ref flac)) => { + flac.fill_codec_params(codec_params); + } + Some(AudioCodecSpecific::Opus(ref opus)) => { + opus.fill_codec_params(codec_params); + } + Some(AudioCodecSpecific::Mp3) => { + codec_params.for_codec(CODEC_TYPE_MP3); + } + Some(AudioCodecSpecific::Pcm(ref pcm)) => { + // PCM codecs. + codec_params + .for_codec(pcm.codec_type) + .with_bits_per_coded_sample(pcm.bits_per_coded_sample) + .with_bits_per_sample(pcm.bits_per_sample) + .with_max_frames_per_packet(pcm.frames_per_packet) + .with_channels(pcm.channels); + } + _ => (), + } + } + } +} + +#[derive(Debug)] +pub struct Pcm { + pub codec_type: CodecType, + pub bits_per_sample: u32, + pub bits_per_coded_sample: u32, + pub frames_per_packet: u64, + pub channels: Channels, +} + +#[derive(Debug)] +pub enum AudioCodecSpecific { + /// MPEG Elementary Stream descriptor. + Esds(EsdsAtom), + /// Apple Lossless Audio Codec (ALAC). + Alac(AlacAtom), + /// Free Lossless Audio Codec (FLAC). + Flac(FlacAtom), + /// Opus. + Opus(OpusAtom), + /// MP3. + Mp3, + /// PCM codecs. + Pcm(Pcm), +} + +#[allow(dead_code)] +#[derive(Debug)] +pub struct AudioSampleEntry { + pub num_channels: u32, + pub sample_size: u16, + pub sample_rate: f64, + pub codec_specific: Option, +} + +#[derive(Debug)] +pub enum SampleEntry { + Audio(AudioSampleEntry), + // Video, + // Metadata, + Other, +} + +/// Gets if the sample entry atom is for a PCM codec. +fn is_pcm_codec(atype: AtomType) -> bool { + // PCM data in version 0 and 1 is signalled by the sample entry atom type. In version 2, the + // atom type for PCM data is always LPCM. + atype == AtomType::Lpcm || pcm_codec_type(atype) != CODEC_TYPE_NULL +} + +/// Gets the PCM codec from the sample entry atom type for version 0 and 1 sample entries. +fn pcm_codec_type(atype: AtomType) -> CodecType { + match atype { + AtomType::U8SampleEntry => CODEC_TYPE_PCM_U8, + AtomType::S16LeSampleEntry => CODEC_TYPE_PCM_S16LE, + AtomType::S16BeSampleEntry => CODEC_TYPE_PCM_S16BE, + AtomType::S24SampleEntry => CODEC_TYPE_PCM_S24LE, + AtomType::S32SampleEntry => CODEC_TYPE_PCM_S32LE, + AtomType::F32SampleEntry => CODEC_TYPE_PCM_F32LE, + AtomType::F64SampleEntry => CODEC_TYPE_PCM_F64LE, + _ => CODEC_TYPE_NULL, + } +} + +/// Determines the number of bytes per PCM sample for a PCM codec type. +fn bytes_per_pcm_sample(pcm_codec_type: CodecType) -> u32 { + match pcm_codec_type { + CODEC_TYPE_PCM_S8 | CODEC_TYPE_PCM_U8 => 1, + CODEC_TYPE_PCM_S16BE | CODEC_TYPE_PCM_S16LE => 2, + CODEC_TYPE_PCM_U16BE | CODEC_TYPE_PCM_U16LE => 2, + CODEC_TYPE_PCM_S24BE | CODEC_TYPE_PCM_S24LE => 3, + CODEC_TYPE_PCM_U24BE | CODEC_TYPE_PCM_U24LE => 3, + CODEC_TYPE_PCM_S32BE | CODEC_TYPE_PCM_S32LE => 4, + CODEC_TYPE_PCM_U32BE | CODEC_TYPE_PCM_U32LE => 4, + CODEC_TYPE_PCM_F32BE | CODEC_TYPE_PCM_F32LE => 4, + CODEC_TYPE_PCM_F64BE | CODEC_TYPE_PCM_F64LE => 8, + _ => unreachable!(), + } +} + +/// Gets the PCM codec from the LPCM parameters in the version 2 sample entry atom. +fn lpcm_codec_type(bits_per_sample: u32, lpcm_flags: u32) -> CodecType { + let is_floating_point = lpcm_flags & 0x1 != 0; + let is_big_endian = lpcm_flags & 0x2 != 0; + let is_signed = lpcm_flags & 0x4 != 0; + + if is_floating_point { + // Floating-point sample format. + match bits_per_sample { + 32 => { + if is_big_endian { + CODEC_TYPE_PCM_F32BE + } + else { + CODEC_TYPE_PCM_F32LE + } + } + 64 => { + if is_big_endian { + CODEC_TYPE_PCM_F64BE + } + else { + CODEC_TYPE_PCM_F64LE + } + } + _ => CODEC_TYPE_NULL, + } + } + else { + // Integer sample format. + if is_signed { + // Signed-integer sample format. + match bits_per_sample { + 8 => CODEC_TYPE_PCM_S8, + 16 => { + if is_big_endian { + CODEC_TYPE_PCM_S16BE + } + else { + CODEC_TYPE_PCM_S16LE + } + } + 24 => { + if is_big_endian { + CODEC_TYPE_PCM_S24BE + } + else { + CODEC_TYPE_PCM_S24LE + } + } + 32 => { + if is_big_endian { + CODEC_TYPE_PCM_S32BE + } + else { + CODEC_TYPE_PCM_S32LE + } + } + _ => CODEC_TYPE_NULL, + } + } + else { + // Unsigned-integer sample format. + match bits_per_sample { + 8 => CODEC_TYPE_PCM_U8, + 16 => { + if is_big_endian { + CODEC_TYPE_PCM_U16BE + } + else { + CODEC_TYPE_PCM_U16LE + } + } + 24 => { + if is_big_endian { + CODEC_TYPE_PCM_U24BE + } + else { + CODEC_TYPE_PCM_U24LE + } + } + 32 => { + if is_big_endian { + CODEC_TYPE_PCM_U32BE + } + else { + CODEC_TYPE_PCM_U32LE + } + } + _ => CODEC_TYPE_NULL, + } + } + } +} + +/// Gets the audio channels for a version 0 or 1 sample entry. +fn pcm_channels(num_channels: u32) -> Result { + match num_channels { + 1 => Ok(Channels::FRONT_LEFT), + 2 => Ok(Channels::FRONT_LEFT | Channels::FRONT_RIGHT), + _ => decode_error("isomp4: invalid number of channels"), + } +} + +/// Gets the audio channels for a version 2 LPCM sample entry. +fn lpcm_channels(num_channels: u32) -> Result { + if num_channels < 1 { + return decode_error("isomp4: invalid number of channels"); + } + + if num_channels > 32 { + return unsupported_error("isomp4: maximum 32 channels"); + } + + // TODO: For LPCM, the channels are "auxilary". They do not have a speaker assignment. Symphonia + // does not have a way to represent this yet. + let channel_mask = !((!0 << 1) << (num_channels - 1)); + + match Channels::from_bits(channel_mask) { + Some(channels) => Ok(channels), + _ => unsupported_error("isomp4: unsupported number of channels"), + } +} + +fn read_audio_sample_entry( + reader: &mut B, + mut header: AtomHeader, +) -> Result { + // An audio sample entry atom is derived from a base sample entry atom. The audio sample entry + // atom contains the fields of the base sample entry first, then the audio sample entry fields + // next. After those fields, a number of other atoms are nested, including the mandatory + // codec-specific atom. Though the codec-specific atom is nested within the (audio) sample entry + // atom, the (audio) sample entry atom uses the atom type of the codec-specific atom. This is + // odd in-that the final structure will appear to have the codec-specific atom nested within + // itself, which is not actually the case. + + let data_start_pos = reader.pos(); + + // First 6 bytes of all sample entries should be all 0. + reader.ignore_bytes(6)?; + + // Sample entry data reference. + let _ = reader.read_be_u16()?; + + // The version of the audio sample entry. + let version = reader.read_be_u16()?; + + // Skip revision and vendor. + reader.ignore_bytes(6)?; + + let mut num_channels = u32::from(reader.read_be_u16()?); + let sample_size = reader.read_be_u16()?; + + // Skip compression ID and packet size. + reader.ignore_bytes(4)?; + + let mut sample_rate = f64::from(FpU16::parse_raw(reader.read_be_u32()?)); + + let is_pcm_codec = is_pcm_codec(header.atype); + + let mut codec_specific = match version { + 0 => { + // Version 0. + if is_pcm_codec { + let codec_type = pcm_codec_type(header.atype); + let bits_per_sample = 8 * bytes_per_pcm_sample(codec_type); + + // Validate the codec-derived bytes-per-sample equals the declared bytes-per-sample. + if u32::from(sample_size) != bits_per_sample { + return decode_error("isomp4: invalid pcm sample size"); + } + + // The original fields describe the PCM sample format. + Some(AudioCodecSpecific::Pcm(Pcm { + codec_type: pcm_codec_type(header.atype), + bits_per_sample, + bits_per_coded_sample: bits_per_sample, + frames_per_packet: 1, + channels: pcm_channels(num_channels)?, + })) + } + else { + None + } + } + 1 => { + // Version 1. + + // The number of frames (ISO/MP4 samples) per packet. For PCM codecs, this is always 1. + let _frames_per_packet = reader.read_be_u32()?; + + // The number of bytes per PCM audio sample. This value supersedes sample_size. For + // non-PCM codecs, this value is not useful. + let bytes_per_audio_sample = reader.read_be_u32()?; + + // The number of bytes per PCM audio frame (ISO/MP4 sample). For non-PCM codecs, this + // value is not useful. + let _bytes_per_frame = reader.read_be_u32()?; + + // The next value, as defined, is seemingly non-sensical. + let _ = reader.read_be_u32()?; + + if is_pcm_codec { + let codec_type = pcm_codec_type(header.atype); + let codec_bytes_per_sample = bytes_per_pcm_sample(codec_type); + + // Validate the codec-derived bytes-per-sample equals the declared bytes-per-sample. + if bytes_per_audio_sample != codec_bytes_per_sample { + return decode_error("isomp4: invalid pcm bytes per sample"); + } + + // The new fields describe the PCM sample format and supersede the original version + // 0 fields. + Some(AudioCodecSpecific::Pcm(Pcm { + codec_type, + bits_per_sample: 8 * codec_bytes_per_sample, + bits_per_coded_sample: 8 * codec_bytes_per_sample, + frames_per_packet: 1, + channels: pcm_channels(num_channels)?, + })) + } + else { + None + } + } + 2 => { + // Version 2. + reader.ignore_bytes(4)?; + + sample_rate = reader.read_be_f64()?; + num_channels = reader.read_be_u32()?; + + if reader.read_be_u32()? != 0x7f00_0000 { + return decode_error("isomp4: audio sample entry v2 reserved must be 0x7f00_0000"); + } + + // The following fields are only useful for PCM codecs. + let bits_per_sample = reader.read_be_u32()?; + let lpcm_flags = reader.read_be_u32()?; + let _bytes_per_packet = reader.read_be_u32()?; + let lpcm_frames_per_packet = reader.read_be_u32()?; + + // This is only valid if this is a PCM codec. + let codec_type = lpcm_codec_type(bits_per_sample, lpcm_flags); + + if is_pcm_codec && codec_type != CODEC_TYPE_NULL { + // Like version 1, the new fields describe the PCM sample format and supersede the + // original version 0 fields. + Some(AudioCodecSpecific::Pcm(Pcm { + codec_type, + bits_per_sample, + bits_per_coded_sample: bits_per_sample, + frames_per_packet: u64::from(lpcm_frames_per_packet), + channels: lpcm_channels(num_channels)?, + })) + } + else { + None + } + } + _ => { + return unsupported_error("isomp4: unknown sample entry version"); + } + }; + + // Need to account for the data already read from the atom. + header.data_len -= reader.pos() - data_start_pos; + + let mut iter = AtomIterator::new(reader, header); + + while let Some(entry_header) = iter.next()? { + match entry_header.atype { + AtomType::Esds => { + // MP4A/ESDS codec-specific atom. + if header.atype != AtomType::Mp4a || codec_specific.is_some() { + return decode_error("isomp4: invalid sample entry"); + } + + codec_specific = Some(AudioCodecSpecific::Esds(iter.read_atom::()?)); + } + AtomType::Alac => { + // ALAC codec-specific atom. + if header.atype != AtomType::Alac || codec_specific.is_some() { + return decode_error("isomp4: invalid sample entry"); + } + + codec_specific = Some(AudioCodecSpecific::Alac(iter.read_atom::()?)); + } + AtomType::FlacDsConfig => { + // FLAC codec-specific atom. + if header.atype != AtomType::Flac || codec_specific.is_some() { + return decode_error("isomp4: invalid sample entry"); + } + + codec_specific = Some(AudioCodecSpecific::Flac(iter.read_atom::()?)); + } + AtomType::OpusDsConfig => { + // Opus codec-specific atom. + if header.atype != AtomType::Opus || codec_specific.is_some() { + return decode_error("isomp4: invalid sample entry"); + } + + codec_specific = Some(AudioCodecSpecific::Opus(iter.read_atom::()?)); + } + AtomType::QtWave => { + // The QuickTime WAVE (aka. siDecompressionParam) atom may contain many different + // types of sub-atoms to store decoder parameters. + let wave = iter.read_atom::()?; + + if let Some(esds) = wave.esds { + if codec_specific.is_some() { + return decode_error("isomp4: invalid sample entry"); + } + + codec_specific = Some(AudioCodecSpecific::Esds(esds)); + } + } + _ => (), + } + } + + // A MP3 sample entry has no codec-specific atom. + if header.atype == AtomType::Mp3 { + if codec_specific.is_some() { + return decode_error("isomp4: invalid sample entry"); + } + + codec_specific = Some(AudioCodecSpecific::Mp3); + } + + Ok(SampleEntry::Audio(AudioSampleEntry { + num_channels, + sample_size, + sample_rate, + codec_specific, + })) +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/stss.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/stss.rs new file mode 100644 index 00000000..beb63343 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/stss.rs @@ -0,0 +1,28 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::Result; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; + +#[allow(dead_code)] +#[derive(Debug)] +pub struct StssAtom { + /// Atom header. + header: AtomHeader, +} + +impl Atom for StssAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(_reader: &mut B, _header: AtomHeader) -> Result { + todo!() + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/stsz.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/stsz.rs new file mode 100644 index 00000000..42491b00 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/stsz.rs @@ -0,0 +1,58 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::Result; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; + +#[derive(Debug)] +pub enum SampleSize { + Constant(u32), + Variable(Vec), +} + +/// Sample Size Atom +#[allow(dead_code)] +#[derive(Debug)] +pub struct StszAtom { + /// Atom header. + header: AtomHeader, + /// The total number of samples. + pub sample_count: u32, + /// A vector of `sample_count` sample sizes, or a constant size for all samples. + pub sample_sizes: SampleSize, +} + +impl Atom for StszAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (_, _) = AtomHeader::read_extra(reader)?; + + let sample_size = reader.read_be_u32()?; + let sample_count = reader.read_be_u32()?; + + let sample_sizes = if sample_size == 0 { + // TODO: Apply a limit. + let mut entries = Vec::with_capacity(sample_count as usize); + + for _ in 0..sample_count { + entries.push(reader.read_be_u32()?); + } + + SampleSize::Variable(entries) + } + else { + SampleSize::Constant(sample_size) + }; + + Ok(StszAtom { header, sample_count, sample_sizes }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/stts.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/stts.rs new file mode 100644 index 00000000..c3cacb9c --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/stts.rs @@ -0,0 +1,105 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::Result; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; + +#[derive(Debug)] +pub struct SampleDurationEntry { + pub sample_count: u32, + pub sample_delta: u32, +} + +/// Time-to-sample atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct SttsAtom { + /// Atom header. + header: AtomHeader, + pub entries: Vec, + pub total_duration: u64, +} + +impl SttsAtom { + /// Get the timestamp and duration for the sample indicated by `sample_num`. Note, `sample_num` + /// is indexed relative to the `SttsAtom`. Complexity of this function in O(N). + pub fn find_timing_for_sample(&self, sample_num: u32) -> Option<(u64, u32)> { + let mut ts = 0; + let mut next_entry_first_sample = 0; + + // The Stts atom compactly encodes a mapping between number of samples and sample duration. + // Iterate through each entry until the entry containing the next sample is found. The next + // packet timestamp is then the sum of the product of sample count and sample duration for + // the n-1 iterated entries, plus the product of the number of consumed samples in the n-th + // iterated entry and sample duration. + for entry in &self.entries { + next_entry_first_sample += entry.sample_count; + + if sample_num < next_entry_first_sample { + let entry_sample_offset = sample_num + entry.sample_count - next_entry_first_sample; + ts += u64::from(entry.sample_delta) * u64::from(entry_sample_offset); + + return Some((ts, entry.sample_delta)); + } + + ts += u64::from(entry.sample_count) * u64::from(entry.sample_delta); + } + + None + } + + /// Get the sample that contains the timestamp indicated by `ts`. Note, the returned `sample_num` + /// is indexed relative to the `SttsAtom`. Complexity of this function in O(N). + pub fn find_sample_for_timestamp(&self, ts: u64) -> Option { + let mut ts_accum = 0; + let mut sample_num = 0; + + for entry in &self.entries { + let delta = u64::from(entry.sample_delta) * u64::from(entry.sample_count); + + if ts_accum + delta > ts { + sample_num += ((ts - ts_accum) / u64::from(entry.sample_delta)) as u32; + return Some(sample_num); + } + + ts_accum += delta; + sample_num += entry.sample_count; + } + + None + } +} + +impl Atom for SttsAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (_, _) = AtomHeader::read_extra(reader)?; + + let entry_count = reader.read_be_u32()?; + + let mut total_duration = 0; + + // TODO: Limit table length. + let mut entries = Vec::with_capacity(entry_count as usize); + + for _ in 0..entry_count { + let sample_count = reader.read_be_u32()?; + let sample_delta = reader.read_be_u32()?; + + total_duration += u64::from(sample_count) * u64::from(sample_delta); + + entries.push(SampleDurationEntry { sample_count, sample_delta }); + } + + Ok(SttsAtom { header, entries, total_duration }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/tfhd.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/tfhd.rs new file mode 100644 index 00000000..7acb87d2 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/tfhd.rs @@ -0,0 +1,84 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::Result; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; + +/// Track fragment header atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct TfhdAtom { + /// Atom header. + header: AtomHeader, + pub track_id: u32, + pub base_data_offset: Option, + pub sample_desc_idx: Option, + pub default_sample_duration: Option, + pub default_sample_size: Option, + pub default_sample_flags: Option, + /// If true, there are no samples for this time duration. + pub duration_is_empty: bool, + /// If true, the base data offset for this track is the first byte of the parent containing moof + /// atom. + pub default_base_is_moof: bool, +} + +impl Atom for TfhdAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (_, flags) = AtomHeader::read_extra(reader)?; + + let track_id = reader.read_be_u32()?; + + let base_data_offset = match flags & 0x1 { + 0 => None, + _ => Some(reader.read_be_u64()?), + }; + + let sample_desc_idx = match flags & 0x2 { + 0 => None, + _ => Some(reader.read_be_u32()?), + }; + + let default_sample_duration = match flags & 0x8 { + 0 => None, + _ => Some(reader.read_be_u32()?), + }; + + let default_sample_size = match flags & 0x10 { + 0 => None, + _ => Some(reader.read_be_u32()?), + }; + + let default_sample_flags = match flags & 0x20 { + 0 => None, + _ => Some(reader.read_be_u32()?), + }; + + let duration_is_empty = (flags & 0x1_0000) != 0; + + // The default-base-is-moof flag is ignored if the base-data-offset flag is set. + let default_base_is_moof = (flags & 0x1 == 0) && (flags & 0x2_0000 != 0); + + Ok(TfhdAtom { + header, + track_id, + base_data_offset, + sample_desc_idx, + default_sample_duration, + default_sample_size, + default_sample_flags, + duration_is_empty, + default_base_is_moof, + }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/tkhd.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/tkhd.rs new file mode 100644 index 00000000..17c28718 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/tkhd.rs @@ -0,0 +1,89 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Result}; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; +use crate::fp::FpU8; + +/// Track header atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct TkhdAtom { + /// Atom header. + header: AtomHeader, + /// Track header flags. + pub flags: u32, + /// Creation time. + pub ctime: u64, + /// Modification time. + pub mtime: u64, + /// Track identifier. + pub id: u32, + /// Track duration in the timescale units specified in the movie header. This value is equal to + /// the sum of the durations of all the track's edits. + pub duration: u64, + /// Layer. + pub layer: u16, + /// Grouping identifier. + pub alternate_group: u16, + /// Preferred volume for track playback. + pub volume: FpU8, +} + +impl Atom for TkhdAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (version, flags) = AtomHeader::read_extra(reader)?; + + let mut tkhd = TkhdAtom { + header, + flags, + ctime: 0, + mtime: 0, + id: 0, + duration: 0, + layer: 0, + alternate_group: 0, + volume: Default::default(), + }; + + // Version 0 uses 32-bit time values, verion 1 used 64-bit values. + match version { + 0 => { + tkhd.ctime = u64::from(reader.read_be_u32()?); + tkhd.mtime = u64::from(reader.read_be_u32()?); + tkhd.id = reader.read_be_u32()?; + let _ = reader.read_be_u32()?; // Reserved + tkhd.duration = u64::from(reader.read_be_u32()?); + } + 1 => { + tkhd.ctime = reader.read_be_u64()?; + tkhd.mtime = reader.read_be_u64()?; + tkhd.id = reader.read_be_u32()?; + let _ = reader.read_be_u32()?; // Reserved + tkhd.duration = reader.read_be_u64()?; + } + _ => return decode_error("isomp4: invalid tkhd version"), + } + + // Reserved + let _ = reader.read_be_u64()?; + + tkhd.layer = reader.read_be_u16()?; + tkhd.alternate_group = reader.read_be_u16()?; + tkhd.volume = FpU8::parse_raw(reader.read_be_u16()?); + + // The remainder of the header is only useful for video tracks. + + Ok(tkhd) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/traf.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/traf.rs new file mode 100644 index 00000000..cedc1a7a --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/traf.rs @@ -0,0 +1,64 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Result}; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader, AtomIterator, AtomType, TfhdAtom, TrunAtom}; + +/// Track fragment atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct TrafAtom { + /// Atom header. + header: AtomHeader, + /// Track fragment header. + pub tfhd: TfhdAtom, + /// Track fragment sample runs. + pub truns: Vec, + /// The total number of samples in this track fragment. + pub total_sample_count: u32, +} + +impl Atom for TrafAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let mut tfhd = None; + let mut truns = Vec::new(); + + let mut iter = AtomIterator::new(reader, header); + + let mut total_sample_count = 0; + + while let Some(header) = iter.next()? { + match header.atype { + AtomType::TrackFragmentHeader => { + tfhd = Some(iter.read_atom::()?); + } + AtomType::TrackFragmentRun => { + let trun = iter.read_atom::()?; + + // Increment the total sample count. + total_sample_count += trun.sample_count; + + truns.push(trun); + } + _ => (), + } + } + + // Tfhd is mandatory. + if tfhd.is_none() { + return decode_error("isomp4: missing tfhd atom"); + } + + Ok(TrafAtom { header, tfhd: tfhd.unwrap(), truns, total_sample_count }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/trak.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/trak.rs new file mode 100644 index 00000000..55115925 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/trak.rs @@ -0,0 +1,64 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Result}; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader, AtomIterator, AtomType, EdtsAtom, MdiaAtom, TkhdAtom}; + +/// Track atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct TrakAtom { + /// Atom header. + header: AtomHeader, + /// Track header atom. + pub tkhd: TkhdAtom, + /// Optional, edit list atom. + pub edts: Option, + /// Media atom. + pub mdia: MdiaAtom, +} + +impl Atom for TrakAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let mut iter = AtomIterator::new(reader, header); + + let mut tkhd = None; + let mut edts = None; + let mut mdia = None; + + while let Some(header) = iter.next()? { + match header.atype { + AtomType::TrackHeader => { + tkhd = Some(iter.read_atom::()?); + } + AtomType::Edit => { + edts = Some(iter.read_atom::()?); + } + AtomType::Media => { + mdia = Some(iter.read_atom::()?); + } + _ => (), + } + } + + if tkhd.is_none() { + return decode_error("isomp4: missing tkhd atom"); + } + + if mdia.is_none() { + return decode_error("isomp4: missing mdia atom"); + } + + Ok(TrakAtom { header, tkhd: tkhd.unwrap(), edts, mdia: mdia.unwrap() }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/trex.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/trex.rs new file mode 100644 index 00000000..23a1ce60 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/trex.rs @@ -0,0 +1,48 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::Result; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader}; + +/// Track extends atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct TrexAtom { + /// Atom header. + header: AtomHeader, + /// Track this atom describes. + pub track_id: u32, + /// Default sample description index. + pub default_sample_desc_idx: u32, + /// Default sample duration. + pub default_sample_duration: u32, + /// Default sample size. + pub default_sample_size: u32, + /// Default sample flags. + pub default_sample_flags: u32, +} + +impl Atom for TrexAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (_, _) = AtomHeader::read_extra(reader)?; + + Ok(TrexAtom { + header, + track_id: reader.read_be_u32()?, + default_sample_desc_idx: reader.read_be_u32()?, + default_sample_duration: reader.read_be_u32()?, + default_sample_size: reader.read_be_u32()?, + default_sample_flags: reader.read_be_u32()?, + }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/trun.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/trun.rs new file mode 100644 index 00000000..25757743 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/trun.rs @@ -0,0 +1,321 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Result}; +use symphonia_core::io::ReadBytes; +use symphonia_core::util::bits; + +use crate::atoms::{Atom, AtomHeader}; + +/// Track fragment run atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct TrunAtom { + /// Atom header. + header: AtomHeader, + /// Extended header flags. + flags: u32, + /// Data offset of this run. + pub data_offset: Option, + /// Number of samples in this run. + pub sample_count: u32, + /// Sample flags for the first sample only. + pub first_sample_flags: Option, + /// Sample duration for each sample in this run. + pub sample_duration: Vec, + /// Sample size for each sample in this run. + pub sample_size: Vec, + /// Sample flags for each sample in this run. + pub sample_flags: Vec, + /// The total size of all samples in this run. 0 if the sample size flag is not set. + total_sample_size: u64, + /// The total duration of all samples in this run. 0 if the sample duration flag is not set. + total_sample_duration: u64, +} + +impl TrunAtom { + // Track fragment run atom flags. + const DATA_OFFSET_PRESENT: u32 = 0x1; + const FIRST_SAMPLE_FLAGS_PRESENT: u32 = 0x4; + const SAMPLE_DURATION_PRESENT: u32 = 0x100; + const SAMPLE_SIZE_PRESENT: u32 = 0x200; + const SAMPLE_FLAGS_PRESENT: u32 = 0x400; + const SAMPLE_COMPOSITION_TIME_OFFSETS_PRESENT: u32 = 0x800; + + /// Indicates if sample durations are provided. + pub fn is_sample_duration_present(&self) -> bool { + self.flags & TrunAtom::SAMPLE_DURATION_PRESENT != 0 + } + + // Indicates if the duration of the first sample is provided. + pub fn is_first_sample_duration_present(&self) -> bool { + match self.first_sample_flags { + Some(flags) => flags & TrunAtom::FIRST_SAMPLE_FLAGS_PRESENT != 0, + None => false, + } + } + + /// Indicates if sample sizes are provided. + pub fn is_sample_size_present(&self) -> bool { + self.flags & TrunAtom::SAMPLE_SIZE_PRESENT != 0 + } + + /// Indicates if the size for the first sample is provided. + pub fn is_first_sample_size_present(&self) -> bool { + match self.first_sample_flags { + Some(flags) => flags & TrunAtom::SAMPLE_SIZE_PRESENT != 0, + None => false, + } + } + + /// Indicates if sample flags are provided. + #[allow(dead_code)] + pub fn are_sample_flags_present(&self) -> bool { + self.flags & TrunAtom::SAMPLE_FLAGS_PRESENT != 0 + } + + /// Indicates if sample composition time offsets are provided. + #[allow(dead_code)] + pub fn are_sample_composition_time_offsets_present(&self) -> bool { + self.flags & TrunAtom::SAMPLE_COMPOSITION_TIME_OFFSETS_PRESENT != 0 + } + + /// Gets the total duration of all samples. + pub fn total_duration(&self, default_dur: u32) -> u64 { + if self.is_sample_duration_present() { + self.total_sample_duration + } + else { + // The duration of all samples in the track fragment are not explictly known. + if self.sample_count > 0 && self.is_first_sample_duration_present() { + // The first sample has an explictly recorded duration. + u64::from(self.sample_duration[0]) + + u64::from(self.sample_count - 1) * u64::from(default_dur) + } + else { + // All samples have the default duration. + u64::from(self.sample_count) * u64::from(default_dur) + } + } + } + + /// Gets the total size of all samples. + pub fn total_size(&self, default_size: u32) -> u64 { + if self.is_sample_size_present() { + self.total_sample_size + } + else if self.sample_count > 0 && self.is_first_sample_size_present() { + u64::from(self.sample_size[0]) + + u64::from(self.sample_count - 1) * u64::from(default_size) + } + else { + u64::from(self.sample_count) * u64::from(default_size) + } + } + + /// Get the timestamp and duration of a sample. The desired sample is specified by the + /// trun-relative sample number, `sample_num_rel`. + pub fn sample_timing(&self, sample_num_rel: u32, default_dur: u32) -> (u64, u32) { + debug_assert!(sample_num_rel < self.sample_count); + + if self.is_sample_duration_present() { + // All sample durations are unique. + let ts = if sample_num_rel > 0 { + self.sample_duration[..sample_num_rel as usize] + .iter() + .map(|&s| u64::from(s)) + .sum::() + } + else { + 0 + }; + + let dur = self.sample_duration[sample_num_rel as usize]; + + (ts, dur) + } + else { + // The duration of all samples in the track fragment are not unique. + let ts = if sample_num_rel > 0 && self.is_first_sample_duration_present() { + // The first sample has a unique duration. + u64::from(self.sample_duration[0]) + + u64::from(sample_num_rel - 1) * u64::from(default_dur) + } + else { + // Zero or more samples with identical durations. + u64::from(sample_num_rel) * u64::from(default_dur) + }; + + (ts, default_dur) + } + } + + /// Get the size of a sample. The desired sample is specified by the trun-relative sample + /// number, `sample_num_rel`. + pub fn sample_size(&self, sample_num_rel: u32, default_size: u32) -> u32 { + debug_assert!(sample_num_rel < self.sample_count); + + if self.is_sample_size_present() { + self.sample_size[sample_num_rel as usize] + } + else if sample_num_rel == 0 && self.is_first_sample_size_present() { + self.sample_size[0] + } + else { + default_size + } + } + + /// Get the byte offset and size of a sample. The desired sample is specified by the + /// trun-relative sample number, `sample_num_rel`. + pub fn sample_offset(&self, sample_num_rel: u32, default_size: u32) -> (u64, u32) { + debug_assert!(sample_num_rel < self.sample_count); + + if self.is_sample_size_present() { + // All sample sizes are unique. + let offset = if sample_num_rel > 0 { + self.sample_size[..sample_num_rel as usize] + .iter() + .map(|&s| u64::from(s)) + .sum::() + } + else { + 0 + }; + + (offset, self.sample_size[sample_num_rel as usize]) + } + else { + // The size of all samples in the track are not unique. + let offset = if sample_num_rel > 0 && self.is_first_sample_size_present() { + // The first sample has a unique size. + u64::from(self.sample_size[0]) + + u64::from(sample_num_rel - 1) * u64::from(default_size) + } + else { + // Zero or more identically sized samples. + u64::from(sample_num_rel) * u64::from(default_size) + }; + + (offset, default_size) + } + } + + /// Get the sample number (relative to the trun) of the sample that contains timestamp `ts`. + pub fn ts_sample(&self, ts_rel: u64, default_dur: u32) -> u32 { + let mut sample_num = 0; + let mut ts_delta = ts_rel; + + if self.is_sample_duration_present() { + // If the sample durations are present, then each sample duration is independently + // stored. Sum sample durations until the delta is reached. + for &dur in &self.sample_duration { + if u64::from(dur) > ts_delta { + break; + } + + ts_delta -= u64::from(dur); + sample_num += 1; + } + } + else { + if self.sample_count > 0 && self.is_first_sample_duration_present() { + // The first sample duration is unique. + let first_sample_dur = u64::from(self.sample_duration[0]); + + if ts_delta >= first_sample_dur { + ts_delta -= first_sample_dur; + sample_num += 1; + } + else { + ts_delta -= ts_delta; + } + } + + sample_num += ts_delta.checked_div(u64::from(default_dur)).unwrap_or(0) as u32; + } + + sample_num + } +} + +impl Atom for TrunAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let (_, flags) = AtomHeader::read_extra(reader)?; + + let sample_count = reader.read_be_u32()?; + + let data_offset = match flags & TrunAtom::DATA_OFFSET_PRESENT { + 0 => None, + _ => Some(bits::sign_extend_leq32_to_i32(reader.read_be_u32()?, 32)), + }; + + let first_sample_flags = match flags & TrunAtom::FIRST_SAMPLE_FLAGS_PRESENT { + 0 => None, + _ => Some(reader.read_be_u32()?), + }; + + // If the first-sample-flags-present flag is set, then the sample-flags-present flag should + // not be set. The samples after the first shall use the default sample flags defined in the + // tfhd or mvex atoms. + if first_sample_flags.is_some() && (flags & TrunAtom::SAMPLE_FLAGS_PRESENT != 0) { + return decode_error( + "isomp4: sample-flag-present and first-sample-flags-present flags are set", + ); + } + + let mut sample_duration = Vec::new(); + let mut sample_size = Vec::new(); + let mut sample_flags = Vec::new(); + + let mut total_sample_size = 0; + let mut total_sample_duration = 0; + + // TODO: Apply a limit. + for _ in 0..sample_count { + if (flags & TrunAtom::SAMPLE_DURATION_PRESENT) != 0 { + let duration = reader.read_be_u32()?; + total_sample_duration += u64::from(duration); + sample_duration.push(duration); + } + + if (flags & TrunAtom::SAMPLE_SIZE_PRESENT) != 0 { + let size = reader.read_be_u32()?; + total_sample_size += u64::from(size); + sample_size.push(size); + } + + if (flags & TrunAtom::SAMPLE_FLAGS_PRESENT) != 0 { + sample_flags.push(reader.read_be_u32()?); + } + + // Ignoring composition time for now since it's a video thing... + if (flags & TrunAtom::SAMPLE_COMPOSITION_TIME_OFFSETS_PRESENT) != 0 { + // For version 0, this is a u32. + // For version 1, this is a i32. + let _ = reader.read_be_u32()?; + } + } + + Ok(TrunAtom { + header, + flags, + data_offset, + sample_count, + first_sample_flags, + sample_duration, + sample_size, + sample_flags, + total_sample_size, + total_sample_duration, + }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/udta.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/udta.rs new file mode 100644 index 00000000..69f0192f --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/udta.rs @@ -0,0 +1,53 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::Result; +use symphonia_core::io::ReadBytes; +use symphonia_core::meta::MetadataRevision; + +use crate::atoms::{Atom, AtomHeader, AtomIterator, AtomType, MetaAtom}; + +/// User data atom. +#[allow(dead_code)] +#[derive(Debug)] +pub struct UdtaAtom { + /// Atom header. + header: AtomHeader, + /// Metadata atom. + pub meta: Option, +} + +impl UdtaAtom { + /// If metadata was read, consumes the metadata and returns it. + pub fn take_metadata(&mut self) -> Option { + self.meta.as_mut().and_then(|meta| meta.take_metadata()) + } +} + +impl Atom for UdtaAtom { + fn header(&self) -> AtomHeader { + self.header + } + + #[allow(clippy::single_match)] + fn read(reader: &mut B, header: AtomHeader) -> Result { + let mut iter = AtomIterator::new(reader, header); + + let mut meta = None; + + while let Some(header) = iter.next()? { + match header.atype { + AtomType::Meta => { + meta = Some(iter.read_atom::()?); + } + _ => (), + } + } + + Ok(UdtaAtom { header, meta }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/atoms/wave.rs b/src-tauri/patches/symphonia-format-isomp4/src/atoms/wave.rs new file mode 100644 index 00000000..eb07a3d6 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/atoms/wave.rs @@ -0,0 +1,41 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::Result; +use symphonia_core::io::ReadBytes; + +use crate::atoms::{Atom, AtomHeader, EsdsAtom}; + +use super::{AtomIterator, AtomType}; + +#[allow(dead_code)] +#[derive(Debug)] +pub struct WaveAtom { + /// Atom header. + header: AtomHeader, + pub esds: Option, +} + +impl Atom for WaveAtom { + fn header(&self) -> AtomHeader { + self.header + } + + fn read(reader: &mut B, header: AtomHeader) -> Result { + let mut iter = AtomIterator::new(reader, header); + + let mut esds = None; + + while let Some(header) = iter.next()? { + if header.atype == AtomType::Esds { + esds = Some(iter.read_atom::()?); + } + } + + Ok(WaveAtom { header, esds }) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/demuxer.rs b/src-tauri/patches/symphonia-format-isomp4/src/demuxer.rs new file mode 100644 index 00000000..dbe780b3 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/demuxer.rs @@ -0,0 +1,606 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::{errors::end_of_stream_error, support_format}; + +use symphonia_core::codecs::CodecParameters; +use symphonia_core::errors::{ + decode_error, seek_error, unsupported_error, Error, Result, SeekErrorKind, +}; +use symphonia_core::formats::prelude::*; +use symphonia_core::io::{MediaSource, MediaSourceStream, ReadBytes, SeekBuffered}; +use symphonia_core::meta::{Metadata, MetadataLog}; +use symphonia_core::probe::{Descriptor, Instantiate, QueryDescriptor}; +use symphonia_core::units::Time; + +use std::io::{Seek, SeekFrom}; +use std::sync::Arc; + +use crate::atoms::{AtomIterator, AtomType}; +use crate::atoms::{FtypAtom, MetaAtom, MoofAtom, MoovAtom, MvexAtom, SidxAtom, TrakAtom}; +use crate::stream::*; + +use log::{debug, info, trace, warn}; + +pub struct TrackState { + codec_params: CodecParameters, + /// The track number. + track_num: usize, + /// The current segment. + cur_seg: usize, + /// The current sample index relative to the track. + next_sample: u32, + /// The current sample byte position relative to the start of the track. + next_sample_pos: u64, +} + +impl TrackState { + #[allow(clippy::single_match)] + pub fn new(track_num: usize, trak: &TrakAtom) -> Self { + let mut codec_params = CodecParameters::new(); + + codec_params + .with_time_base(TimeBase::new(1, trak.mdia.mdhd.timescale)) + .with_n_frames(trak.mdia.mdhd.duration); + + // Fill the codec parameters using the sample description atom. + trak.mdia.minf.stbl.stsd.fill_codec_params(&mut codec_params); + + Self { codec_params, track_num, cur_seg: 0, next_sample: 0, next_sample_pos: 0 } + } + + pub fn codec_params(&self) -> CodecParameters { + self.codec_params.clone() + } +} + +/// Information regarding the next sample. +#[derive(Debug)] +struct NextSampleInfo { + /// The track number of the next sample. + track_num: usize, + /// The timestamp of the next sample. + ts: u64, + /// The timestamp expressed in seconds. + time: Time, + /// The duration of the next sample. + dur: u32, + /// The segment containing the next sample. + seg_idx: usize, +} + +/// Information regarding a sample. +#[derive(Debug)] +struct SampleDataInfo { + /// The position of the sample in the track. + pos: u64, + /// The length of the sample. + len: u32, +} + +/// ISO Base Media File Format (MP4, M4A, MOV, etc.) demultiplexer. +/// +/// `IsoMp4Reader` implements a demuxer for the ISO Base Media File Format. +pub struct IsoMp4Reader { + iter: AtomIterator, + tracks: Vec, + cues: Vec, + metadata: MetadataLog, + /// Segments of the movie. Sorted in ascending order by sequence number. + segs: Vec>, + /// State tracker for each track. + track_states: Vec, + /// Optional, movie extends atom used for fragmented streams. + mvex: Option>, +} + +impl IsoMp4Reader { + /// Idempotently gets information regarding the next sample of the media stream. This function + /// selects the next sample with the lowest timestamp of all tracks. + fn next_sample_info(&self) -> Result> { + let mut earliest = None; + + // TODO: Consider returning samples based on lowest byte position in the track instead of + // timestamp. This may be important if video tracks are ever decoded (i.e., DTS vs. PTS). + + for (state, track) in self.track_states.iter().zip(&self.tracks) { + // Get the timebase of the track used to calculate the presentation time. + let tb = track.codec_params.time_base.unwrap(); + + // Get the next timestamp for the next sample of the current track. The next sample may + // be in a future segment. + for (seg_idx_delta, seg) in self.segs[state.cur_seg..].iter().enumerate() { + // Try to get the timestamp for the next sample of the track from the segment. + if let Some(timing) = seg.sample_timing(state.track_num, state.next_sample)? { + // Calculate the presentation time using the timestamp. + let sample_time = tb.calc_time(timing.ts); + + // Compare the presentation time of the sample from this track to other tracks, + // and select the track with the earliest presentation time. + match earliest { + Some(NextSampleInfo { track_num: _, ts: _, time, dur: _, seg_idx: _ }) + if time <= sample_time => + { + // Earliest is less than or equal to the track's next sample + // presentation time. No need to update earliest. + } + _ => { + // Earliest was either None, or greater than the track's next sample + // presentation time. Update earliest. + earliest = Some(NextSampleInfo { + track_num: state.track_num, + ts: timing.ts, + time: sample_time, + dur: timing.dur, + seg_idx: seg_idx_delta + state.cur_seg, + }); + } + } + + // Either the next sample of the track had the earliest presentation time seen + // thus far, or it was greater than those from other tracks, but there is no + // reason to check samples in future segments. + break; + } + } + } + + Ok(earliest) + } + + fn consume_next_sample(&mut self, info: &NextSampleInfo) -> Result> { + // Get the track state. + let track = &mut self.track_states[info.track_num]; + + // Get the segment associated with the sample. + let seg = &self.segs[info.seg_idx]; + + // Get the sample data descriptor. + let sample_data_desc = seg.sample_data(track.track_num, track.next_sample, false)?; + + // The sample base position in the sample data descriptor remains constant if the sample + // followed immediately after the previous sample. In this case, the track state's + // next_sample_pos is the position of the current sample. If the base position has jumped, + // then the base position is the position of current the sample. + let pos = if sample_data_desc.base_pos > track.next_sample_pos { + sample_data_desc.base_pos + } + else { + track.next_sample_pos + }; + + // Advance the track's current segment to the next sample's segment. + track.cur_seg = info.seg_idx; + + // Advance the track's next sample number and position. + track.next_sample += 1; + track.next_sample_pos = pos + u64::from(sample_data_desc.size); + + Ok(Some(SampleDataInfo { pos, len: sample_data_desc.size })) + } + + fn try_read_more_segments(&mut self) -> Result<()> { + // Continue iterating over atoms until a segment (a moof + mdat atom pair) is found. All + // other atoms will be ignored. + while let Some(header) = self.iter.next_no_consume()? { + match header.atype { + AtomType::MediaData => { + // Consume the atom from the iterator so that on the next iteration a new atom + // will be read. + self.iter.consume_atom(); + + return Ok(()); + } + AtomType::MovieFragment => { + let moof = self.iter.read_atom::()?; + + // A moof segment can only be created if the mvex atom is present. + if let Some(mvex) = &self.mvex { + // Get the last segment. Note, there will always be one segment because the + // moov atom is converted into a segment when the reader is instantiated. + let last_seg = self.segs.last().unwrap(); + + // Create a new segment for the moof atom. + let seg = MoofSegment::new(moof, mvex.clone(), last_seg.as_ref()); + + // Segments should have a monotonic sequence number. + if seg.sequence_num() <= last_seg.sequence_num() { + warn!("moof fragment has a non-monotonic sequence number."); + } + + // Push the segment. + self.segs.push(Box::new(seg)); + } + else { + // TODO: This is a fatal error. + return decode_error("isomp4: moof atom present without mvex atom"); + } + } + _ => { + trace!("skipping atom: {:?}.", header.atype); + self.iter.consume_atom(); + } + } + } + + // If no atoms were returned above, then the end-of-stream has been reached. + end_of_stream_error() + } + + fn seek_track_by_time(&mut self, track_num: usize, time: Time) -> Result { + // Convert time to timestamp for the track. + if let Some(track) = self.tracks.get(track_num) { + let tb = track.codec_params.time_base.unwrap(); + self.seek_track_by_ts(track_num, tb.calc_timestamp(time)) + } + else { + seek_error(SeekErrorKind::Unseekable) + } + } + + fn seek_track_by_ts(&mut self, track_num: usize, ts: u64) -> Result { + debug!("seeking track={} to frame_ts={}", track_num, ts); + + struct SeekLocation { + seg_idx: usize, + sample_num: u32, + } + + let mut seek_loc = None; + let mut seg_skip = 0; + + loop { + // Iterate over all segments and attempt to find the segment and sample number that + // contains the desired timestamp. Skip segments already examined. + for (seg_idx, seg) in self.segs.iter().enumerate().skip(seg_skip) { + if let Some(sample_num) = seg.ts_sample(track_num, ts)? { + seek_loc = Some(SeekLocation { seg_idx, sample_num }); + break; + } + + // Mark the segment as examined. + seg_skip = seg_idx + 1; + } + + // If a seek location is found, break. + if seek_loc.is_some() { + break; + } + + // Otherwise, try to read more segments from the stream. + self.try_read_more_segments()?; + } + + if let Some(seek_loc) = seek_loc { + let seg = &self.segs[seek_loc.seg_idx]; + + // Get the sample information. + let data_desc = seg.sample_data(track_num, seek_loc.sample_num, true)?; + + // Update the track's next sample information to point to the seeked sample. + let track = &mut self.track_states[track_num]; + + track.cur_seg = seek_loc.seg_idx; + track.next_sample = seek_loc.sample_num; + track.next_sample_pos = data_desc.base_pos + data_desc.offset.unwrap(); + + // Get the actual timestamp for this sample. + let timing = seg.sample_timing(track_num, seek_loc.sample_num)?.unwrap(); + + debug!( + "seeked track={} to packet_ts={} (delta={})", + track_num, + timing.ts, + timing.ts as i64 - ts as i64 + ); + + Ok(SeekedTo { track_id: track_num as u32, required_ts: ts, actual_ts: timing.ts }) + } + else { + // Timestamp was not found. + seek_error(SeekErrorKind::OutOfRange) + } + } +} + +impl QueryDescriptor for IsoMp4Reader { + fn query() -> &'static [Descriptor] { + &[support_format!( + "isomp4", + "ISO Base Media File Format", + &["mp4", "m4a", "m4p", "m4b", "m4r", "m4v", "mov"], + &["video/mp4", "audio/mp4"], + &[b"ftyp"] // Top-level atoms + )] + } + + fn score(_context: &[u8]) -> u8 { + 255 + } +} + +impl FormatReader for IsoMp4Reader { + fn try_new(mut mss: MediaSourceStream, _options: &FormatOptions) -> Result { + // To get to beginning of the atom. + mss.seek_buffered_rel(-4); + + let is_seekable = mss.is_seekable(); + + let mut ftyp = None; + let mut moov = None; + let mut sidx = None; + + // Get the total length of the stream, if possible. + let total_len = if is_seekable { + let pos = mss.pos(); + let len = mss.byte_len().ok_or(Error::SeekError(SeekErrorKind::Unseekable))?; + mss.seek(SeekFrom::Start(pos))?; + info!("stream is seekable with len={} bytes.", len); + Some(len) + } + else { + None + }; + + let mut metadata = MetadataLog::default(); + + // Parse all atoms if the stream is seekable, otherwise parse all atoms up-to the mdat atom. + let mut iter = AtomIterator::new_root(mss, total_len); + + while let Some(header) = iter.next()? { + // Top-level atoms. + match header.atype { + AtomType::FileType => { + ftyp = Some(iter.read_atom::()?); + } + AtomType::Movie => { + moov = Some(iter.read_atom::()?); + } + AtomType::SegmentIndex => { + // If the stream is not seekable, then it can only be assumed that the first + // segment index atom is indeed the first segment index because the format + // reader cannot practically skip past this point. + if !is_seekable { + sidx = Some(iter.read_atom::()?); + break; + } + else { + // If the stream is seekable, examine all segment indexes and select the + // index with the earliest presentation timestamp to be the first. + let new_sidx = iter.read_atom::()?; + + let is_earlier = match &sidx { + Some(sidx) => new_sidx.earliest_pts < sidx.earliest_pts, + _ => true, + }; + + if is_earlier { + sidx = Some(new_sidx); + } + } + } + AtomType::MediaData | AtomType::MovieFragment => { + // The mdat atom contains the codec bitstream data. For segmented streams, a + // moof + mdat pair is required for playback. If the source is unseekable then + // the format reader cannot skip past these atoms without dropping samples. + if !is_seekable { + // If the moov atom hasn't been seen before the moof and/or mdat atom, and + // the stream is not seekable, then the mp4 is not streamable. + if moov.is_none() || ftyp.is_none() { + warn!("mp4 is not streamable."); + } + + // The remainder of the stream will be read incrementally. + break; + } + } + AtomType::Meta => { + // Read the metadata atom and append it to the log. + let mut meta = iter.read_atom::()?; + + if let Some(rev) = meta.take_metadata() { + metadata.push(rev); + } + } + AtomType::Free => (), + AtomType::Skip => (), + _ => { + info!("skipping top-level atom: {:?}.", header.atype); + } + } + } + + if ftyp.is_none() { + return unsupported_error("isomp4: missing ftyp atom"); + } + + if moov.is_none() { + return unsupported_error("isomp4: missing moov atom"); + } + + // If the stream was seekable, then all atoms in the media source stream were scanned. Seek + // back to the first mdat atom for playback. If the stream is not seekable, then the atom + // iterator is currently positioned at the first mdat atom. + if is_seekable { + let mut mss = iter.into_inner(); + mss.seek(SeekFrom::Start(0))?; + + iter = AtomIterator::new_root(mss, total_len); + + while let Some(header) = iter.next_no_consume()? { + match header.atype { + AtomType::MediaData | AtomType::MovieFragment => break, + _ => (), + } + iter.consume_atom(); + } + } + + let mut moov = moov.unwrap(); + + if moov.is_fragmented() { + // If a Segment Index (sidx) atom was found, add the segments contained within. + if sidx.is_some() { + info!("stream is segmented with a segment index."); + } + else { + info!("stream is segmented without a segment index."); + } + } + + if let Some(rev) = moov.take_metadata() { + metadata.push(rev); + } + + // Instantiate a TrackState for each track in the stream. + let track_states = moov + .traks + .iter() + .enumerate() + .map(|(t, trak)| TrackState::new(t, trak)) + .collect::>(); + + // Instantiate a Tracks for all tracks above. + let tracks = track_states + .iter() + .map(|track| Track::new(track.track_num as u32, track.codec_params())) + .collect(); + + // A Movie Extends (mvex) atom is required to support segmented streams. If the mvex atom is + // present, wrap it in an Arc so it can be shared amongst all segments. + let mvex = moov.mvex.take().map(Arc::new); + + // The number of tracks specified in the moov atom must match the number in the mvex atom. + if let Some(mvex) = &mvex { + if mvex.trexs.len() != moov.traks.len() { + return decode_error("isomp4: mvex and moov track number mismatch"); + } + } + + let segs: Vec> = vec![Box::new(MoovSegment::new(moov))]; + + Ok(IsoMp4Reader { + iter, + tracks, + cues: Default::default(), + metadata, + track_states, + segs, + mvex, + }) + } + + fn next_packet(&mut self) -> Result { + // Get the index of the track with the next-nearest (minimum) timestamp. + let next_sample_info = loop { + // Using the current set of segments, try to get the next sample info. + if let Some(info) = self.next_sample_info()? { + break info; + } + else { + // No more segments. If the stream is unseekable, it may be the case that there are + // more segments coming. Iterate atoms until a new segment is found or the + // end-of-stream is reached. + self.try_read_more_segments()?; + } + }; + + // Get the position and length information of the next sample. + let sample_info = self.consume_next_sample(&next_sample_info)?.unwrap(); + + let reader = self.iter.inner_mut(); + + // Attempt a fast seek within the buffer cache. + if reader.seek_buffered(sample_info.pos) != sample_info.pos { + if reader.is_seekable() { + // Fallback to a slow seek if the stream is seekable. + reader.seek(SeekFrom::Start(sample_info.pos))?; + } + else if sample_info.pos > reader.pos() { + // The stream is not seekable but the desired seek position is ahead of the reader's + // current position, thus the seek can be emulated by ignoring the bytes up to the + // the desired seek position. + reader.ignore_bytes(sample_info.pos - reader.pos())?; + } + else { + // The stream is not seekable and the desired seek position falls outside the lower + // bound of the buffer cache. This sample cannot be read. + return decode_error("isomp4: packet out-of-bounds for a non-seekable stream"); + } + } + + Ok(Packet::new_from_boxed_slice( + next_sample_info.track_num as u32, + next_sample_info.ts, + u64::from(next_sample_info.dur), + reader.read_boxed_slice_exact(sample_info.len as usize)?, + )) + } + + fn metadata(&mut self) -> Metadata<'_> { + self.metadata.metadata() + } + + fn cues(&self) -> &[Cue] { + &self.cues + } + + fn tracks(&self) -> &[Track] { + &self.tracks + } + + fn seek(&mut self, _mode: SeekMode, to: SeekTo) -> Result { + if self.tracks.is_empty() { + return seek_error(SeekErrorKind::Unseekable); + } + + match to { + SeekTo::TimeStamp { ts, track_id } => { + let selected_track_id = track_id as usize; + + // The seek timestamp is in timebase units specific to the selected track. Get the + // selected track and use the timebase to convert the timestamp into time units so + // that the other tracks can be seeked. + if let Some(selected_track) = self.tracks().get(selected_track_id) { + // Convert to time units. + let time = selected_track.codec_params.time_base.unwrap().calc_time(ts); + + // Seek all tracks excluding the primary track to the desired time. + for t in 0..self.track_states.len() { + if t != selected_track_id { + self.seek_track_by_time(t, time)?; + } + } + + // Seek the primary track and return the result. + self.seek_track_by_ts(selected_track_id, ts) + } + else { + seek_error(SeekErrorKind::Unseekable) + } + } + SeekTo::Time { time, track_id } => { + // Select the first track if a selected track was not provided. + let selected_track_id = track_id.unwrap_or(0) as usize; + + // Seek all tracks excluding the selected track and discard the result. + for t in 0..self.track_states.len() { + if t != selected_track_id { + self.seek_track_by_time(t, time)?; + } + } + + // Seek the primary track and return the result. + self.seek_track_by_time(selected_track_id, time) + } + } + } + + fn into_inner(self: Box) -> MediaSourceStream { + self.iter.into_inner() + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/fourcc.rs b/src-tauri/patches/symphonia-format-isomp4/src/fourcc.rs new file mode 100644 index 00000000..d67a1f1b --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/fourcc.rs @@ -0,0 +1,31 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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; + +/// Four character codes for typical Ftyps (reference: http://ftyps.com/). +#[derive(PartialEq, Eq, Clone, Copy)] +#[repr(transparent)] +pub struct FourCc { + val: [u8; 4], +} + +impl FourCc { + /// Construct a new FourCC code from the given byte array. + pub fn new(val: [u8; 4]) -> Self { + Self { val } + } +} + +impl fmt::Debug for FourCc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match std::str::from_utf8(&self.val) { + Ok(name) => f.write_str(name), + _ => write!(f, "{:x?}", self.val), + } + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/fp.rs b/src-tauri/patches/symphonia-format-isomp4/src/fp.rs new file mode 100644 index 00000000..73e2db14 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/fp.rs @@ -0,0 +1,78 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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/. + +/// An unsigned 16.16-bit fixed point value. +#[derive(Copy, Clone, Debug, Default)] +pub struct FpU16(u32); + +impl FpU16 { + pub fn new(val: u16) -> Self { + Self(u32::from(val) << 16) + } + + pub fn parse_raw(val: u32) -> Self { + Self(val) + } +} + +impl From for f64 { + fn from(fp: FpU16) -> Self { + f64::from(fp.0) / f64::from(1u32 << 16) + } +} + +/// An unsigned 8.8-bit fixed point value. +#[derive(Copy, Clone, Debug, Default)] +pub struct FpU8(u16); + +impl FpU8 { + pub fn new(val: u8) -> Self { + Self(u16::from(val) << 8) + } + + pub fn parse_raw(val: u16) -> Self { + Self(val) + } +} + +impl From for f64 { + fn from(fp: FpU8) -> Self { + f64::from(fp.0) / f64::from(1u16 << 8) + } +} + +impl From for f32 { + fn from(fp: FpU8) -> Self { + f32::from(fp.0) / f32::from(1u16 << 8) + } +} + +/// An unsigned 8.8-bit fixed point value. +#[derive(Copy, Clone, Debug, Default)] +pub struct FpI8(i16); + +impl FpI8 { + pub fn new(val: i8) -> Self { + Self(i16::from(val) * 0x100) + } + + pub fn parse_raw(val: i16) -> Self { + Self(val) + } +} + +impl From for f64 { + fn from(fp: FpI8) -> Self { + f64::from(fp.0) / f64::from(1u16 << 8) + } +} + +impl From for f32 { + fn from(fp: FpI8) -> Self { + f32::from(fp.0) / f32::from(1u16 << 8) + } +} diff --git a/src-tauri/patches/symphonia-format-isomp4/src/lib.rs b/src-tauri/patches/symphonia-format-isomp4/src/lib.rs new file mode 100644 index 00000000..667b793e --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/lib.rs @@ -0,0 +1,22 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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/. + +#![warn(rust_2018_idioms)] +// The following lints are allowed in all Symphonia crates. Please see clippy.toml for their +// justification. +#![allow(clippy::comparison_chain)] +#![allow(clippy::excessive_precision)] +#![allow(clippy::identity_op)] +#![allow(clippy::manual_range_contains)] + +mod atoms; +mod demuxer; +mod fourcc; +mod fp; +mod stream; + +pub use demuxer::IsoMp4Reader; diff --git a/src-tauri/patches/symphonia-format-isomp4/src/stream.rs b/src-tauri/patches/symphonia-format-isomp4/src/stream.rs new file mode 100644 index 00000000..ec93c475 --- /dev/null +++ b/src-tauri/patches/symphonia-format-isomp4/src/stream.rs @@ -0,0 +1,444 @@ +// Symphonia +// Copyright (c) 2019-2022 The Project Symphonia Developers. +// +// 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 symphonia_core::errors::{decode_error, Error, Result}; + +use crate::atoms::{stsz::SampleSize, Co64Atom, MoofAtom, MoovAtom, MvexAtom, StcoAtom, TrafAtom}; + +use std::ops::Range; +use std::sync::Arc; + +/// Sample data information. +pub struct SampleDataDesc { + /// The starting byte position within the media data of the group of samples that contains the + /// sample described. + pub base_pos: u64, + /// The offset relative to the base position of the sample described. + pub offset: Option, + /// The size of the sample. + pub size: u32, +} + +/// Timing information for one sample. +pub struct SampleTiming { + /// The timestamp of the sample. + pub ts: u64, + /// The duration of the sample. + pub dur: u32, +} + +pub trait StreamSegment: Send + Sync { + /// Gets the sequence number of this segment. + fn sequence_num(&self) -> u32; + + /// Gets the first and last sample numbers for the track `track_num`. + fn track_sample_range(&self, track_num: usize) -> Range; + + /// Gets the first and last sample timestamps for the track `track_num`. + fn track_ts_range(&self, track_num: usize) -> Range; + + /// Get the timestamp and duration for the sample indicated by `sample_num` for the track + /// `track_num`. + fn sample_timing(&self, track_num: usize, sample_num: u32) -> Result>; + + /// Get the sample number of the sample containing the timestamp indicated by `ts` for track + // `track_num`. + fn ts_sample(&self, track_num: usize, ts: u64) -> Result>; + + /// Get the byte position of the group of samples containing the sample indicated by + /// `sample_num` for track `track_num`, and it's size. + /// + /// Optionally, the offset of the sample relative to the aforementioned byte position can be + /// returned. + fn sample_data( + &self, + track_num: usize, + sample_num: u32, + get_offset: bool, + ) -> Result; +} + +/// Track-to-stream sequencing information. +#[derive(Copy, Clone, Debug, Default)] +struct SequenceInfo { + /// The sample number of the first sample of a track in a fragment. + first_sample: u32, + /// The timestamp of the first sample of a track in a fragment. + first_ts: u64, + /// The total duration of all samples of a track in a fragment. + total_sample_duration: u64, + /// The total sample count of a track in a fragment. + total_sample_count: u32, + /// If present in the moof segment, this is the index of the track fragment atom for the track + /// this sequence information is associated with. + traf_idx: Option, +} + +pub struct MoofSegment { + moof: MoofAtom, + mvex: Arc, + seq: Vec, +} + +impl MoofSegment { + /// Instantiate a new segment from a `MoofAtom`. + pub fn new(moof: MoofAtom, mvex: Arc, prev: &dyn StreamSegment) -> MoofSegment { + let mut seq = Vec::with_capacity(mvex.trexs.len()); + + // Calculate the sequence information for each track, even if not present in the fragment. + for (track_num, trex) in mvex.trexs.iter().enumerate() { + let mut info = SequenceInfo { + first_sample: prev.track_sample_range(track_num).end, + first_ts: prev.track_ts_range(track_num).end, + ..Default::default() + }; + + // Find the track fragment for the track. + for (traf_idx, traf) in moof.trafs.iter().enumerate() { + if trex.track_id != traf.tfhd.track_id { + continue; + } + + // Calculate the total duration of all runs in the fragment for the track. + let default_dur = + traf.tfhd.default_sample_duration.unwrap_or(trex.default_sample_duration); + + for trun in traf.truns.iter() { + info.total_sample_duration += trun.total_duration(default_dur); + } + + info.total_sample_count = traf.total_sample_count; + info.traf_idx = Some(traf_idx); + } + + seq.push(info); + } + + MoofSegment { moof, mvex, seq } + } + + /// Try to get the Track Fragment atom associated with the track identified by `track_num`. + fn try_get_traf(&self, track_num: usize) -> Option<&TrafAtom> { + debug_assert!(track_num < self.seq.len()); + self.seq[track_num].traf_idx.map(|idx| &self.moof.trafs[idx]) + } +} + +impl StreamSegment for MoofSegment { + fn sequence_num(&self) -> u32 { + self.moof.mfhd.sequence_number + } + + fn sample_timing(&self, track_num: usize, sample_num: u32) -> Result> { + // Get the track fragment associated with track_num. + let traf = match self.try_get_traf(track_num) { + Some(traf) => traf, + None => return Ok(None), + }; + + let mut sample_num_rel = sample_num - self.seq[track_num].first_sample; + let mut trun_ts_offset = self.seq[track_num].first_ts; + + let default_dur = traf + .tfhd + .default_sample_duration + .unwrap_or(self.mvex.trexs[track_num].default_sample_duration); + + for trun in traf.truns.iter() { + // If the sample is contained within the this track run, get the timing of of the + // sample. + if sample_num_rel < trun.sample_count { + let (ts, dur) = trun.sample_timing(sample_num_rel, default_dur); + return Ok(Some(SampleTiming { ts: trun_ts_offset + ts, dur })); + } + + let trun_dur = trun.total_duration(default_dur); + + sample_num_rel -= trun.sample_count; + trun_ts_offset += trun_dur; + } + + Ok(None) + } + + fn ts_sample(&self, track_num: usize, ts: u64) -> Result> { + // Get the track fragment associated with track_num. + let traf = match self.try_get_traf(track_num) { + Some(traf) => traf, + None => return Ok(None), + }; + + let mut sample_num = self.seq[track_num].first_sample; + let mut ts_accum = self.seq[track_num].first_ts; + + let default_dur = traf + .tfhd + .default_sample_duration + .unwrap_or(self.mvex.trexs[track_num].default_sample_duration); + + for trun in traf.truns.iter() { + // Get the total duration of this track run. + let trun_dur = trun.total_duration(default_dur); + + // If the timestamp after the track run is greater than the desired timestamp, then the + // desired sample must be in this run of samples. + if ts_accum + trun_dur > ts { + sample_num += trun.ts_sample(ts - ts_accum, default_dur); + return Ok(Some(sample_num)); + } + + sample_num += trun.sample_count; + ts_accum += trun_dur; + } + + Ok(None) + } + + fn sample_data( + &self, + track_num: usize, + sample_num: u32, + get_offset: bool, + ) -> Result { + // Get the track fragment associated with track_num. + let traf = self.try_get_traf(track_num).unwrap(); + + // If an explicit anchor-point is set, then use that for the position, otherwise use the + // first-byte of the enclosing moof atom. + let traf_base_pos = match traf.tfhd.base_data_offset { + Some(pos) => pos, + _ => self.moof.moof_base_pos, + }; + + let mut sample_num_rel = sample_num - self.seq[track_num].first_sample; + let mut trun_offset = traf_base_pos; + + let default_size = + traf.tfhd.default_sample_size.unwrap_or(self.mvex.trexs[track_num].default_sample_size); + + for trun in traf.truns.iter() { + // If a data offset is present for this track fragment run, then calculate the new base + // position for the run. When a data offset is not present, do nothing because this run + // follows the previous run. + if let Some(offset) = trun.data_offset { + // The offset for the run is relative to the anchor-point defined in the track + // fragment header. + trun_offset = if offset.is_negative() { + traf_base_pos - u64::from(offset.wrapping_abs() as u32) + } + else { + traf_base_pos + offset as u64 + }; + } + + if sample_num_rel < trun.sample_count { + let (offset, size) = if get_offset { + // Get the size and offset of the sample. + let (offset, size) = trun.sample_offset(sample_num_rel, default_size); + (Some(offset), size) + } + else { + // Just get the size of the sample. + let size = trun.sample_size(sample_num_rel, default_size); + (None, size) + }; + + return Ok(SampleDataDesc { base_pos: trun_offset, size, offset }); + } + + // Get the total size of the track fragment run. + let trun_size = trun.total_size(default_size); + + sample_num_rel -= trun.sample_count; + trun_offset += trun_size; + } + + decode_error("isomp4: invalid sample index") + } + + fn track_sample_range(&self, track_num: usize) -> Range { + debug_assert!(track_num < self.seq.len()); + + let track = &self.seq[track_num]; + track.first_sample..track.first_sample + track.total_sample_count + } + + fn track_ts_range(&self, track_num: usize) -> Range { + debug_assert!(track_num < self.seq.len()); + + let track = &self.seq[track_num]; + track.first_ts..track.first_ts + track.total_sample_duration + } +} + +fn get_chunk_offset( + stco: &Option, + co64: &Option, + chunk: usize, +) -> Result> { + // Get the offset from either the stco or co64 atoms. + if let Some(stco) = stco.as_ref() { + // 32-bit offset + if let Some(offset) = stco.chunk_offsets.get(chunk) { + Ok(Some(u64::from(*offset))) + } + else { + decode_error("isomp4: missing stco entry") + } + } + else if let Some(co64) = co64.as_ref() { + // 64-bit offset + if let Some(offset) = co64.chunk_offsets.get(chunk) { + Ok(Some(*offset)) + } + else { + decode_error("isomp4: missing co64 entry") + } + } + else { + // This should never happen because it is mandatory to have either a stco or co64 atom. + decode_error("isomp4: missing stco or co64 atom") + } +} + +pub struct MoovSegment { + moov: MoovAtom, +} + +impl MoovSegment { + /// Instantiate a segment from the provide moov atom. + pub fn new(moov: MoovAtom) -> MoovSegment { + MoovSegment { moov } + } +} + +impl StreamSegment for MoovSegment { + fn sequence_num(&self) -> u32 { + // The segment defined by the moov atom is always 0. + 0 + } + + fn sample_timing(&self, track_num: usize, sample_num: u32) -> Result> { + // Get the trak atom associated with track_num. + debug_assert!(track_num < self.moov.traks.len()); + + let trak = &self.moov.traks[track_num]; + + // Find the sample timing. Note, complexity of O(N). + let timing = trak.mdia.minf.stbl.stts.find_timing_for_sample(sample_num); + + if let Some((ts, dur)) = timing { + Ok(Some(SampleTiming { ts, dur })) + } + else { + Ok(None) + } + } + + fn ts_sample(&self, track_num: usize, ts: u64) -> Result> { + // Get the trak atom associated with track_num. + debug_assert!(track_num < self.moov.traks.len()); + + let trak = &self.moov.traks[track_num]; + + // Find the sample timestamp. Note, complexity of O(N). + Ok(trak.mdia.minf.stbl.stts.find_sample_for_timestamp(ts)) + } + + fn sample_data( + &self, + track_num: usize, + sample_num: u32, + get_offset: bool, + ) -> Result { + // Get the trak atom associated with track_num. + debug_assert!(track_num < self.moov.traks.len()); + + let trak = &self.moov.traks[track_num]; + + // Get the constituent tables. + let stsz = &trak.mdia.minf.stbl.stsz; + let stsc = &trak.mdia.minf.stbl.stsc; + let stco = &trak.mdia.minf.stbl.stco; + let co64 = &trak.mdia.minf.stbl.co64; + + // Find the sample-to-chunk mapping. Note, complexity of O(log N). + let group = stsc + .find_entry_for_sample(sample_num) + .ok_or(Error::DecodeError("invalid sample index"))?; + + // Index of the sample relative to the chunk group. + let sample_in_group = sample_num - group.first_sample; + + // Index of the chunk containing the sample relative to the chunk group. + let chunk_in_group = sample_in_group / group.samples_per_chunk; + + // Index of the chunk containing the sample relative to the entire stream. + let chunk_in_stream = group.first_chunk + chunk_in_group; + + // Get the byte position of the first sample of the chunk containing the sample. + let base_pos = get_chunk_offset(stco, co64, chunk_in_stream as usize)?.unwrap(); + + // Determine the absolute sample byte position if requested by calculating the offset of + // the sample from the base position of the chunk. + let offset = if get_offset { + // Index of the sample relative to the chunk containing the sample. + let sample_in_chunk = sample_in_group - (chunk_in_group * group.samples_per_chunk); + + // Calculat the byte offset of the sample relative to the chunk containing it. + let offset = match stsz.sample_sizes { + SampleSize::Constant(size) => { + // Constant size samples can be calculated directly. + u64::from(sample_in_chunk) * u64::from(size) + } + SampleSize::Variable(ref entries) => { + // For variable size samples, sum the sizes of all the samples preceeding the + // desired sample in the chunk. + let chunk_first_sample = (sample_num - sample_in_chunk) as usize; + + if let Some(samples) = entries.get(chunk_first_sample..sample_num as usize) { + samples.iter().map(|&size| u64::from(size)).sum() + } + else { + return decode_error("isomp4: missing one or more stsz entries"); + } + } + }; + + Some(offset) + } + else { + None + }; + + // Get the size in bytes of the sample. + let size = match stsz.sample_sizes { + SampleSize::Constant(size) => size, + SampleSize::Variable(ref entries) => { + if let Some(size) = entries.get(sample_num as usize) { + *size + } + else { + return decode_error("isomp4: missing stsz entry"); + } + } + }; + + Ok(SampleDataDesc { base_pos, size, offset }) + } + + fn track_sample_range(&self, track_num: usize) -> Range { + debug_assert!(track_num < self.moov.traks.len()); + + 0..self.moov.traks[track_num].mdia.minf.stbl.stsz.sample_count + } + + fn track_ts_range(&self, track_num: usize) -> Range { + debug_assert!(track_num < self.moov.traks.len()); + + 0..self.moov.traks[track_num].mdia.minf.stbl.stts.total_duration + } +} diff --git a/src-tauri/src/audio.rs b/src-tauri/src/audio.rs index 991ecd6c..6535097c 100644 --- a/src-tauri/src/audio.rs +++ b/src-tauri/src/audio.rs @@ -923,14 +923,25 @@ impl SizedDecoder { hint.with_extension(ext); } let format_opts = FormatOptions { - enable_gapless: true, + // Disable gapless parsing — Symphonia 0.5.5 crashes on `edts` atoms + // present in older iTunes-purchased M4A files. + enable_gapless: false, + ..Default::default() + }; + + let meta_opts = symphonia::core::meta::MetadataOptions { + // Cap embedded cover art at 8 MiB so oversized MJPEG images in + // iTunes M4A files don't choke the parser. + limit_visual_bytes: symphonia::core::meta::Limit::Maximum(8 * 1024 * 1024), ..Default::default() }; let probed = symphonia::default::get_probe() - .format(&hint, mss, &format_opts, &MetadataOptions::default()) + .format(&hint, mss, &format_opts, &meta_opts) .map_err(|e| { let hint_str = format_hint.unwrap_or("unknown"); + // Always print the raw Symphonia error to the terminal for diagnosis. + eprintln!("[psysonic] probe failed (hint={hint_str}): {e}"); if e.to_string().to_lowercase().contains("unsupported") { format!("unsupported format: .{hint_str} files cannot be played (no demuxer)") } else { @@ -941,8 +952,17 @@ impl SizedDecoder { let track = probed.format .tracks() .iter() - .find(|t| t.codec_params.codec != CODEC_TYPE_NULL) - .ok_or_else(|| "no playable audio track found in file".to_string())?; + // Explicitly select only audio tracks: must have a valid codec and a + // sample_rate. This skips MJPEG cover-art streams that iTunes M4A + // files embed as a secondary video track. + .find(|t| { + t.codec_params.codec != CODEC_TYPE_NULL + && t.codec_params.sample_rate.is_some() + }) + .ok_or_else(|| { + eprintln!("[psysonic] no audio track found among {} tracks", probed.format.tracks().len()); + "no playable audio track found in file".to_string() + })?; let track_id = track.id; let total_duration = track.codec_params.time_base @@ -952,6 +972,7 @@ impl SizedDecoder { let mut decoder = symphonia::default::get_codecs() .make(&track.codec_params, &DecoderOptions::default()) .map_err(|e| { + eprintln!("[psysonic] codec init failed: {e}"); if e.to_string().to_lowercase().contains("unsupported") { "unsupported codec: no decoder available for this audio format".to_string() } else { @@ -969,18 +990,28 @@ impl SizedDecoder { Err(symphonia::core::errors::Error::IoError(_)) => { break decoder.last_decoded(); } - Err(e) => return Err(format!("could not read audio data: {e}")), + Err(e) => { + eprintln!("[psysonic] next_packet error: {e}"); + return Err(format!("could not read audio data: {e}")); + } }; - if packet.track_id() != track_id { continue; } + if packet.track_id() != track_id { + eprintln!("[psysonic] skipping packet for track {} (want {})", packet.track_id(), track_id); + continue; + } match decoder.decode(&packet) { Ok(decoded) => break decoded, - Err(symphonia::core::errors::Error::DecodeError(_)) => { + Err(symphonia::core::errors::Error::DecodeError(ref msg)) => { + eprintln!("[psysonic] decode error (retry {decode_errors}): {msg}"); decode_errors += 1; if decode_errors > DECODE_MAX_RETRIES { return Err("too many decode errors — file may be corrupt".into()); } } - Err(e) => return Err(format!("audio decode error: {e}")), + Err(e) => { + eprintln!("[psysonic] fatal decode error: {e}"); + return Err(format!("audio decode error: {e}")); + } } }; @@ -1201,8 +1232,17 @@ fn parse_gapless_info(data: &[u8]) -> GaplessInfo { None => return GaplessInfo::default(), }; - // Collect printable ASCII bytes after the tag (skip nulls / control chars) - let tail = &data[pos + 8..data.len().min(pos + 8 + 256)]; + // In M4A/iTunes files the key is followed by a binary 'data' atom header + // (16 bytes: size[4] + "data"[4] + type_flags[4] + locale[4]) before the + // actual value string. Search for the " 00000000 " sentinel that every + // iTunSMPB value starts with to locate the true start of the text. + let search_end = data.len().min(pos + 8 + 128); + let search_window = &data[pos + 8..search_end]; + let value_start = find_subsequence(search_window, b" 00000000 ") + .map(|off| pos + 8 + off) + .unwrap_or(pos + 8); + + let tail = &data[value_start..data.len().min(value_start + 256)]; let text: String = tail.iter() .map(|&b| b as char) .filter(|c| c.is_ascii_hexdigit() || *c == ' ')