├── gen ├── .gitignore ├── Cargo.toml └── src │ └── main.rs ├── .gitignore ├── fuzz ├── .gitignore ├── fuzz_targets │ └── fuzz_read.rs └── Cargo.toml ├── files ├── sample.m4a ├── artwork.png ├── sample-64.mp4 ├── sample-chaptered.m4a ├── sample-multi-data.m4a ├── sample-multi-track.3gp └── .gitignore ├── .rustfmt.toml ├── src ├── atom │ ├── mdat.rs │ ├── ftyp.rs │ ├── state.rs │ ├── url.rs │ ├── chap.rs │ ├── dinf.rs │ ├── tref.rs │ ├── ilst.rs │ ├── gmin.rs │ ├── gmhd.rs │ ├── stco.rs │ ├── stts.rs │ ├── co64.rs │ ├── hdlr.rs │ ├── udta.rs │ ├── stsc.rs │ ├── stsd.rs │ ├── dref.rs │ ├── moov.rs │ ├── stsz.rs │ ├── meta.rs │ ├── minf.rs │ ├── mdia.rs │ ├── trak.rs │ ├── text.rs │ ├── mvhd.rs │ ├── metaitem.rs │ ├── mdhd.rs │ ├── chpl.rs │ ├── tkhd.rs │ ├── head.rs │ ├── stbl.rs │ ├── mp4a.rs │ ├── util.rs │ └── change.rs ├── util.rs ├── tag │ ├── userdata │ │ ├── generate.toml │ │ ├── genre.rs │ │ └── tuple.rs │ ├── readonly.rs │ └── mod.rs ├── error.rs ├── lib.rs └── types.rs ├── examples └── string_encoding.rs ├── Cargo.toml ├── .github └── workflows │ └── build.yml ├── LICENSE-MIT ├── CHANGELOG.md ├── README.md ├── tests └── handling.rs └── LICENSE-APACHE /gen/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /target 3 | Cargo.lock 4 | bacon.toml 5 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | corpus 3 | artifacts 4 | coverage 5 | -------------------------------------------------------------------------------- /files/sample.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saecki/mp4ameta/HEAD/files/sample.m4a -------------------------------------------------------------------------------- /files/artwork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saecki/mp4ameta/HEAD/files/artwork.png -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_small_heuristics = "Max" 2 | max_width = 100 3 | struct_lit_width = 50 4 | -------------------------------------------------------------------------------- /files/sample-64.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saecki/mp4ameta/HEAD/files/sample-64.mp4 -------------------------------------------------------------------------------- /files/sample-chaptered.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saecki/mp4ameta/HEAD/files/sample-chaptered.m4a -------------------------------------------------------------------------------- /files/sample-multi-data.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saecki/mp4ameta/HEAD/files/sample-multi-data.m4a -------------------------------------------------------------------------------- /files/sample-multi-track.3gp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saecki/mp4ameta/HEAD/files/sample-multi-track.3gp -------------------------------------------------------------------------------- /files/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !sample.m4a 4 | !sample-multi-data.m4a 5 | !sample-64.mp4 6 | !sample-multi-track.3gp 7 | !sample-chaptered.m4a 8 | !artwork.png 9 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/fuzz_read.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use libfuzzer_sys::fuzz_target; 4 | 5 | fuzz_target!(|data: &[u8]| { 6 | let mut reader = std::io::Cursor::new(data); 7 | _ = mp4ameta::Tag::read_from(&mut reader); 8 | }); 9 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mp4ameta-fuzz" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2024" 6 | 7 | [package.metadata] 8 | cargo-fuzz = true 9 | 10 | [dependencies] 11 | libfuzzer-sys = "0.4" 12 | 13 | [dependencies.mp4ameta] 14 | path = ".." 15 | 16 | [[bin]] 17 | name = "fuzz_read" 18 | path = "fuzz_targets/fuzz_read.rs" 19 | test = false 20 | doc = false 21 | bench = false 22 | -------------------------------------------------------------------------------- /gen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mp4ameta_gen" 3 | version = "0.7.0" 4 | authors = ["Saecki "] 5 | license = "MIT OR Apache-2.0" 6 | description = "Code generation for common accessors of the mp4ameta crate." 7 | repository = "https://github.com/Saecki/mp4ameta" 8 | edition = "2021" 9 | 10 | [dependencies] 11 | toml = { version = "0.8.20", default-features = false, features = ["parse", "preserve_order"] } 12 | -------------------------------------------------------------------------------- /src/atom/mdat.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 4 | pub struct Mdat; 5 | 6 | impl Atom for Mdat { 7 | const FOURCC: Fourcc = MEDIA_DATA; 8 | } 9 | 10 | impl Mdat { 11 | pub fn read_bounds(reader: &mut (impl Read + Seek), size: Size) -> crate::Result { 12 | let bounds = find_bounds(reader, size)?; 13 | reader.seek(SeekFrom::Start(bounds.end()))?; 14 | Ok(bounds) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/string_encoding.rs: -------------------------------------------------------------------------------- 1 | use mp4ameta::{Data, Tag}; 2 | 3 | /// Reencode all utf-16 encoded strings in utf-8. 4 | fn main() { 5 | let mut tag = Tag::read_from_path("music.m4a").expect("error reading tag"); 6 | 7 | tag.data_mut().for_each(|(_, d)| { 8 | if let Data::Utf16(s) = d { 9 | let value = std::mem::take(s); 10 | *d = Data::Utf8(value); 11 | } 12 | }); 13 | 14 | tag.write_to_path("music.m4a").expect("error writing tag"); 15 | } 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mp4ameta" 3 | version = "0.13.0" 4 | authors = ["Saecki "] 5 | license = "MIT OR Apache-2.0" 6 | readme = "README.md" 7 | description = "A library for reading and writing iTunes style MPEG-4 audio metadata." 8 | categories = ["multimedia::audio", "parser-implementations"] 9 | keywords = ["mp4", "m4a", "audio", "metadata", "parser"] 10 | repository = "https://github.com/Saecki/rust-mp4ameta" 11 | edition = "2024" 12 | include = ["src", "LICENSE-APACHE", "LICENSE-MIT"] 13 | 14 | [dev-dependencies] 15 | walkdir = "2.5.0" 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | schedule: 8 | - cron: '0 0 * * 1' # weekly 9 | 10 | jobs: 11 | build: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ ubuntu-latest, windows-latest ] 16 | toolchain: [ stable, nightly ] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: dtolnay/rust-toolchain@stable 21 | with: 22 | toolchain: ${{ matrix.toolchain }} 23 | components: rustfmt 24 | - run: cargo test 25 | - run: cargo fmt -- --check 26 | -------------------------------------------------------------------------------- /src/atom/ftyp.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, PartialEq, Eq)] 4 | pub struct Ftyp { 5 | pub size: Size, 6 | pub string: String, 7 | } 8 | 9 | impl Ftyp { 10 | pub fn parse(reader: &mut (impl Read + Seek), file_len: u64) -> crate::Result { 11 | let head = head::parse(reader, file_len)?; 12 | if head.fourcc() != FILETYPE { 13 | return Err(crate::Error::new(ErrorKind::NoFtyp, "No filetype atom found.")); 14 | } 15 | 16 | let string = reader.read_utf8(head.content_len())?; 17 | 18 | Ok(Ftyp { size: head.size(), string }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tobias Schmitz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/atom/state.rs: -------------------------------------------------------------------------------- 1 | use crate::atom::head::AtomBounds; 2 | 3 | /// The state of an atom. 4 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 5 | pub enum State { 6 | /// The atom already exists. Contains the current bounds the atom. 7 | Existing(AtomBounds), 8 | /// The atom already existed and will be replaced. Contains the old bounds the atom. 9 | Replace(AtomBounds), 10 | /// The atom already existed and will be removed. Contains the old bounds the atom. 11 | Remove(AtomBounds), 12 | /// The atom will be added. 13 | #[default] 14 | Insert, 15 | } 16 | 17 | impl State { 18 | pub fn has_existed(&self) -> bool { 19 | matches!(self, Self::Existing(_) | Self::Replace(_) | Self::Remove(_)) 20 | } 21 | 22 | pub fn is_existing(&self) -> bool { 23 | matches!(self, Self::Existing(_)) 24 | } 25 | 26 | pub fn replace_existing(&mut self) { 27 | if let Self::Existing(b) = self { 28 | *self = Self::Replace(b.clone()) 29 | } 30 | } 31 | 32 | pub fn remove_existing(&mut self) { 33 | if let Self::Existing(b) = self { 34 | *self = Self::Remove(b.clone()) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/atom/url.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 4 | pub struct Url { 5 | pub state: State, 6 | pub data: Cow<'static, [u8]>, 7 | } 8 | 9 | impl Atom for Url { 10 | const FOURCC: Fourcc = URL_MEDIA; 11 | } 12 | 13 | impl ParseAtom for Url { 14 | fn parse_atom( 15 | reader: &mut (impl Read + Seek), 16 | _cfg: &ParseConfig<'_>, 17 | size: Size, 18 | ) -> crate::Result { 19 | let bounds = find_bounds(reader, size)?; 20 | let data = reader.read_u8_vec(size.content_len())?; 21 | Ok(Self { 22 | state: State::Existing(bounds), 23 | data: Cow::Owned(data), 24 | }) 25 | } 26 | } 27 | 28 | impl AtomSize for Url { 29 | fn size(&self) -> Size { 30 | Size::from(self.data.len() as u64) 31 | } 32 | } 33 | 34 | impl WriteAtom for Url { 35 | fn write_atom(&self, writer: &mut impl Write, _changes: &[Change<'_>]) -> crate::Result<()> { 36 | self.write_head(writer)?; 37 | writer.write_all(&self.data)?; 38 | Ok(()) 39 | } 40 | } 41 | 42 | impl Url { 43 | pub fn track() -> Self { 44 | Self { 45 | state: State::Insert, 46 | data: Cow::Borrowed(&[0x01, 0x00, 0x00, 0x00]), 47 | } 48 | } 49 | } 50 | 51 | impl LeafAtomCollectChanges for Url { 52 | fn state(&self) -> &State { 53 | &self.state 54 | } 55 | 56 | fn atom_ref(&self) -> AtomRef<'_> { 57 | AtomRef::Url(self) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::time::Duration; 3 | 4 | use crate::Chapter; 5 | 6 | pub(crate) fn format_duration(f: &mut fmt::Formatter<'_>, duration: Duration) -> fmt::Result { 7 | let total_seconds = duration.as_secs(); 8 | let nanos = duration.subsec_nanos(); 9 | let micros = nanos / 1_000; 10 | let millis = nanos / 1_000_000; 11 | let seconds = total_seconds % 60; 12 | let minutes = total_seconds / 60 % 60; 13 | let hours = total_seconds / 60 / 60; 14 | 15 | match (hours, minutes, seconds, millis, micros, nanos) { 16 | (0, 0, 0, 0, 0, n) => write!(f, "{n}ns"), 17 | (0, 0, 0, 0, u, _) => write!(f, "{u}µs"), 18 | (0, 0, 0, m, _, _) => write!(f, "{m}ms"), 19 | (0, 0, s, _, _, _) => write!(f, "{s}s"), 20 | (0, m, s, _, _, _) => write!(f, "{m}:{s:02}"), 21 | (h, m, s, _, _, _) => write!(f, "{h}:{m:02}:{s:02}"), 22 | } 23 | } 24 | 25 | pub(crate) fn format_chapters( 26 | f: &mut fmt::Formatter<'_>, 27 | chapters: &[Chapter], 28 | duration: Duration, 29 | ) -> fmt::Result { 30 | for (i, c) in chapters.iter().enumerate() { 31 | writeln!(f, " {}", c.title)?; 32 | if c.start == Duration::ZERO { 33 | f.write_str(" start: 0:00")?; 34 | } else { 35 | f.write_str(" start: ")?; 36 | format_duration(f, c.start)?; 37 | } 38 | 39 | let end = chapters.get(i + 1).map(|c| c.start).unwrap_or(duration); 40 | let duration = end.saturating_sub(c.start); 41 | f.write_str(", duration: ")?; 42 | format_duration(f, duration)?; 43 | writeln!(f)?; 44 | } 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## mp4ameta v0.13.0 6 | 7 | - Add label accessors (`----:com.apple.iTunes:LABEL`) 8 | - Make write_to function generic over a StorageFile trait 9 | 10 | ## mp4ameta v0.12.1 11 | 12 | - Add ReadConfig::NONE and WriteConfig::NONE 13 | 14 | ## mp4ameta v0.12.0 15 | 16 | - Implement FromStr for Fourcc 17 | - Add Userdata struct with metadata items and chapters 18 | - Enforce rust_2018_idioms 19 | - Add support for chapter tracks and lists 20 | - Replace proc-macro for userdata accessors with code generation 21 | - [**breaking**] Remove Userdata::dump functions 22 | - Fix some lifetime issues with userdata accessors 23 | - Fix length computation of utf-16 strings 24 | - Reduce number of read calls when parsing an atom head 25 | - Avoid calling stream_position in seek_to_end 26 | - Reduce number of read calls when parsing atom version and flags 27 | - Reduce number number syscalls when parsing mvhd, tkhd, mdhd 28 | - Reduce number of read calls when parsing data atom headers 29 | - Read sample table atoms only when needed 30 | - Allow using non-static borrowed strings in FreeformIdent 31 | - Avoid allocating statically known data 32 | - Rename Userdata::take_(data|bytes|strings|images) as into_* and don't use Rc 33 | - Reduce the number of read/seek calls when parsing mp4a atoms 34 | - Add clear_meta_items and meta_items_is_empty Userdata functions 35 | - Migrate to 2021 edition 36 | - Migrate to 2024 edition 37 | - Bound all allocations to parent atom size or file size 38 | - Avoid intermediary buffer when decoding utf-16 strings 39 | - Add sort order tags 40 | - Update quicktime readme links 41 | -------------------------------------------------------------------------------- /src/tag/userdata/generate.toml: -------------------------------------------------------------------------------- 1 | # This file is used to generate the `generated.rs` file with userdata accessors. 2 | 3 | [accessors.single_strings] 4 | "album" = "©alb" 5 | "copyright" = "cprt" 6 | "encoder" = "©too" 7 | "lyrics" = "©lyr" 8 | "movement" = "©mvn" 9 | "publisher" = "©pub" 10 | "title" = "©nam" 11 | "tv_episode_name" = "tven" 12 | "tv_network_name" = "tvnn" 13 | "tv_show_name" = "tvsh" 14 | "work" = "©wrk" 15 | "year" = "©day" 16 | "isrc" = "----:com.apple.iTunes:ISRC" 17 | "label" = "----:com.apple.iTunes:LABEL" 18 | "album_sort_order" = "soal" 19 | "title_sort_order" = "sonm" 20 | "tv_show_name_sort_order" = "sosn" 21 | 22 | 23 | [accessors.multiple_strings] 24 | "album_artist" = "aART" 25 | "artist" = "©ART" 26 | "category" = "catg" 27 | "comment" = "©cmt" 28 | "composer" = "©wrt" 29 | "custom_genre" = "©gen" 30 | "description" = "desc" 31 | "grouping" = "©grp" 32 | "keyword" = "keyw" 33 | "lyricist" = "----:com.apple.iTunes:LYRICIST" 34 | "album_artist_sort_order" = "soaa" 35 | "artist_sort_order" = "soar" 36 | "composer_sort_order" = "soco" 37 | 38 | [accessors.bool_flags] 39 | "compilation" = "cpil" 40 | "gapless_playback" = "pgap" 41 | "show_movement" = "shwm" 42 | 43 | [accessors.u16_ints] 44 | "bpm" = "tmpo" 45 | "movement_count" = "©mvc" 46 | "movement_index" = "©mvi" 47 | 48 | [accessors.u32_ints] 49 | "tv_episode" = "tves" 50 | "tv_season" = "tvsn" 51 | -------------------------------------------------------------------------------- /src/atom/chap.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub const ENTRY_SIZE: u64 = 4; 4 | 5 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 6 | pub struct Chap { 7 | pub state: State, 8 | pub chapter_ids: Vec, 9 | } 10 | 11 | impl Atom for Chap { 12 | const FOURCC: Fourcc = CHAPTER_REFERENCE; 13 | } 14 | 15 | impl ParseAtom for Chap { 16 | fn parse_atom( 17 | reader: &mut (impl Read + Seek), 18 | _cfg: &ParseConfig<'_>, 19 | size: Size, 20 | ) -> crate::Result { 21 | let bounds = find_bounds(reader, size)?; 22 | if !size.content_len().is_multiple_of(4) { 23 | return Err(crate::Error::new( 24 | ErrorKind::InvalidAtomSize, 25 | "Chapter reference (chap) atom size is not a multiple of 4", 26 | )); 27 | } 28 | 29 | let num_entries = size.content_len() / ENTRY_SIZE; 30 | let mut chapter_ids = Vec::with_capacity(num_entries as usize); 31 | for _ in 0..num_entries { 32 | chapter_ids.push(reader.read_be_u32()?); 33 | } 34 | 35 | Ok(Self { state: State::Existing(bounds), chapter_ids }) 36 | } 37 | } 38 | 39 | impl AtomSize for Chap { 40 | fn size(&self) -> Size { 41 | let content_len = ENTRY_SIZE * self.chapter_ids.len() as u64; 42 | Size::from(content_len) 43 | } 44 | } 45 | 46 | impl WriteAtom for Chap { 47 | fn write_atom(&self, writer: &mut impl Write, _changes: &[Change<'_>]) -> crate::Result<()> { 48 | self.write_head(writer)?; 49 | for c in self.chapter_ids.iter() { 50 | writer.write_be_u32(*c)?; 51 | } 52 | Ok(()) 53 | } 54 | } 55 | 56 | impl LeafAtomCollectChanges for Chap { 57 | fn state(&self) -> &State { 58 | &self.state 59 | } 60 | 61 | fn atom_ref(&self) -> AtomRef<'_> { 62 | AtomRef::Chap(self) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/atom/dinf.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 4 | pub struct Dinf { 5 | pub state: State, 6 | pub dref: Option, 7 | } 8 | 9 | impl Atom for Dinf { 10 | const FOURCC: Fourcc = DATA_INFORMATION; 11 | } 12 | 13 | impl ParseAtom for Dinf { 14 | fn parse_atom( 15 | reader: &mut (impl Read + Seek), 16 | cfg: &ParseConfig<'_>, 17 | size: Size, 18 | ) -> crate::Result { 19 | let bounds = find_bounds(reader, size)?; 20 | let mut dinf = Self { 21 | state: State::Existing(bounds), 22 | ..Default::default() 23 | }; 24 | let mut parsed_bytes = 0; 25 | 26 | while parsed_bytes < size.content_len() { 27 | let remaining_bytes = size.content_len() - parsed_bytes; 28 | let head = head::parse(reader, remaining_bytes)?; 29 | 30 | match head.fourcc() { 31 | DATA_REFERENCE => dinf.dref = Some(Dref::parse(reader, cfg, head.size())?), 32 | _ => reader.skip(head.content_len() as i64)?, 33 | } 34 | 35 | parsed_bytes += head.len(); 36 | } 37 | 38 | Ok(dinf) 39 | } 40 | } 41 | 42 | impl AtomSize for Dinf { 43 | fn size(&self) -> Size { 44 | let content_len = self.dref.len_or_zero(); 45 | Size::from(content_len) 46 | } 47 | } 48 | 49 | impl WriteAtom for Dinf { 50 | fn write_atom(&self, writer: &mut impl Write, changes: &[Change<'_>]) -> crate::Result<()> { 51 | self.write_head(writer)?; 52 | if let Some(a) = &self.dref { 53 | a.write(writer, changes)?; 54 | } 55 | Ok(()) 56 | } 57 | } 58 | 59 | impl SimpleCollectChanges for Dinf { 60 | fn state(&self) -> &State { 61 | &self.state 62 | } 63 | 64 | fn existing<'a>( 65 | &'a self, 66 | level: u8, 67 | bounds: &'a AtomBounds, 68 | changes: &mut Vec>, 69 | ) -> i64 { 70 | self.dref.collect_changes(bounds.end(), level, changes) 71 | } 72 | 73 | fn atom_ref(&self) -> AtomRef<'_> { 74 | AtomRef::Dinf(self) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/atom/tref.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 4 | pub struct Tref { 5 | pub state: State, 6 | pub chap: Option, 7 | } 8 | 9 | impl Atom for Tref { 10 | const FOURCC: Fourcc = TRACK_REFERENCE; 11 | } 12 | 13 | impl ParseAtom for Tref { 14 | fn parse_atom( 15 | reader: &mut (impl Read + Seek), 16 | cfg: &ParseConfig<'_>, 17 | size: Size, 18 | ) -> crate::Result { 19 | let bounds = find_bounds(reader, size)?; 20 | let mut tref = Self { 21 | state: State::Existing(bounds), 22 | ..Default::default() 23 | }; 24 | let mut parsed_bytes = 0; 25 | 26 | while parsed_bytes < size.content_len() { 27 | let remaining_bytes = size.content_len() - parsed_bytes; 28 | let head = head::parse(reader, remaining_bytes)?; 29 | 30 | match head.fourcc() { 31 | CHAPTER_REFERENCE => tref.chap = Some(Chap::parse(reader, cfg, head.size())?), 32 | _ => reader.skip(head.content_len() as i64)?, 33 | } 34 | 35 | parsed_bytes += head.len(); 36 | } 37 | 38 | Ok(tref) 39 | } 40 | } 41 | 42 | impl AtomSize for Tref { 43 | fn size(&self) -> Size { 44 | let content_len = self.chap.len_or_zero(); 45 | Size::from(content_len) 46 | } 47 | } 48 | 49 | impl WriteAtom for Tref { 50 | fn write_atom(&self, writer: &mut impl Write, changes: &[Change<'_>]) -> crate::Result<()> { 51 | self.write_head(writer)?; 52 | if let Some(a) = &self.chap { 53 | a.write(writer, changes)?; 54 | } 55 | Ok(()) 56 | } 57 | } 58 | 59 | impl SimpleCollectChanges for Tref { 60 | fn state(&self) -> &State { 61 | &self.state 62 | } 63 | 64 | fn existing<'a>( 65 | &'a self, 66 | level: u8, 67 | bounds: &'a AtomBounds, 68 | changes: &mut Vec>, 69 | ) -> i64 { 70 | self.chap.collect_changes(bounds.end(), level, changes) 71 | } 72 | 73 | fn atom_ref(&self) -> AtomRef<'_> { 74 | AtomRef::Tref(self) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/atom/ilst.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 4 | pub struct Ilst<'a> { 5 | pub state: State, 6 | pub data: Cow<'a, [MetaItem]>, 7 | } 8 | 9 | impl Atom for Ilst<'_> { 10 | const FOURCC: Fourcc = ITEM_LIST; 11 | } 12 | 13 | impl ParseAtom for Ilst<'_> { 14 | fn parse_atom( 15 | reader: &mut (impl Read + Seek), 16 | cfg: &ParseConfig<'_>, 17 | size: Size, 18 | ) -> crate::Result { 19 | let bounds = find_bounds(reader, size)?; 20 | let mut ilst = Vec::::new(); 21 | let mut parsed_bytes = 0; 22 | 23 | while parsed_bytes < size.content_len() { 24 | let remaining_bytes = size.content_len() - parsed_bytes; 25 | let head = head::parse(reader, remaining_bytes)?; 26 | 27 | match head.fourcc() { 28 | FREE => reader.skip(head.content_len() as i64)?, 29 | _ => { 30 | let atom = MetaItem::parse(reader, cfg, head)?; 31 | let other = ilst.iter_mut().find(|o| atom.ident == o.ident); 32 | 33 | match other { 34 | Some(other) => other.data.extend(atom.data), 35 | None => ilst.push(atom), 36 | } 37 | } 38 | } 39 | 40 | parsed_bytes += head.len(); 41 | } 42 | 43 | Ok(Self { 44 | state: State::Existing(bounds), 45 | data: Cow::Owned(ilst), 46 | }) 47 | } 48 | } 49 | 50 | impl AtomSize for Ilst<'_> { 51 | fn size(&self) -> Size { 52 | let content_len = self.data.iter().map(|a| a.len()).sum(); 53 | Size::from(content_len) 54 | } 55 | } 56 | 57 | impl WriteAtom for Ilst<'_> { 58 | fn write_atom(&self, writer: &mut impl Write, _changes: &[Change<'_>]) -> crate::Result<()> { 59 | self.write_head(writer)?; 60 | for a in self.data.iter() { 61 | a.write(writer)?; 62 | } 63 | Ok(()) 64 | } 65 | } 66 | 67 | // Not really a leaf atom, but it is treated like one. 68 | impl LeafAtomCollectChanges for Ilst<'_> { 69 | fn state(&self) -> &State { 70 | &self.state 71 | } 72 | 73 | fn atom_ref(&self) -> AtomRef<'_> { 74 | AtomRef::Ilst(self) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/atom/gmin.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 4 | pub struct Gmin { 5 | pub state: State, 6 | pub version: u8, 7 | pub flags: [u8; 3], 8 | pub graphics_mode: u16, 9 | pub op_color: [u16; 3], 10 | pub balance: u16, 11 | } 12 | 13 | impl Atom for Gmin { 14 | const FOURCC: Fourcc = BASE_MEDIA_INFORMATION; 15 | } 16 | 17 | impl ParseAtom for Gmin { 18 | fn parse_atom( 19 | reader: &mut (impl Read + Seek), 20 | _cfg: &ParseConfig<'_>, 21 | size: Size, 22 | ) -> crate::Result { 23 | let bounds = find_bounds(reader, size)?; 24 | let mut gmin = Self { 25 | state: State::Existing(bounds), 26 | ..Default::default() 27 | }; 28 | 29 | let (version, flags) = head::parse_full(reader)?; 30 | gmin.version = version; 31 | gmin.flags = flags; 32 | if version != 0 { 33 | return unknown_version("base media information (gmin)", version); 34 | } 35 | 36 | gmin.graphics_mode = reader.read_be_u16()?; 37 | for c in gmin.op_color.iter_mut() { 38 | *c = reader.read_be_u16()?; 39 | } 40 | gmin.balance = reader.read_be_u16()?; 41 | reader.skip(2)?; // reserved 42 | 43 | Ok(gmin) 44 | } 45 | } 46 | 47 | impl AtomSize for Gmin { 48 | fn size(&self) -> Size { 49 | Size::from(16) 50 | } 51 | } 52 | 53 | impl WriteAtom for Gmin { 54 | fn write_atom(&self, writer: &mut impl Write, _changes: &[Change<'_>]) -> crate::Result<()> { 55 | self.write_head(writer)?; 56 | head::write_full(writer, self.version, self.flags)?; 57 | 58 | writer.write_be_u16(self.graphics_mode)?; 59 | for c in self.op_color { 60 | writer.write_be_u16(c)?; 61 | } 62 | writer.write_be_u16(self.balance)?; 63 | writer.write_be_u16(0)?; // reserved 64 | 65 | Ok(()) 66 | } 67 | } 68 | 69 | impl LeafAtomCollectChanges for Gmin { 70 | fn state(&self) -> &State { 71 | &self.state 72 | } 73 | 74 | fn atom_ref(&self) -> AtomRef<'_> { 75 | AtomRef::Gmin(self) 76 | } 77 | } 78 | 79 | impl Gmin { 80 | pub fn chapter() -> Self { 81 | Self { 82 | state: State::Insert, 83 | version: 0, 84 | flags: [0; 3], 85 | graphics_mode: 0x0040, 86 | op_color: [0x8000; 3], 87 | balance: 0, 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/atom/gmhd.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 4 | pub struct Gmhd { 5 | pub state: State, 6 | pub gmin: Option, 7 | pub text: Option, 8 | } 9 | 10 | impl Atom for Gmhd { 11 | const FOURCC: Fourcc = BASE_MEDIA_INFORMATION_HEADER; 12 | } 13 | 14 | impl ParseAtom for Gmhd { 15 | fn parse_atom( 16 | reader: &mut (impl Read + Seek), 17 | cfg: &ParseConfig<'_>, 18 | size: Size, 19 | ) -> crate::Result { 20 | let bounds = find_bounds(reader, size)?; 21 | let mut gmhd = Self { 22 | state: State::Existing(bounds), 23 | ..Default::default() 24 | }; 25 | let mut parsed_bytes = 0; 26 | 27 | while parsed_bytes < size.content_len() { 28 | let remaining_bytes = size.content_len() - parsed_bytes; 29 | let head = head::parse(reader, remaining_bytes)?; 30 | 31 | match head.fourcc() { 32 | BASE_MEDIA_INFORMATION => gmhd.gmin = Some(Gmin::parse(reader, cfg, head.size())?), 33 | TEXT_MEDIA => gmhd.text = Some(Text::parse(reader, cfg, head.size())?), 34 | _ => reader.skip(head.content_len() as i64)?, 35 | } 36 | 37 | parsed_bytes += head.len(); 38 | } 39 | 40 | Ok(gmhd) 41 | } 42 | } 43 | 44 | impl AtomSize for Gmhd { 45 | fn size(&self) -> Size { 46 | let content_len = self.gmin.len_or_zero() + self.text.len_or_zero(); 47 | Size::from(content_len) 48 | } 49 | } 50 | 51 | impl WriteAtom for Gmhd { 52 | fn write_atom(&self, writer: &mut impl Write, changes: &[Change<'_>]) -> crate::Result<()> { 53 | self.write_head(writer)?; 54 | 55 | if let Some(a) = &self.gmin { 56 | a.write(writer, changes)?; 57 | } 58 | if let Some(a) = &self.text { 59 | a.write(writer, changes)?; 60 | } 61 | Ok(()) 62 | } 63 | } 64 | 65 | impl SimpleCollectChanges for Gmhd { 66 | fn state(&self) -> &State { 67 | &self.state 68 | } 69 | 70 | fn existing<'a>( 71 | &'a self, 72 | level: u8, 73 | bounds: &'a AtomBounds, 74 | changes: &mut Vec>, 75 | ) -> i64 { 76 | self.gmin.collect_changes(bounds.end(), level, changes) 77 | + self.text.collect_changes(bounds.end(), level, changes) 78 | } 79 | 80 | fn atom_ref(&self) -> AtomRef<'_> { 81 | AtomRef::Gmhd(self) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/atom/stco.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub const HEADER_SIZE: u64 = 8; 4 | pub const ENTRY_SIZE: u64 = 4; 5 | 6 | /// A struct representing of a sample table chunk offset atom (`stco`). 7 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 8 | pub struct Stco { 9 | pub state: State, 10 | pub offsets: Table, 11 | } 12 | 13 | impl Atom for Stco { 14 | const FOURCC: Fourcc = SAMPLE_TABLE_CHUNK_OFFSET; 15 | } 16 | 17 | impl ParseAtom for Stco { 18 | fn parse_atom( 19 | reader: &mut (impl Read + Seek), 20 | cfg: &ParseConfig<'_>, 21 | size: Size, 22 | ) -> crate::Result { 23 | let bounds = find_bounds(reader, size)?; 24 | let (version, _) = head::parse_full(reader)?; 25 | 26 | if version != 0 { 27 | return unknown_version("sample table chunk offset (stco)", version); 28 | } 29 | 30 | let num_entries = reader.read_be_u32()?; 31 | let table_size = ENTRY_SIZE * num_entries as u64; 32 | expect_size("Sample table chunk offset (stco)", size, HEADER_SIZE + table_size)?; 33 | 34 | let offsets = if cfg.write { 35 | let offsets = Table::read_items(reader, num_entries)?; 36 | Table::Full(offsets) 37 | } else { 38 | reader.skip(table_size as i64)?; 39 | Table::Shallow { 40 | pos: bounds.content_pos() + HEADER_SIZE, 41 | num_entries, 42 | } 43 | }; 44 | 45 | Ok(Self { state: State::Existing(bounds), offsets }) 46 | } 47 | } 48 | 49 | impl AtomSize for Stco { 50 | fn size(&self) -> Size { 51 | let content_len = HEADER_SIZE + ENTRY_SIZE * self.offsets.len() as u64; 52 | Size::from(content_len) 53 | } 54 | } 55 | 56 | impl WriteAtom for Stco { 57 | fn write_atom(&self, writer: &mut impl Write, changes: &[Change<'_>]) -> crate::Result<()> { 58 | self.write_head(writer)?; 59 | head::write_full(writer, 0, [0; 3])?; 60 | 61 | writer.write_be_u32(self.offsets.len() as u32)?; 62 | match &self.offsets { 63 | Table::Shallow { .. } => unreachable!(), 64 | Table::Full(offsets) => { 65 | change::write_shifted_offsets(writer, offsets, changes)?; 66 | } 67 | } 68 | 69 | Ok(()) 70 | } 71 | } 72 | 73 | impl LeafAtomCollectChanges for Stco { 74 | fn state(&self) -> &State { 75 | &self.state 76 | } 77 | 78 | fn atom_ref(&self) -> AtomRef<'_> { 79 | AtomRef::Stco(self) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/atom/stts.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub const HEADER_SIZE: u64 = 8; 4 | pub const ENTRY_SIZE: u64 = 8; 5 | 6 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 7 | pub struct Stts { 8 | pub state: State, 9 | pub items: Table, 10 | } 11 | 12 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 13 | pub struct SttsItem { 14 | pub sample_count: u32, 15 | pub sample_duration: u32, 16 | } 17 | 18 | impl Atom for Stts { 19 | const FOURCC: Fourcc = SAMPLE_TABLE_TIME_TO_SAMPLE; 20 | } 21 | 22 | impl ParseAtom for Stts { 23 | fn parse_atom( 24 | reader: &mut (impl Read + Seek), 25 | _cfg: &ParseConfig<'_>, 26 | size: Size, 27 | ) -> crate::Result { 28 | let bounds = find_bounds(reader, size)?; 29 | let (version, _) = head::parse_full(reader)?; 30 | 31 | if version != 0 { 32 | return unknown_version("sample table time to sample (stts)", version); 33 | } 34 | 35 | let num_entries = reader.read_be_u32()?; 36 | let table_size = ENTRY_SIZE * num_entries as u64; 37 | expect_size("Sample table time to sample (stts)", size, HEADER_SIZE + table_size)?; 38 | 39 | reader.skip(table_size as i64)?; 40 | let items = Table::Shallow { 41 | pos: bounds.content_pos() + HEADER_SIZE, 42 | num_entries, 43 | }; 44 | 45 | Ok(Self { state: State::Existing(bounds), items }) 46 | } 47 | } 48 | 49 | impl AtomSize for Stts { 50 | fn size(&self) -> Size { 51 | let content_len = HEADER_SIZE + ENTRY_SIZE * self.items.len() as u64; 52 | Size::from(content_len) 53 | } 54 | } 55 | 56 | impl WriteAtom for Stts { 57 | fn write_atom(&self, writer: &mut impl Write, _changes: &[Change<'_>]) -> crate::Result<()> { 58 | self.write_head(writer)?; 59 | head::write_full(writer, 0, [0; 3])?; 60 | 61 | writer.write_be_u32(self.items.len() as u32)?; 62 | match &self.items { 63 | Table::Shallow { .. } => unreachable!(), 64 | Table::Full(items) => { 65 | for i in items.iter() { 66 | writer.write_be_u32(i.sample_count)?; 67 | writer.write_be_u32(i.sample_duration)?; 68 | } 69 | } 70 | } 71 | 72 | Ok(()) 73 | } 74 | } 75 | 76 | impl LeafAtomCollectChanges for Stts { 77 | fn state(&self) -> &State { 78 | &self.state 79 | } 80 | 81 | fn atom_ref(&self) -> AtomRef<'_> { 82 | AtomRef::Stts(self) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/atom/co64.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub const HEADER_SIZE: u64 = 8; 4 | pub const ENTRY_SIZE: u64 = 8; 5 | 6 | /// A struct representing of a 64bit sample table chunk offset atom (`co64`). 7 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 8 | pub struct Co64 { 9 | pub state: State, 10 | pub offsets: Table, 11 | } 12 | 13 | impl Atom for Co64 { 14 | const FOURCC: Fourcc = SAMPLE_TABLE_CHUNK_OFFSET_64; 15 | } 16 | 17 | impl ParseAtom for Co64 { 18 | fn parse_atom( 19 | reader: &mut (impl Read + Seek), 20 | cfg: &ParseConfig<'_>, 21 | size: Size, 22 | ) -> crate::Result { 23 | let bounds = find_bounds(reader, size)?; 24 | let (version, _) = head::parse_full(reader)?; 25 | 26 | if version != 0 { 27 | return unknown_version("64-bit sample table chunk offset (co64)", version); 28 | } 29 | 30 | let num_entries = reader.read_be_u32()?; 31 | let table_size = ENTRY_SIZE * num_entries as u64; 32 | expect_size("64-bit sample table chunk offset (co64)", size, HEADER_SIZE + table_size)?; 33 | 34 | let offsets = if cfg.write { 35 | let offsets = Table::read_items(reader, num_entries)?; 36 | Table::Full(offsets) 37 | } else { 38 | reader.skip(table_size as i64)?; 39 | Table::Shallow { 40 | pos: bounds.content_pos() + HEADER_SIZE, 41 | num_entries, 42 | } 43 | }; 44 | 45 | Ok(Self { state: State::Existing(bounds), offsets }) 46 | } 47 | } 48 | 49 | impl AtomSize for Co64 { 50 | fn size(&self) -> Size { 51 | let content_len = HEADER_SIZE + ENTRY_SIZE * self.offsets.len() as u64; 52 | Size::from(content_len) 53 | } 54 | } 55 | 56 | impl WriteAtom for Co64 { 57 | fn write_atom(&self, writer: &mut impl Write, changes: &[Change<'_>]) -> crate::Result<()> { 58 | self.write_head(writer)?; 59 | head::write_full(writer, 0, [0; 3])?; 60 | 61 | writer.write_be_u32(self.offsets.len() as u32)?; 62 | match &self.offsets { 63 | Table::Shallow { .. } => unreachable!(), 64 | Table::Full(offsets) => { 65 | change::write_shifted_offsets(writer, offsets, changes)?; 66 | } 67 | } 68 | 69 | Ok(()) 70 | } 71 | } 72 | 73 | impl LeafAtomCollectChanges for Co64 { 74 | fn state(&self) -> &State { 75 | &self.state 76 | } 77 | 78 | fn atom_ref(&self) -> AtomRef<'_> { 79 | AtomRef::Co64(self) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/atom/hdlr.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 4 | pub struct Hdlr { 5 | pub state: State, 6 | pub data: Cow<'static, [u8]>, 7 | } 8 | 9 | impl Atom for Hdlr { 10 | const FOURCC: Fourcc = HANDLER_REFERENCE; 11 | } 12 | 13 | impl ParseAtom for Hdlr { 14 | fn parse_atom( 15 | reader: &mut (impl Read + Seek), 16 | _cfg: &ParseConfig<'_>, 17 | size: Size, 18 | ) -> crate::Result { 19 | let bounds = find_bounds(reader, size)?; 20 | let data = reader.read_u8_vec(size.content_len())?; 21 | Ok(Self { 22 | state: State::Existing(bounds), 23 | data: Cow::Owned(data), 24 | }) 25 | } 26 | } 27 | 28 | impl AtomSize for Hdlr { 29 | fn size(&self) -> Size { 30 | Size::from(self.data.len() as u64) 31 | } 32 | } 33 | 34 | impl WriteAtom for Hdlr { 35 | fn write_atom(&self, writer: &mut impl Write, _changes: &[Change<'_>]) -> crate::Result<()> { 36 | self.write_head(writer)?; 37 | writer.write_all(&self.data)?; 38 | Ok(()) 39 | } 40 | } 41 | 42 | impl LeafAtomCollectChanges for Hdlr { 43 | fn state(&self) -> &State { 44 | &self.state 45 | } 46 | 47 | fn atom_ref(&self) -> AtomRef<'_> { 48 | AtomRef::Hdlr(self) 49 | } 50 | } 51 | 52 | impl Hdlr { 53 | pub fn meta() -> Self { 54 | Self { 55 | state: State::Insert, 56 | data: Cow::Borrowed(&[ 57 | 0x00, 0x00, 0x00, 0x00, // version + flags 58 | 0x00, 0x00, 0x00, 0x00, // component type 59 | 0x6d, 0x64, 0x69, 0x72, // component subtype 60 | 0x61, 0x70, 0x70, 0x6c, // component manufacturer 61 | 0x00, 0x00, 0x00, 0x00, // component flags 62 | 0x00, 0x00, 0x00, 0x00, // component flags mask 63 | 0x00, // component name 64 | ]), 65 | } 66 | } 67 | 68 | pub fn text_mdia() -> Self { 69 | Self { 70 | state: State::Insert, 71 | data: Cow::Borrowed(&[ 72 | 0x00, 0x00, 0x00, 0x00, // version + flags 73 | 0x00, 0x00, 0x00, 0x00, // component type 74 | 0x74, 0x65, 0x78, 0x74, // component subtype 75 | 0x00, 0x00, 0x00, 0x00, // component manufacturer 76 | 0x00, 0x00, 0x00, 0x00, // component flags 77 | 0x00, 0x00, 0x00, 0x00, // component flags mask 78 | 0x00, // component name 79 | ]), 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/tag/readonly.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::time::Duration; 3 | 4 | use crate::{AudioInfo, ChannelConfig, SampleRate, Tag, util}; 5 | 6 | /// ### Audio information 7 | impl Tag { 8 | /// Returns a reference of the audio information. 9 | pub fn audio_info(&self) -> &AudioInfo { 10 | &self.info 11 | } 12 | 13 | /// Returns the duration in seconds. 14 | pub fn duration(&self) -> Duration { 15 | self.info.duration 16 | } 17 | 18 | /// Returns the duration formatted in an easily readable way. 19 | pub(crate) fn format_duration(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 20 | write!(f, "duration: ")?; 21 | util::format_duration(f, self.duration())?; 22 | writeln!(f) 23 | } 24 | 25 | /// Returns the channel configuration. 26 | pub fn channel_config(&self) -> Option { 27 | self.info.channel_config 28 | } 29 | 30 | pub(crate) fn format_channel_config(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 31 | match self.channel_config() { 32 | Some(c) => writeln!(f, "channel config: {c}"), 33 | None => Ok(()), 34 | } 35 | } 36 | 37 | /// Returns the channel configuration. 38 | pub fn sample_rate(&self) -> Option { 39 | self.info.sample_rate 40 | } 41 | 42 | pub(crate) fn format_sample_rate(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 43 | match self.sample_rate() { 44 | Some(r) => writeln!(f, "sample rate: {r}"), 45 | None => Ok(()), 46 | } 47 | } 48 | 49 | /// Returns the average bitrate. 50 | pub fn avg_bitrate(&self) -> Option { 51 | self.info.avg_bitrate 52 | } 53 | 54 | pub(crate) fn format_avg_bitrate(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 55 | match self.avg_bitrate() { 56 | Some(c) => writeln!(f, "average bitrate: {}kbps", c / 1024), 57 | None => Ok(()), 58 | } 59 | } 60 | 61 | /// Returns the maximum bitrate. 62 | pub fn max_bitrate(&self) -> Option { 63 | self.info.max_bitrate 64 | } 65 | 66 | pub(crate) fn format_max_bitrate(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 67 | match self.max_bitrate() { 68 | Some(c) => writeln!(f, "maximum bitrate: {}kbps", c / 1024), 69 | None => Ok(()), 70 | } 71 | } 72 | } 73 | 74 | /// ### Filetype 75 | impl Tag { 76 | /// returns the filetype (`ftyp`). 77 | pub fn filetype(&self) -> &str { 78 | self.ftyp.as_str() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/atom/udta.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 4 | pub struct Udta<'a> { 5 | pub state: State, 6 | pub chpl: Option>, 7 | pub meta: Option>, 8 | } 9 | 10 | impl Atom for Udta<'_> { 11 | const FOURCC: Fourcc = USER_DATA; 12 | } 13 | 14 | impl ParseAtom for Udta<'_> { 15 | fn parse_atom( 16 | reader: &mut (impl Read + Seek), 17 | cfg: &ParseConfig<'_>, 18 | size: Size, 19 | ) -> crate::Result { 20 | let bounds = find_bounds(reader, size)?; 21 | let mut udta = Self { 22 | state: State::Existing(bounds), 23 | ..Default::default() 24 | }; 25 | let mut parsed_bytes = 0; 26 | 27 | while parsed_bytes < size.content_len() { 28 | let remaining_bytes = size.content_len() - parsed_bytes; 29 | let head = head::parse(reader, remaining_bytes)?; 30 | 31 | match head.fourcc() { 32 | CHAPTER_LIST if cfg.cfg.read_chapter_list => { 33 | udta.chpl = Some(Chpl::parse(reader, cfg, head.size())?); 34 | } 35 | METADATA if cfg.cfg.read_meta_items => { 36 | udta.meta = Some(Meta::parse(reader, cfg, head.size())?) 37 | } 38 | _ => reader.skip(head.content_len() as i64)?, 39 | } 40 | 41 | parsed_bytes += head.len(); 42 | } 43 | 44 | Ok(udta) 45 | } 46 | } 47 | 48 | impl AtomSize for Udta<'_> { 49 | fn size(&self) -> Size { 50 | let content_len = self.meta.len_or_zero() + self.chpl.len_or_zero(); 51 | Size::from(content_len) 52 | } 53 | } 54 | 55 | impl WriteAtom for Udta<'_> { 56 | fn write_atom(&self, writer: &mut impl Write, changes: &[Change<'_>]) -> crate::Result<()> { 57 | self.write_head(writer)?; 58 | if let Some(a) = &self.chpl { 59 | a.write(writer, changes)?; 60 | } 61 | if let Some(a) = &self.meta { 62 | a.write(writer, changes)?; 63 | } 64 | Ok(()) 65 | } 66 | } 67 | 68 | impl SimpleCollectChanges for Udta<'_> { 69 | fn state(&self) -> &State { 70 | &self.state 71 | } 72 | 73 | fn existing<'a>( 74 | &'a self, 75 | level: u8, 76 | bounds: &AtomBounds, 77 | changes: &mut Vec>, 78 | ) -> i64 { 79 | self.chpl.collect_changes(bounds.end(), level, changes) 80 | + self.meta.collect_changes(bounds.end(), level, changes) 81 | } 82 | 83 | fn atom_ref(&self) -> AtomRef<'_> { 84 | AtomRef::Udta(self) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/atom/stsc.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub const HEADER_SIZE: u64 = 8; 4 | pub const ENTRY_SIZE: u64 = 12; 5 | 6 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 7 | pub struct Stsc { 8 | pub state: State, 9 | pub items: Table, 10 | } 11 | 12 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 13 | pub struct StscItem { 14 | pub first_chunk: u32, 15 | pub samples_per_chunk: u32, 16 | pub sample_description_id: u32, 17 | } 18 | 19 | impl Atom for Stsc { 20 | const FOURCC: Fourcc = SAMPLE_TABLE_SAMPLE_TO_CHUNK; 21 | } 22 | 23 | impl ParseAtom for Stsc { 24 | fn parse_atom( 25 | reader: &mut (impl Read + Seek), 26 | _cfg: &ParseConfig<'_>, 27 | size: Size, 28 | ) -> crate::Result { 29 | let bounds = find_bounds(reader, size)?; 30 | let (version, _) = head::parse_full(reader)?; 31 | 32 | if version != 0 { 33 | return unknown_version("sample table sample size (stsz)", version); 34 | } 35 | 36 | let num_entries = reader.read_be_u32()?; 37 | let table_size = ENTRY_SIZE * num_entries as u64; 38 | expect_size("Sample table sample to chunk (stsc)", size, HEADER_SIZE + table_size)?; 39 | 40 | reader.skip(table_size as i64)?; 41 | let items = Table::Shallow { 42 | pos: bounds.content_pos() + HEADER_SIZE, 43 | num_entries, 44 | }; 45 | 46 | Ok(Self { state: State::Existing(bounds), items }) 47 | } 48 | } 49 | 50 | impl AtomSize for Stsc { 51 | fn size(&self) -> Size { 52 | let content_len = HEADER_SIZE + ENTRY_SIZE * self.items.len() as u64; 53 | Size::from(content_len) 54 | } 55 | } 56 | 57 | impl WriteAtom for Stsc { 58 | fn write_atom(&self, writer: &mut impl Write, _changes: &[Change<'_>]) -> crate::Result<()> { 59 | self.write_head(writer)?; 60 | head::write_full(writer, 0, [0; 3])?; 61 | 62 | writer.write_be_u32(self.items.len() as u32)?; 63 | match &self.items { 64 | Table::Shallow { .. } => unreachable!(), 65 | Table::Full(items) => { 66 | for i in items.iter() { 67 | writer.write_be_u32(i.first_chunk)?; 68 | writer.write_be_u32(i.samples_per_chunk)?; 69 | writer.write_be_u32(i.sample_description_id)?; 70 | } 71 | } 72 | } 73 | 74 | Ok(()) 75 | } 76 | } 77 | 78 | impl LeafAtomCollectChanges for Stsc { 79 | fn state(&self) -> &State { 80 | &self.state 81 | } 82 | 83 | fn atom_ref(&self) -> AtomRef<'_> { 84 | AtomRef::Stsc(self) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/atom/stsd.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub const HEADER_SIZE: u64 = 8; 4 | 5 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 6 | pub struct Stsd { 7 | pub state: State, 8 | pub mp4a: Option, 9 | pub text: Option, 10 | } 11 | 12 | impl Atom for Stsd { 13 | const FOURCC: Fourcc = SAMPLE_TABLE_SAMPLE_DESCRIPTION; 14 | } 15 | 16 | impl ParseAtom for Stsd { 17 | fn parse_atom( 18 | reader: &mut (impl Read + Seek), 19 | cfg: &ParseConfig<'_>, 20 | size: Size, 21 | ) -> crate::Result { 22 | let bounds = find_bounds(reader, size)?; 23 | let (version, _) = head::parse_full(reader)?; 24 | 25 | if version != 0 { 26 | return unknown_version("sample table sample description (stsd)", version); 27 | } 28 | reader.skip(4)?; // number of entries 29 | 30 | expect_min_size("Sample table sampel description (stsd)", size, HEADER_SIZE)?; 31 | 32 | let mut stsd = Self { 33 | state: State::Existing(bounds), 34 | ..Default::default() 35 | }; 36 | let mut parsed_bytes = HEADER_SIZE; 37 | 38 | while parsed_bytes < size.content_len() { 39 | let remaining_bytes = size.content_len() - parsed_bytes; 40 | let head = head::parse(reader, remaining_bytes)?; 41 | 42 | match head.fourcc() { 43 | MP4_AUDIO if !cfg.write => stsd.mp4a = Some(Mp4a::parse(reader, cfg, head.size())?), 44 | TEXT_MEDIA if cfg.write => stsd.text = Some(Text::parse(reader, cfg, head.size())?), 45 | _ => reader.skip(head.content_len() as i64)?, 46 | } 47 | 48 | parsed_bytes += head.len(); 49 | } 50 | 51 | Ok(stsd) 52 | } 53 | } 54 | 55 | impl AtomSize for Stsd { 56 | fn size(&self) -> Size { 57 | let content_len = HEADER_SIZE + self.text.len_or_zero(); 58 | Size::from(content_len) 59 | } 60 | } 61 | 62 | impl WriteAtom for Stsd { 63 | fn write_atom(&self, writer: &mut impl Write, changes: &[Change<'_>]) -> crate::Result<()> { 64 | self.write_head(writer)?; 65 | head::write_full(writer, 0, [0; 3])?; 66 | 67 | if self.text.is_some() { 68 | writer.write_be_u32(1)?; 69 | } else { 70 | writer.write_be_u32(0)?; 71 | } 72 | 73 | if let Some(a) = &self.text { 74 | a.write(writer, changes)?; 75 | } 76 | Ok(()) 77 | } 78 | } 79 | 80 | impl LeafAtomCollectChanges for Stsd { 81 | fn state(&self) -> &State { 82 | &self.state 83 | } 84 | 85 | fn atom_ref(&self) -> AtomRef<'_> { 86 | AtomRef::Stsd(self) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/atom/dref.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub const HEADER_SIZE: u64 = 8; 4 | 5 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 6 | pub struct Dref { 7 | pub state: State, 8 | pub url: Option, 9 | } 10 | 11 | impl Atom for Dref { 12 | const FOURCC: Fourcc = DATA_REFERENCE; 13 | } 14 | 15 | impl ParseAtom for Dref { 16 | fn parse_atom( 17 | reader: &mut (impl Read + Seek), 18 | cfg: &ParseConfig<'_>, 19 | size: Size, 20 | ) -> crate::Result { 21 | let bounds = find_bounds(reader, size)?; 22 | let (version, _) = head::parse_full(reader)?; 23 | 24 | if version != 0 { 25 | return unknown_version("data reference (dref)", version); 26 | } 27 | reader.skip(4)?; // number of entries 28 | 29 | expect_min_size("Data ", size, HEADER_SIZE)?; 30 | 31 | let mut dref = Self { 32 | state: State::Existing(bounds), 33 | ..Default::default() 34 | }; 35 | let mut parsed_bytes = HEADER_SIZE; 36 | while parsed_bytes < size.content_len() { 37 | let remaining_bytes = size.content_len() - parsed_bytes; 38 | let head = head::parse(reader, remaining_bytes)?; 39 | 40 | match head.fourcc() { 41 | URL_MEDIA => dref.url = Some(Url::parse(reader, cfg, head.size())?), 42 | _ => reader.skip(head.content_len() as i64)?, 43 | } 44 | 45 | parsed_bytes += head.len(); 46 | } 47 | 48 | Ok(dref) 49 | } 50 | } 51 | 52 | impl AtomSize for Dref { 53 | fn size(&self) -> Size { 54 | let content_len = 8 + self.url.len_or_zero(); 55 | Size::from(content_len) 56 | } 57 | } 58 | 59 | impl WriteAtom for Dref { 60 | fn write_atom(&self, writer: &mut impl Write, changes: &[Change<'_>]) -> crate::Result<()> { 61 | self.write_head(writer)?; 62 | head::write_full(writer, 0, [0; 3])?; 63 | 64 | if self.url.is_some() { 65 | writer.write_be_u32(1)?; 66 | } else { 67 | writer.write_be_u32(0)?; 68 | } 69 | 70 | if let Some(a) = &self.url { 71 | a.write(writer, changes)?; 72 | } 73 | Ok(()) 74 | } 75 | } 76 | 77 | impl SimpleCollectChanges for Dref { 78 | fn state(&self) -> &State { 79 | &self.state 80 | } 81 | 82 | fn existing<'a>( 83 | &'a self, 84 | level: u8, 85 | bounds: &'a AtomBounds, 86 | changes: &mut Vec>, 87 | ) -> i64 { 88 | self.url.collect_changes(bounds.end(), level, changes) 89 | } 90 | 91 | fn atom_ref(&self) -> AtomRef<'_> { 92 | AtomRef::Dref(self) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/atom/moov.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 4 | pub struct Moov<'a> { 5 | pub state: State, 6 | pub mvhd: Mvhd, 7 | pub trak: Vec, 8 | pub udta: Option>, 9 | } 10 | 11 | impl Atom for Moov<'_> { 12 | const FOURCC: Fourcc = MOVIE; 13 | } 14 | 15 | impl ParseAtom for Moov<'_> { 16 | fn parse_atom( 17 | reader: &mut (impl Read + Seek), 18 | cfg: &ParseConfig<'_>, 19 | size: Size, 20 | ) -> crate::Result { 21 | let bounds = find_bounds(reader, size)?; 22 | let mut parsed_bytes = 0; 23 | let mut mvhd = None; 24 | let mut trak = Vec::new(); 25 | let mut udta = None; 26 | 27 | while parsed_bytes < size.content_len() { 28 | let remaining_bytes = size.content_len() - parsed_bytes; 29 | let head = head::parse(reader, remaining_bytes)?; 30 | 31 | match head.fourcc() { 32 | MOVIE_HEADER => mvhd = Some(Mvhd::parse(reader, cfg, head.size())?), 33 | TRACK if cfg.write || cfg.cfg.read_chapter_track || cfg.cfg.read_audio_info => { 34 | trak.push(Trak::parse(reader, cfg, head.size())?) 35 | } 36 | USER_DATA if cfg.cfg.read_meta_items || cfg.cfg.read_chapter_list => { 37 | udta = Some(Udta::parse(reader, cfg, head.size())?) 38 | } 39 | _ => reader.skip(head.content_len() as i64)?, 40 | } 41 | 42 | parsed_bytes += head.len(); 43 | } 44 | 45 | let mvhd = mvhd.ok_or_else(|| { 46 | crate::Error::new( 47 | ErrorKind::AtomNotFound(MOVIE_HEADER), 48 | "Missing necessary data, no movie header (mvhd) atom found", 49 | ) 50 | })?; 51 | 52 | let moov = Self { state: State::Existing(bounds), mvhd, trak, udta }; 53 | 54 | Ok(moov) 55 | } 56 | } 57 | 58 | impl AtomSize for Moov<'_> { 59 | fn size(&self) -> Size { 60 | let content_len = self.mvhd.len() 61 | + self.trak.iter().map(Trak::len).sum::() 62 | + self.udta.len_or_zero(); 63 | Size::from(content_len) 64 | } 65 | } 66 | 67 | impl SimpleCollectChanges for Moov<'_> { 68 | fn state(&self) -> &State { 69 | &self.state 70 | } 71 | 72 | fn existing<'a>( 73 | &'a self, 74 | level: u8, 75 | bounds: &'a AtomBounds, 76 | changes: &mut Vec>, 77 | ) -> i64 { 78 | self.trak.iter().map(|a| a.collect_changes(bounds.end(), level, changes)).sum::() 79 | + self.udta.collect_changes(bounds.end(), level, changes) 80 | } 81 | 82 | fn atom_ref(&self) -> AtomRef<'_> { 83 | AtomRef::Moov(self) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/atom/stsz.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub const HEADER_SIZE: u64 = 12; 4 | pub const ENTRY_SIZE: u64 = 4; 5 | 6 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 7 | pub struct Stsz { 8 | pub state: State, 9 | /// If this field is set to zero, a list of sizes is read instead. 10 | pub uniform_sample_size: u32, 11 | pub sizes: Table, 12 | } 13 | 14 | impl Atom for Stsz { 15 | const FOURCC: Fourcc = SAMPLE_TABLE_SAMPLE_SIZE; 16 | } 17 | 18 | impl ParseAtom for Stsz { 19 | fn parse_atom( 20 | reader: &mut (impl Read + Seek), 21 | _cfg: &ParseConfig<'_>, 22 | size: Size, 23 | ) -> crate::Result { 24 | let bounds = find_bounds(reader, size)?; 25 | let (version, _) = head::parse_full(reader)?; 26 | 27 | if version != 0 { 28 | return unknown_version("sample table sample size (stsz)", version); 29 | } 30 | 31 | let uniform_sample_size = reader.read_be_u32()?; 32 | 33 | let num_entries = reader.read_be_u32()?; 34 | let sizes = if uniform_sample_size == 0 { 35 | let table_size = ENTRY_SIZE * num_entries as u64; 36 | expect_size("Sample table sample size (stsz)", size, HEADER_SIZE + table_size)?; 37 | 38 | reader.skip(table_size as i64)?; 39 | Table::Shallow { 40 | pos: bounds.content_pos() + HEADER_SIZE, 41 | num_entries, 42 | } 43 | } else { 44 | expect_size("Sample table sample size (stsz)", size, HEADER_SIZE)?; 45 | Table::Full(Vec::new()) 46 | }; 47 | 48 | Ok(Self { 49 | state: State::Existing(bounds), 50 | uniform_sample_size, 51 | sizes, 52 | }) 53 | } 54 | } 55 | 56 | impl AtomSize for Stsz { 57 | fn size(&self) -> Size { 58 | let content_len = HEADER_SIZE + ENTRY_SIZE * self.sizes.len() as u64; 59 | Size::from(content_len) 60 | } 61 | } 62 | 63 | impl WriteAtom for Stsz { 64 | fn write_atom(&self, writer: &mut impl Write, _changes: &[Change<'_>]) -> crate::Result<()> { 65 | self.write_head(writer)?; 66 | head::write_full(writer, 0, [0; 3])?; 67 | 68 | writer.write_be_u32(self.uniform_sample_size)?; 69 | writer.write_be_u32(self.sizes.len() as u32)?; 70 | 71 | match &self.sizes { 72 | Table::Shallow { .. } => unreachable!(), 73 | Table::Full(sizes) => { 74 | for s in sizes.iter() { 75 | writer.write_be_u32(*s)?; 76 | } 77 | } 78 | } 79 | 80 | Ok(()) 81 | } 82 | } 83 | 84 | impl LeafAtomCollectChanges for Stsz { 85 | fn state(&self) -> &State { 86 | &self.state 87 | } 88 | 89 | fn atom_ref(&self) -> AtomRef<'_> { 90 | AtomRef::Stsz(self) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/atom/meta.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub const HEADER_SIZE: u64 = 4; 4 | 5 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 6 | pub struct Meta<'a> { 7 | pub state: State, 8 | pub hdlr: Option, 9 | pub ilst: Option>, 10 | } 11 | 12 | impl Atom for Meta<'_> { 13 | const FOURCC: Fourcc = METADATA; 14 | } 15 | 16 | impl ParseAtom for Meta<'_> { 17 | fn parse_atom( 18 | reader: &'_ mut (impl Read + Seek), 19 | cfg: &ParseConfig<'_>, 20 | size: Size, 21 | ) -> crate::Result { 22 | let bounds = find_bounds(reader, size)?; 23 | let (version, _) = head::parse_full(reader)?; 24 | 25 | if version != 0 { 26 | return unknown_version("metadata (meta)", version); 27 | } 28 | 29 | expect_min_size("Metadata (meta)", size, HEADER_SIZE)?; 30 | 31 | let mut meta = Self { 32 | state: State::Existing(bounds), 33 | ..Default::default() 34 | }; 35 | let mut parsed_bytes = HEADER_SIZE; 36 | 37 | while parsed_bytes < size.content_len() { 38 | let remaining_bytes = size.content_len() - parsed_bytes; 39 | let head = head::parse(reader, remaining_bytes)?; 40 | 41 | match head.fourcc() { 42 | HANDLER_REFERENCE if cfg.write => { 43 | meta.hdlr = Some(Hdlr::parse(reader, cfg, head.size())?) 44 | } 45 | ITEM_LIST => meta.ilst = Some(Ilst::parse(reader, cfg, head.size())?), 46 | _ => reader.skip(head.content_len() as i64)?, 47 | } 48 | 49 | parsed_bytes += head.len(); 50 | } 51 | 52 | Ok(meta) 53 | } 54 | } 55 | 56 | impl AtomSize for Meta<'_> { 57 | fn size(&self) -> Size { 58 | let content_len = HEADER_SIZE + self.hdlr.len_or_zero() + self.ilst.len_or_zero(); 59 | Size::from(content_len) 60 | } 61 | } 62 | 63 | impl WriteAtom for Meta<'_> { 64 | fn write_atom(&self, writer: &mut impl Write, changes: &[Change<'_>]) -> crate::Result<()> { 65 | self.write_head(writer)?; 66 | head::write_full(writer, 0, [0; 3])?; 67 | if let Some(a) = &self.hdlr { 68 | a.write(writer, changes)?; 69 | } 70 | if let Some(a) = &self.ilst { 71 | a.write(writer, changes)?; 72 | } 73 | Ok(()) 74 | } 75 | } 76 | 77 | impl SimpleCollectChanges for Meta<'_> { 78 | fn state(&self) -> &State { 79 | &self.state 80 | } 81 | 82 | fn existing<'a>( 83 | &'a self, 84 | level: u8, 85 | bounds: &AtomBounds, 86 | changes: &mut Vec>, 87 | ) -> i64 { 88 | self.hdlr.collect_changes(bounds.content_pos() + HEADER_SIZE, level, changes) 89 | + self.ilst.collect_changes(bounds.end(), level, changes) 90 | } 91 | 92 | fn atom_ref(&self) -> AtomRef<'_> { 93 | AtomRef::Meta(self) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/atom/minf.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 4 | pub struct Minf { 5 | pub state: State, 6 | pub gmhd: Option, 7 | pub dinf: Option, 8 | pub stbl: Option, 9 | } 10 | 11 | impl Atom for Minf { 12 | const FOURCC: Fourcc = MEDIA_INFORMATION; 13 | } 14 | 15 | impl ParseAtom for Minf { 16 | fn parse_atom( 17 | reader: &mut (impl Read + Seek), 18 | cfg: &ParseConfig<'_>, 19 | size: Size, 20 | ) -> crate::Result { 21 | let bounds = find_bounds(reader, size)?; 22 | let mut minf = Self { 23 | state: State::Existing(bounds), 24 | ..Default::default() 25 | }; 26 | let mut parsed_bytes = 0; 27 | 28 | while parsed_bytes < size.content_len() { 29 | let remaining_bytes = size.content_len() - parsed_bytes; 30 | let head = head::parse(reader, remaining_bytes)?; 31 | 32 | match head.fourcc() { 33 | BASE_MEDIA_INFORMATION_HEADER if cfg.write => { 34 | minf.gmhd = Some(Gmhd::parse(reader, cfg, head.size())?) 35 | } 36 | DATA_INFORMATION if cfg.write => { 37 | minf.dinf = Some(Dinf::parse(reader, cfg, head.size())?) 38 | } 39 | SAMPLE_TABLE => minf.stbl = Some(Stbl::parse(reader, cfg, head.size())?), 40 | _ => reader.skip(head.content_len() as i64)?, 41 | } 42 | 43 | parsed_bytes += head.len(); 44 | } 45 | 46 | Ok(minf) 47 | } 48 | } 49 | 50 | impl AtomSize for Minf { 51 | fn size(&self) -> Size { 52 | let content_len = 53 | self.gmhd.len_or_zero() + self.dinf.len_or_zero() + self.stbl.len_or_zero(); 54 | Size::from(content_len) 55 | } 56 | } 57 | 58 | impl WriteAtom for Minf { 59 | fn write_atom(&self, writer: &mut impl Write, changes: &[Change<'_>]) -> crate::Result<()> { 60 | self.write_head(writer)?; 61 | if let Some(a) = &self.gmhd { 62 | a.write(writer, changes)?; 63 | } 64 | if let Some(a) = &self.dinf { 65 | a.write(writer, changes)?; 66 | } 67 | if let Some(a) = &self.stbl { 68 | a.write(writer, changes)?; 69 | } 70 | Ok(()) 71 | } 72 | } 73 | 74 | impl SimpleCollectChanges for Minf { 75 | fn state(&self) -> &State { 76 | &self.state 77 | } 78 | 79 | fn existing<'a>( 80 | &'a self, 81 | level: u8, 82 | bounds: &'a AtomBounds, 83 | changes: &mut Vec>, 84 | ) -> i64 { 85 | self.gmhd.collect_changes(bounds.end(), level, changes) 86 | + self.dinf.collect_changes(bounds.end(), level, changes) 87 | + self.stbl.collect_changes(bounds.end(), level, changes) 88 | } 89 | 90 | fn atom_ref(&self) -> AtomRef<'_> { 91 | AtomRef::Minf(self) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/atom/mdia.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 4 | pub struct Mdia { 5 | pub state: State, 6 | pub mdhd: Mdhd, 7 | pub hdlr: Option, 8 | pub minf: Option, 9 | } 10 | 11 | impl Atom for Mdia { 12 | const FOURCC: Fourcc = MEDIA; 13 | } 14 | 15 | impl ParseAtom for Mdia { 16 | fn parse_atom( 17 | reader: &mut (impl Read + Seek), 18 | cfg: &ParseConfig<'_>, 19 | size: Size, 20 | ) -> crate::Result { 21 | let bounds = find_bounds(reader, size)?; 22 | let mut parsed_bytes = 0; 23 | let mut mdhd = None; 24 | let mut hdlr = None; 25 | let mut minf = None; 26 | 27 | while parsed_bytes < size.content_len() { 28 | let remaining_bytes = size.content_len() - parsed_bytes; 29 | let head = head::parse(reader, remaining_bytes)?; 30 | 31 | match head.fourcc() { 32 | MEDIA_HEADER => mdhd = Some(Mdhd::parse(reader, cfg, head.size())?), 33 | HANDLER_REFERENCE if cfg.write => { 34 | hdlr = Some(Hdlr::parse(reader, cfg, head.size())?) 35 | } 36 | MEDIA_INFORMATION => minf = Some(Minf::parse(reader, cfg, head.size())?), 37 | _ => reader.skip(head.content_len() as i64)?, 38 | } 39 | 40 | parsed_bytes += head.len(); 41 | } 42 | 43 | let mdhd = mdhd.ok_or_else(|| { 44 | crate::Error::new( 45 | ErrorKind::AtomNotFound(MEDIA_HEADER), 46 | "Missing necessary data, no media header (mdhd) atom found", 47 | ) 48 | })?; 49 | 50 | let mdia = Self { state: State::Existing(bounds), mdhd, hdlr, minf }; 51 | 52 | Ok(mdia) 53 | } 54 | } 55 | 56 | impl AtomSize for Mdia { 57 | fn size(&self) -> Size { 58 | let content_len = self.mdhd.len() + self.hdlr.len_or_zero() + self.minf.len_or_zero(); 59 | Size::from(content_len) 60 | } 61 | } 62 | 63 | impl WriteAtom for Mdia { 64 | fn write_atom(&self, writer: &mut impl Write, changes: &[Change<'_>]) -> crate::Result<()> { 65 | self.write_head(writer)?; 66 | self.mdhd.write(writer, changes)?; 67 | if let Some(a) = &self.hdlr { 68 | a.write(writer, changes)?; 69 | } 70 | if let Some(a) = &self.minf { 71 | a.write(writer, changes)?; 72 | } 73 | Ok(()) 74 | } 75 | } 76 | 77 | impl SimpleCollectChanges for Mdia { 78 | fn state(&self) -> &State { 79 | &self.state 80 | } 81 | 82 | fn existing<'a>( 83 | &'a self, 84 | level: u8, 85 | bounds: &'a AtomBounds, 86 | changes: &mut Vec>, 87 | ) -> i64 { 88 | self.hdlr.collect_changes(bounds.end(), level, changes) 89 | + self.minf.collect_changes(bounds.end(), level, changes) 90 | } 91 | 92 | fn atom_ref(&self) -> AtomRef<'_> { 93 | AtomRef::Mdia(self) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/atom/trak.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 4 | pub struct Trak { 5 | pub state: State, 6 | pub tkhd: Tkhd, 7 | pub tref: Option, 8 | pub mdia: Option, 9 | } 10 | 11 | impl Atom for Trak { 12 | const FOURCC: Fourcc = TRACK; 13 | } 14 | 15 | impl ParseAtom for Trak { 16 | fn parse_atom( 17 | reader: &mut (impl Read + Seek), 18 | cfg: &ParseConfig<'_>, 19 | size: Size, 20 | ) -> crate::Result { 21 | let bounds = find_bounds(reader, size)?; 22 | let mut parsed_bytes = 0; 23 | let mut tkhd = None; 24 | let mut tref = None; 25 | let mut mdia = None; 26 | 27 | while parsed_bytes < size.content_len() { 28 | let remaining_bytes = size.content_len() - parsed_bytes; 29 | let head = head::parse(reader, remaining_bytes)?; 30 | 31 | match head.fourcc() { 32 | TRACK_HEADER => tkhd = Some(Tkhd::parse(reader, cfg, head.size())?), 33 | TRACK_REFERENCE if cfg.cfg.read_chapter_track => { 34 | tref = Some(Tref::parse(reader, cfg, head.size())?) 35 | } 36 | MEDIA if cfg.write || cfg.cfg.read_chapter_track || cfg.cfg.read_audio_info => { 37 | mdia = Some(Mdia::parse(reader, cfg, head.size())?) 38 | } 39 | _ => reader.skip(head.content_len() as i64)?, 40 | } 41 | 42 | parsed_bytes += head.len(); 43 | } 44 | 45 | let tkhd = tkhd.ok_or_else(|| { 46 | crate::Error::new( 47 | crate::ErrorKind::AtomNotFound(TRACK_HEADER), 48 | "Missing necessary data, no track header (tkhd) atom found", 49 | ) 50 | })?; 51 | 52 | let trak = Self { state: State::Existing(bounds), tkhd, tref, mdia }; 53 | 54 | Ok(trak) 55 | } 56 | } 57 | 58 | impl AtomSize for Trak { 59 | fn size(&self) -> Size { 60 | let content_len = self.tkhd.len() + self.tref.len_or_zero() + self.mdia.len_or_zero(); 61 | Size::from(content_len) 62 | } 63 | } 64 | 65 | impl WriteAtom for Trak { 66 | fn write_atom(&self, writer: &mut impl Write, changes: &[Change<'_>]) -> crate::Result<()> { 67 | self.write_head(writer)?; 68 | self.tkhd.write(writer, changes)?; 69 | if let Some(a) = &self.tref { 70 | a.write(writer, changes)?; 71 | } 72 | if let Some(a) = &self.mdia { 73 | a.write(writer, changes)?; 74 | } 75 | Ok(()) 76 | } 77 | } 78 | 79 | impl SimpleCollectChanges for Trak { 80 | fn state(&self) -> &State { 81 | &self.state 82 | } 83 | 84 | fn existing<'a>( 85 | &'a self, 86 | level: u8, 87 | bounds: &'a AtomBounds, 88 | changes: &mut Vec>, 89 | ) -> i64 { 90 | self.tref.collect_changes(bounds.end(), level, changes) 91 | + self.mdia.collect_changes(bounds.end(), level, changes) 92 | } 93 | 94 | fn atom_ref(&self) -> AtomRef<'_> { 95 | AtomRef::Trak(self) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/atom/text.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 4 | pub struct Text { 5 | pub state: State, 6 | pub data: Cow<'static, [u8]>, 7 | } 8 | 9 | impl Atom for Text { 10 | const FOURCC: Fourcc = TEXT_MEDIA; 11 | } 12 | 13 | impl ParseAtom for Text { 14 | fn parse_atom( 15 | reader: &mut (impl Read + Seek), 16 | _cfg: &ParseConfig<'_>, 17 | size: Size, 18 | ) -> crate::Result { 19 | let bounds = find_bounds(reader, size)?; 20 | let data = reader.read_u8_vec(size.content_len())?; 21 | Ok(Self { 22 | state: State::Existing(bounds), 23 | data: Cow::Owned(data), 24 | }) 25 | } 26 | } 27 | 28 | impl AtomSize for Text { 29 | fn size(&self) -> Size { 30 | Size::from(self.data.len() as u64) 31 | } 32 | } 33 | 34 | impl WriteAtom for Text { 35 | fn write_atom(&self, writer: &mut impl Write, _changes: &[Change<'_>]) -> crate::Result<()> { 36 | self.write_head(writer)?; 37 | writer.write_all(&self.data)?; 38 | Ok(()) 39 | } 40 | } 41 | 42 | impl LeafAtomCollectChanges for Text { 43 | fn state(&self) -> &State { 44 | &self.state 45 | } 46 | 47 | fn atom_ref(&self) -> AtomRef<'_> { 48 | AtomRef::Text(self) 49 | } 50 | } 51 | 52 | impl Text { 53 | pub fn media_chapter() -> Self { 54 | Self { 55 | state: State::Insert, 56 | data: Cow::Borrowed(&[ 57 | // Text Sample Entry 58 | 0x00, 0x00, 0x00, 0x01, // displayFlags 59 | 0x00, 0x00, // horizontal and vertical justification 60 | 0x00, 0x00, 0x00, 0x00, // bg color rgba 61 | // Box Record 62 | 0x00, 0x00, // def text box top 63 | 0x00, 0x00, // def text box left 64 | 0x00, 0x00, // def text box bottom 65 | 0x00, 0x00, // def text box right 66 | // Style Record 67 | 0x00, 0x00, // start char 68 | 0x00, 0x00, // end char 69 | 0x00, 0x01, // font ID 70 | 0x00, // font style flags 71 | 0x00, // font size 72 | 0x00, 0x00, 0x00, 0x00, // fg color rgba 73 | // Font Table Box 74 | 0x00, 0x00, 0x00, 0x0D, // box size 75 | b'f', b't', b'a', b'b', // box atom name 76 | 0x00, 0x01, // entry count 77 | // Font Record 78 | 0x00, 0x01, // font ID 79 | 0x00, // font name length 80 | ]), 81 | } 82 | } 83 | 84 | pub fn media_information_chapter() -> Self { 85 | Self { 86 | state: State::Insert, 87 | data: Cow::Borrowed(&[ 88 | 0x00, 0x01, // ?? 89 | 0x00, 0x00, 0x00, 0x00, // 90 | 0x00, 0x00, 0x00, 0x00, // 91 | 0x00, 0x00, 0x00, 0x00, // 92 | 0x00, 0x00, 0x00, 0x01, // ?? 93 | 0x00, 0x00, 0x00, 0x00, // 94 | 0x00, 0x00, 0x00, 0x00, // 95 | 0x00, 0x00, 0x00, 0x00, // 96 | 0x00, 0x00, 0x40, 0x00, // ?? 97 | 0x00, 0x00, // 98 | ]), 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::{error, fmt, io}; 3 | 4 | use crate::Fourcc; 5 | 6 | /// Type alias for the result of tag operations. 7 | pub type Result = std::result::Result; 8 | 9 | /// Kinds of errors that may occur while performing metadata operations. 10 | #[derive(Debug)] 11 | pub enum ErrorKind { 12 | /// An atom could not be found. Contains the atom's identifier. 13 | AtomNotFound(Fourcc), 14 | /// A descriptor could not be found. Contains the descriptor's tag. 15 | DescriptorNotFound(u8), 16 | /// No filetype (`ftyp`) atom, which indicates na MPEG-4 file, could be found. 17 | NoFtyp, 18 | /// The size of an atom is smaller than its header, or otherwise unsound. 19 | InvalidAtomSize, 20 | /// The content of an atom suggests another length than its header. 21 | SizeMismatch, 22 | /// The header of an atom specifies a size that either exceeds the parent atom or the file. 23 | AtomSizeOutOfBounds, 24 | /// The sample table atom (`stbl`) contains inconsistent data. 25 | InvalidSampleTable, 26 | /// The [`ChannelConfig`] code is unknown. Contains the unknown code. 27 | /// 28 | /// [`ChannelConfig`]: crate::ChannelConfig 29 | UnknownChannelConfig(u8), 30 | /// The [`MediaType`] code is unknown. Contains the unknown code. 31 | /// 32 | /// [`MediaType`]: crate::MediaType 33 | UnknownMediaType(u8), 34 | /// The [`SampleRate`] index is unknown. Contains the unknown index. 35 | /// 36 | /// [`SampleRate`]: crate::SampleRate 37 | UnknownSampleRate(u8), 38 | /// Either the version byte of an atom or a descriptor is unknown. Contains the unknown version. 39 | UnknownVersion(u8), 40 | /// An invalid utf-8 string was found. 41 | Utf8StringDecoding, 42 | /// An invalid utf-16 string was found. 43 | Utf16StringDecoding, 44 | /// An IO error has occurred. 45 | Io(io::Error), 46 | } 47 | 48 | /// Any error that may occur while performing metadata operations. 49 | pub struct Error { 50 | /// The kind of error that occurred. 51 | pub kind: ErrorKind, 52 | /// A human readable string describing the error. 53 | pub description: Cow<'static, str>, 54 | } 55 | 56 | impl Error { 57 | pub fn new(kind: ErrorKind, description: impl Into>) -> Error { 58 | Error { kind, description: description.into() } 59 | } 60 | } 61 | 62 | impl error::Error for Error { 63 | fn cause(&self) -> Option<&dyn error::Error> { 64 | match self.kind { 65 | ErrorKind::Io(ref err) => Some(err), 66 | _ => None, 67 | } 68 | } 69 | } 70 | 71 | impl From for Error { 72 | fn from(err: io::Error) -> Error { 73 | let description = format!("IO error: {err}"); 74 | Error::new(ErrorKind::Io(err), description) 75 | } 76 | } 77 | 78 | impl fmt::Debug for Error { 79 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 80 | if self.description.is_empty() { 81 | write!(f, "{:?}", self.kind) 82 | } else { 83 | write!(f, "{}:\n{:?}", self.description, self.kind) 84 | } 85 | } 86 | } 87 | 88 | impl fmt::Display for Error { 89 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 90 | if self.description.is_empty() { 91 | write!(f, "{:?}", self.kind) 92 | } else { 93 | write!(f, "{}:\n{:?}", self.description, self.kind) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A library for reading and writing iTunes style MPEG-4 audio metadata. 2 | //! 3 | //! # Examples 4 | //! 5 | //! ## The Easy Way 6 | //! ```no_run 7 | //! let mut tag = mp4ameta::Tag::read_from_path("music.m4a").unwrap(); 8 | //! 9 | //! println!("{}", tag.artist().unwrap()); 10 | //! 11 | //! tag.set_artist("artist"); 12 | //! tag.write_to_path("music.m4a").unwrap(); 13 | //! ``` 14 | //! 15 | //! ## The Hard Way 16 | //! ```no_run 17 | //! use mp4ameta::{Data, Fourcc, Tag}; 18 | //! 19 | //! let mut tag = Tag::read_from_path("music.m4a").unwrap(); 20 | //! let artist_ident = Fourcc(*b"\xa9ART"); 21 | //! 22 | //! let artist = tag.strings_of(&artist_ident).next().unwrap(); 23 | //! println!("{}", artist); 24 | //! 25 | //! tag.set_data(artist_ident, Data::Utf8("artist".to_owned())); 26 | //! tag.write_to_path("music.m4a").unwrap(); 27 | //! ``` 28 | //! 29 | //! ## Using Freeform Identifiers 30 | //! ```no_run 31 | //! use mp4ameta::{Data, FreeformIdent, Tag}; 32 | //! 33 | //! let mut tag = Tag::read_from_path("music.m4a").unwrap(); 34 | //! let isrc_ident = FreeformIdent::new_static("com.apple.iTunes", "ISRC"); 35 | //! 36 | //! let isrc = tag.strings_of(&isrc_ident).next().unwrap(); 37 | //! println!("{}", isrc); 38 | //! 39 | //! tag.set_data(isrc_ident, Data::Utf8("isrc".to_owned())); 40 | //! tag.write_to_path("music.m4a").unwrap(); 41 | //! ``` 42 | //! 43 | //! ## Chapters 44 | //! There are two ways of storing chapters in mp4 files. 45 | //! They can either be stored inside a chapter list, or a chapter track. 46 | //! ```no_run 47 | //! use mp4ameta::{Chapter, Tag}; 48 | //! use std::time::Duration; 49 | //! 50 | //! let mut tag = Tag::read_from_path("audiobook.m4b").unwrap(); 51 | //! 52 | //! for chapter in tag.chapter_track() { 53 | //! let mins = chapter.start.as_secs() / 60; 54 | //! let secs = chapter.start.as_secs() % 60; 55 | //! println!("{mins:02}:{secs:02} {}", chapter.title); 56 | //! } 57 | //! tag.chapter_track_mut().clear(); 58 | //! 59 | //! tag.chapter_list_mut().extend([ 60 | //! Chapter::new(Duration::ZERO, "first chapter"), 61 | //! Chapter::new(Duration::from_secs(3 * 60 + 42), "second chapter"), 62 | //! Chapter::new(Duration::from_secs(7 * 60 + 13), "third chapter"), 63 | //! ]); 64 | //! 65 | //! tag.write_to_path("audiobook.m4b").unwrap(); 66 | //! ``` 67 | //! 68 | //! ## Read and Write Configurations 69 | //! Read only the data that is relevant for your usecase. 70 | //! And (over)write only the data that you want to edit. 71 | //! 72 | //! By default all data is read and written. 73 | //! ```no_run 74 | //! use mp4ameta::{ChplTimescale, ReadConfig, Tag, WriteConfig}; 75 | //! 76 | //! // Only read the metadata item list, not chapters or audio information 77 | //! let read_cfg = ReadConfig { 78 | //! read_meta_items: true, 79 | //! read_image_data: false, 80 | //! ..ReadConfig::NONE 81 | //! }; 82 | //! let mut tag = Tag::read_with_path("music.m4a", &read_cfg).unwrap(); 83 | //! 84 | //! println!("{tag}"); 85 | //! 86 | //! tag.clear_meta_items(); 87 | //! 88 | //! // Only overwrite the metadata item list, leave chapters intact 89 | //! let write_cfg = WriteConfig { 90 | //! write_meta_items: true, 91 | //! ..WriteConfig::NONE 92 | //! }; 93 | //! tag.write_with_path("music.m4a", &write_cfg).unwrap(); 94 | //! ``` 95 | #![deny(rust_2018_idioms)] 96 | 97 | pub use crate::atom::ident::{self, DataIdent, Fourcc, FreeformIdent, Ident}; 98 | pub use crate::atom::{ChplTimescale, Data, ReadConfig, StorageFile, WriteConfig}; 99 | pub use crate::error::{Error, ErrorKind, Result}; 100 | pub use crate::tag::{STANDARD_GENRES, Tag, Userdata}; 101 | pub use crate::types::*; 102 | 103 | pub(crate) use crate::atom::MetaItem; 104 | 105 | #[macro_use] 106 | mod atom; 107 | mod error; 108 | mod tag; 109 | mod types; 110 | mod util; 111 | -------------------------------------------------------------------------------- /src/atom/mvhd.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub const HEADER_SIZE_V0: usize = 100; 4 | pub const HEADER_SIZE_V1: usize = 112; 5 | const BUF_SIZE_V0: usize = HEADER_SIZE_V0 - 4; 6 | const BUF_SIZE_V1: usize = HEADER_SIZE_V1 - 4; 7 | 8 | const_assert!(std::mem::size_of::() == BUF_SIZE_V0); 9 | const_assert!(std::mem::size_of::() == BUF_SIZE_V1); 10 | 11 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 12 | pub struct Mvhd { 13 | pub version: u8, 14 | pub flags: [u8; 3], 15 | pub timescale: u32, 16 | pub duration: u64, 17 | } 18 | 19 | #[derive(Default)] 20 | #[repr(C)] 21 | struct MvhdBufV0 { 22 | creation_time: [u8; 4], 23 | modification_time: [u8; 4], 24 | timescale: [u8; 4], 25 | duration: [u8; 4], 26 | preferred_rate: [u8; 4], 27 | preferred_volume: [u8; 2], 28 | reserved: [u8; 10], 29 | matrix: [[[u8; 4]; 3]; 3], 30 | preview_time: [u8; 4], 31 | preview_duration: [u8; 4], 32 | poster_time: [u8; 4], 33 | selection_time: [u8; 4], 34 | selection_duration: [u8; 4], 35 | current_time: [u8; 4], 36 | next_track_id: [u8; 4], 37 | } 38 | 39 | impl MvhdBufV0 { 40 | fn bytes_mut(&mut self) -> &mut [u8; BUF_SIZE_V0] { 41 | // SAFETY: alignment and size match because all fields are byte arrays 42 | unsafe { std::mem::transmute(self) } 43 | } 44 | } 45 | 46 | #[derive(Default)] 47 | #[repr(C)] 48 | struct MvhdBufV1 { 49 | creation_time: [u8; 8], 50 | modification_time: [u8; 8], 51 | timescale: [u8; 4], 52 | duration: [u8; 8], 53 | preferred_rate: [u8; 4], 54 | preferred_volume: [u8; 2], 55 | reserved: [u8; 10], 56 | matrix: [[[u8; 4]; 3]; 3], 57 | preview_time: [u8; 4], 58 | preview_duration: [u8; 4], 59 | poster_time: [u8; 4], 60 | selection_time: [u8; 4], 61 | selection_duration: [u8; 4], 62 | current_time: [u8; 4], 63 | next_track_id: [u8; 4], 64 | } 65 | 66 | impl MvhdBufV1 { 67 | fn bytes_mut(&mut self) -> &mut [u8; BUF_SIZE_V1] { 68 | // SAFETY: alignment and size match because all fields are byte arrays 69 | unsafe { std::mem::transmute(self) } 70 | } 71 | } 72 | 73 | impl Atom for Mvhd { 74 | const FOURCC: Fourcc = MOVIE_HEADER; 75 | } 76 | 77 | impl ParseAtom for Mvhd { 78 | fn parse_atom( 79 | reader: &mut (impl Read + Seek), 80 | _cfg: &ParseConfig<'_>, 81 | size: Size, 82 | ) -> crate::Result { 83 | let mut mvhd = Self::default(); 84 | 85 | let (version, flags) = head::parse_full(reader)?; 86 | mvhd.version = version; 87 | mvhd.flags = flags; 88 | 89 | match version { 90 | 0 => { 91 | expect_size("Movie header (mvhd) version 0", size, HEADER_SIZE_V0 as u64)?; 92 | 93 | let mut buf = MvhdBufV0::default(); 94 | reader.read_exact(buf.bytes_mut())?; 95 | mvhd.timescale = u32::from_be_bytes(buf.timescale); 96 | mvhd.duration = u32::from_be_bytes(buf.duration) as u64; 97 | } 98 | 1 => { 99 | expect_size("Movie header (mvhd) version 1", size, HEADER_SIZE_V1 as u64)?; 100 | 101 | let mut buf = MvhdBufV1::default(); 102 | reader.read_exact(buf.bytes_mut())?; 103 | mvhd.timescale = u32::from_be_bytes(buf.timescale); 104 | mvhd.duration = u64::from_be_bytes(buf.duration); 105 | } 106 | _ => { 107 | return unknown_version("movie header (mvhd)", version); 108 | } 109 | } 110 | 111 | Ok(mvhd) 112 | } 113 | } 114 | 115 | impl AtomSize for Mvhd { 116 | fn size(&self) -> Size { 117 | match self.version { 118 | 0 => Size::from(HEADER_SIZE_V0 as u64), 119 | 1 => Size::from(HEADER_SIZE_V1 as u64), 120 | _ => Size::from(0), 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/tag/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fs::File; 3 | use std::io::{BufReader, Read, Seek}; 4 | use std::ops::{Deref, DerefMut}; 5 | use std::path::Path; 6 | 7 | use crate::{AudioInfo, ReadConfig, atom, util}; 8 | 9 | pub use userdata::*; 10 | 11 | mod readonly; 12 | mod userdata; 13 | 14 | /// A tag containing MPEG-4 audio metadata. 15 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 16 | pub struct Tag { 17 | /// The filetype (`ftyp`) atom. 18 | pub ftyp: String, 19 | pub info: AudioInfo, 20 | pub userdata: Userdata, 21 | } 22 | 23 | impl Deref for Tag { 24 | type Target = Userdata; 25 | 26 | fn deref(&self) -> &Self::Target { 27 | &self.userdata 28 | } 29 | } 30 | 31 | impl DerefMut for Tag { 32 | fn deref_mut(&mut self) -> &mut Self::Target { 33 | &mut self.userdata 34 | } 35 | } 36 | 37 | impl fmt::Display for Tag { 38 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 39 | self.format_album_artists(f)?; 40 | self.format_album_artist_sort_orders(f)?; 41 | self.format_artists(f)?; 42 | self.format_artist_sort_orders(f)?; 43 | self.format_composers(f)?; 44 | self.format_composer_sort_orders(f)?; 45 | self.format_lyricists(f)?; 46 | self.format_album(f)?; 47 | self.format_album_sort_order(f)?; 48 | self.format_title(f)?; 49 | self.format_title_sort_order(f)?; 50 | self.format_genres(f)?; 51 | self.format_year(f)?; 52 | self.format_track(f)?; 53 | self.format_disc(f)?; 54 | self.format_artworks(f)?; 55 | self.format_advisory_rating(f)?; 56 | self.format_media_type(f)?; 57 | self.format_groupings(f)?; 58 | self.format_descriptions(f)?; 59 | self.format_comments(f)?; 60 | self.format_categories(f)?; 61 | self.format_keywords(f)?; 62 | self.format_copyright(f)?; 63 | self.format_encoder(f)?; 64 | self.format_publisher(f)?; 65 | self.format_tv_show_name(f)?; 66 | self.format_tv_show_name_sort_order(f)?; 67 | self.format_tv_network_name(f)?; 68 | self.format_tv_episode_name(f)?; 69 | self.format_tv_episode(f)?; 70 | self.format_tv_season(f)?; 71 | self.format_bpm(f)?; 72 | self.format_movement(f)?; 73 | self.format_work(f)?; 74 | self.format_movement_count(f)?; 75 | self.format_movement_index(f)?; 76 | self.format_duration(f)?; 77 | self.format_channel_config(f)?; 78 | self.format_sample_rate(f)?; 79 | self.format_avg_bitrate(f)?; 80 | self.format_max_bitrate(f)?; 81 | self.format_show_movement(f)?; 82 | self.format_gapless_playback(f)?; 83 | self.format_compilation(f)?; 84 | self.format_isrc(f)?; 85 | self.format_label(f)?; 86 | self.format_lyrics(f)?; 87 | self.format_chapter_list(f)?; 88 | self.format_chapter_track(f)?; 89 | writeln!(f, "filetype: {}", self.filetype()) 90 | } 91 | } 92 | 93 | impl Tag { 94 | fn format_chapter_list(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 95 | if !self.userdata.chapter_list.is_empty() { 96 | writeln!(f, "chapter list:")?; 97 | util::format_chapters(f, &self.chapter_list, self.info.duration)?; 98 | } 99 | Ok(()) 100 | } 101 | 102 | fn format_chapter_track(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 103 | if !self.userdata.chapter_track.is_empty() { 104 | writeln!(f, "chapter track:")?; 105 | util::format_chapters(f, &self.chapter_track, self.info.duration)?; 106 | } 107 | Ok(()) 108 | } 109 | } 110 | 111 | impl Tag { 112 | /// Attempts to read a MPEG-4 audio tag from the reader. 113 | pub fn read_with(reader: &mut (impl Read + Seek), cfg: &ReadConfig) -> crate::Result { 114 | atom::read_tag(reader, cfg) 115 | } 116 | 117 | /// Attempts to read a MPEG-4 audio tag from the reader. 118 | pub fn read_from(reader: &mut (impl Read + Seek)) -> crate::Result { 119 | Self::read_with(reader, &ReadConfig::DEFAULT) 120 | } 121 | 122 | /// Attempts to read a MPEG-4 audio tag from the file at the indicated path. 123 | pub fn read_with_path(path: impl AsRef, cfg: &ReadConfig) -> crate::Result { 124 | let mut file = BufReader::new(File::open(path)?); 125 | Self::read_with(&mut file, cfg) 126 | } 127 | 128 | /// Attempts to read a MPEG-4 audio tag from the file at the indicated path. 129 | pub fn read_from_path(path: impl AsRef) -> crate::Result { 130 | Self::read_with_path(path, &ReadConfig::DEFAULT) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/atom/metaitem.rs: -------------------------------------------------------------------------------- 1 | //! A metadata item can either have a plain fourcc as it's identifier: 2 | //! **** (any fourcc) 3 | //! └─ data 4 | //! 5 | //! Or it can contain a mean and name children atom which make up the identifier. 6 | //! ---- (freeform fourcc) 7 | //! ├─ mean 8 | //! ├─ name 9 | //! └─ data 10 | use super::*; 11 | 12 | /// A struct representing a metadata item, containing data that is associated with an identifier. 13 | #[derive(Clone, Debug, PartialEq, Eq)] 14 | pub struct MetaItem { 15 | /// The identifier of the atom. 16 | pub ident: DataIdent, 17 | /// The data contained in the atom. 18 | pub data: Vec, 19 | } 20 | 21 | impl MetaItem { 22 | /// Creates a metadata item with the identifier and data. 23 | pub const fn new(ident: DataIdent, data: Vec) -> Self { 24 | Self { ident, data } 25 | } 26 | 27 | /// Returns the external length of the atom in bytes. 28 | pub fn len(&self) -> u64 { 29 | let parent_len = Head::NORMAL_SIZE; 30 | let data_len: u64 = self.data.iter().map(Data::len).sum(); 31 | 32 | match &self.ident { 33 | DataIdent::Fourcc(_) => parent_len + data_len, 34 | DataIdent::Freeform { mean, name } => { 35 | let mean_len = 12 + mean.len() as u64; 36 | let name_len = 12 + name.len() as u64; 37 | 38 | parent_len + mean_len + name_len + data_len 39 | } 40 | } 41 | } 42 | 43 | pub fn parse( 44 | reader: &mut (impl Read + Seek), 45 | cfg: &ParseConfig<'_>, 46 | head: Head, 47 | ) -> crate::Result { 48 | let mut data = Vec::new(); 49 | let mut mean: Option = None; 50 | let mut name: Option = None; 51 | let mut parsed_bytes = 0; 52 | 53 | while parsed_bytes < head.content_len() { 54 | let remaining_bytes = head.content_len() - parsed_bytes; 55 | let head = head::parse(reader, remaining_bytes)?; 56 | 57 | match head.fourcc() { 58 | DATA => data.push(Data::parse(reader, cfg, head.size())?), 59 | MEAN => { 60 | let (version, _) = head::parse_full(reader)?; 61 | if version != 0 { 62 | return unknown_version("mean (mean)", version); 63 | } 64 | expect_min_size("Mean (mean)", head.size(), 4)?; 65 | 66 | mean = Some(reader.read_utf8(head.content_len() - 4)?); 67 | } 68 | NAME => { 69 | let (version, _) = head::parse_full(reader)?; 70 | if version != 0 { 71 | return unknown_version("name (name)", version); 72 | } 73 | expect_min_size("Name (name)", head.size(), 4)?; 74 | 75 | name = Some(reader.read_utf8(head.content_len() - 4)?); 76 | } 77 | _ => reader.skip(head.content_len() as i64)?, 78 | } 79 | 80 | parsed_bytes += head.len(); 81 | } 82 | 83 | let ident = match (head.fourcc(), mean, name) { 84 | (FREEFORM, Some(mean), Some(name)) => DataIdent::freeform(mean, name), 85 | (fourcc, _, _) => DataIdent::Fourcc(fourcc), 86 | }; 87 | 88 | Ok(MetaItem { ident, data }) 89 | } 90 | 91 | /// Attempts to write the metadata item to the writer. 92 | pub fn write(&self, writer: &mut impl Write) -> crate::Result<()> { 93 | writer.write_be_u32(self.len() as u32)?; 94 | 95 | match &self.ident { 96 | DataIdent::Fourcc(ident) => writer.write_all(ident.deref())?, 97 | _ => { 98 | let (mean, name) = match &self.ident { 99 | DataIdent::Freeform { mean, name } => (mean.as_ref(), name.as_ref()), 100 | DataIdent::Fourcc(_) => unreachable!(), 101 | }; 102 | writer.write_all(FREEFORM.deref())?; 103 | 104 | let mean_len: u32 = 12 + mean.len() as u32; 105 | writer.write_be_u32(mean_len)?; 106 | writer.write_all(&*MEAN)?; 107 | writer.write_all(&[0; 4])?; 108 | writer.write_utf8(mean)?; 109 | 110 | let name_len: u32 = 12 + name.len() as u32; 111 | writer.write_be_u32(name_len)?; 112 | writer.write_all(&*NAME)?; 113 | writer.write_all(&[0; 4])?; 114 | writer.write_utf8(name)?; 115 | } 116 | } 117 | 118 | for d in self.data.iter() { 119 | d.write(writer)?; 120 | } 121 | 122 | Ok(()) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mp4ameta 2 | [![Build](https://github.com/Saecki/mp4ameta/actions/workflows/build.yml/badge.svg)](https://github.com/Saecki/mp4ameta/actions/workflows/build.yml) 3 | [![Crate](https://img.shields.io/crates/v/mp4ameta.svg)](https://crates.io/crates/mp4ameta) 4 | [![Documentation](https://img.shields.io/docsrs/mp4ameta?label=docs.rs)](https://docs.rs/mp4ameta) 5 | ![License](https://img.shields.io/crates/l/mp4ameta) 6 | ![LOC](https://tokei.rs/b1/github/saecki/mp4ameta?category=code) 7 | 8 | A library for reading and writing iTunes style MPEG-4 audio metadata. 9 | Most commonly this kind of metadata is found inside `m4a` or `m4b` files but basically any `mp4` container supports it. 10 | 11 | ## Examples 12 | 13 | ### The Easy Way 14 | ```rs 15 | let mut tag = mp4ameta::Tag::read_from_path("music.m4a").unwrap(); 16 | 17 | println!("{}", tag.artist().unwrap()); 18 | 19 | tag.set_artist("artist"); 20 | tag.write_to_path("music.m4a").unwrap(); 21 | ``` 22 | 23 | ### The Hard Way 24 | ```rs 25 | use mp4ameta::{Data, Fourcc, Tag}; 26 | 27 | let mut tag = Tag::read_from_path("music.m4a").unwrap(); 28 | let artist_ident = Fourcc(*b"\xa9ART"); 29 | 30 | let artist = tag.strings_of(&artist_ident).next().unwrap(); 31 | println!("{}", artist); 32 | 33 | tag.set_data(artist_ident, Data::Utf8("artist".to_owned())); 34 | tag.write_to_path("music.m4a").unwrap(); 35 | ``` 36 | 37 | ### Using Freeform Identifiers 38 | ```rs 39 | use mp4ameta::{Data, FreeformIdent, Tag}; 40 | 41 | let mut tag = Tag::read_from_path("music.m4a").unwrap(); 42 | let isrc_ident = FreeformIdent::new_static("com.apple.iTunes", "ISRC"); 43 | 44 | let isrc = tag.strings_of(&isrc_ident).next().unwrap(); 45 | println!("{}", isrc); 46 | 47 | tag.set_data(isrc_ident, Data::Utf8("isrc".to_owned())); 48 | tag.write_to_path("music.m4a").unwrap(); 49 | ``` 50 | 51 | ### Chapters 52 | There are two ways of storing chapters in mp4 files. 53 | They can either be stored inside a chapter list, or a chapter track. 54 | ```rs 55 | use mp4ameta::{Chapter, Tag}; 56 | use std::time::Duration; 57 | 58 | let mut tag = Tag::read_from_path("audiobook.m4b").unwrap(); 59 | 60 | for chapter in tag.chapter_track() { 61 | let mins = chapter.start.as_secs() / 60; 62 | let secs = chapter.start.as_secs() % 60; 63 | println!("{mins:02}:{secs:02} {}", chapter.title); 64 | } 65 | tag.chapter_track_mut().clear(); 66 | 67 | tag.chapter_list_mut().extend([ 68 | Chapter::new(Duration::ZERO, "first chapter"), 69 | Chapter::new(Duration::from_secs(3 * 60 + 42), "second chapter"), 70 | Chapter::new(Duration::from_secs(7 * 60 + 13), "third chapter"), 71 | ]); 72 | 73 | tag.write_to_path("audiobook.m4b").unwrap(); 74 | ``` 75 | 76 | ### Read and Write Configurations 77 | Read only the data that is relevant for your usecase. 78 | And (over)write only the data that you want to edit. 79 | 80 | By default all data is read and written. 81 | ```rs 82 | use mp4ameta::{ChplTimescale, ReadConfig, Tag, WriteConfig}; 83 | 84 | // Only read the metadata item list, not chapters or audio information 85 | let read_cfg = ReadConfig { 86 | read_meta_items: true, 87 | read_image_data: false, 88 | read_chapter_list: false, 89 | read_chapter_track: false, 90 | read_audio_info: false, 91 | chpl_timescale: ChplTimescale::DEFAULT, 92 | }; 93 | let mut tag = Tag::read_with_path("music.m4a", &read_cfg).unwrap(); 94 | 95 | println!("{tag}"); 96 | 97 | tag.clear_meta_items(); 98 | 99 | // Only overwrite the metadata item list, leave chapters intact 100 | let write_cfg = WriteConfig { 101 | write_meta_items: true, 102 | write_chapter_list: false, 103 | write_chapter_track: false, 104 | chpl_timescale: ChplTimescale::DEFAULT, 105 | }; 106 | tag.write_with_path("music.m4a", &write_cfg).unwrap(); 107 | ``` 108 | 109 | ## Useful Links 110 | - QuickTime spec 111 | - [Overview of QTFF](https://developer.apple.com/documentation/quicktime-file-format) 112 | - [Movie atoms](https://developer.apple.com/documentation/quicktime-file-format/movie_atoms) 113 | - [User data atoms](https://developer.apple.com/documentation/quicktime-file-format/user_data_atoms) 114 | - [MultimediaWiki QuickTime container](https://wiki.multimedia.cx/index.php/QuickTime_container) 115 | - [AtomicParsley docs](http://atomicparsley.sourceforge.net/mpeg-4files.html) 116 | - [Mutagen docs](https://mutagen.readthedocs.io/en/latest/api/mp4.html) 117 | - [Hydrogen audio tag mapping](https://wiki.hydrogenaud.io/index.php?title=Tag_Mapping) 118 | - [MusicBrainz Picard tag mapping](https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html) 119 | - [Filetype list](https://ftyps.com/) 120 | 121 | ## Testing 122 | __Run all tests:__
123 | `cargo test` 124 | 125 | __Test this library on your collection:__
126 | `cargo test -- --nocapture collection ` 127 | 128 | -------------------------------------------------------------------------------- /src/atom/mdhd.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub const HEADER_SIZE_V0: usize = 24; 4 | pub const HEADER_SIZE_V1: usize = 36; 5 | const BUF_SIZE_V0: usize = HEADER_SIZE_V0 - 4; 6 | const BUF_SIZE_V1: usize = HEADER_SIZE_V1 - 4; 7 | 8 | const_assert!(std::mem::size_of::() == BUF_SIZE_V0); 9 | const_assert!(std::mem::size_of::() == BUF_SIZE_V1); 10 | 11 | const UNSPECIFIED_LANGUAGE: u16 = i16::MAX as u16; 12 | 13 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 14 | pub struct Mdhd { 15 | pub version: u8, 16 | pub flags: [u8; 3], 17 | pub timescale: u32, 18 | pub duration: u64, 19 | } 20 | 21 | #[derive(Default)] 22 | #[repr(C)] 23 | struct MdhdBufV0 { 24 | creation_time: [u8; 4], 25 | modification_time: [u8; 4], 26 | timescale: [u8; 4], 27 | duration: [u8; 4], 28 | language: [u8; 2], 29 | quality: [u8; 2], 30 | } 31 | 32 | impl MdhdBufV0 { 33 | fn bytes_mut(&mut self) -> &mut [u8; BUF_SIZE_V0] { 34 | // SAFETY: alignment and size match because all fields are byte arrays 35 | unsafe { std::mem::transmute(self) } 36 | } 37 | } 38 | 39 | #[derive(Default)] 40 | #[repr(C)] 41 | struct MdhdBufV1 { 42 | creation_time: [u8; 8], 43 | modification_time: [u8; 8], 44 | timescale: [u8; 4], 45 | duration: [u8; 8], 46 | language: [u8; 2], 47 | quality: [u8; 2], 48 | } 49 | 50 | impl MdhdBufV1 { 51 | fn bytes_mut(&mut self) -> &mut [u8; BUF_SIZE_V1] { 52 | // SAFETY: alignment and size match because all fields are byte arrays 53 | unsafe { std::mem::transmute(self) } 54 | } 55 | } 56 | 57 | impl Atom for Mdhd { 58 | const FOURCC: Fourcc = MEDIA_HEADER; 59 | } 60 | 61 | impl ParseAtom for Mdhd { 62 | fn parse_atom( 63 | reader: &mut (impl Read + Seek), 64 | _cfg: &ParseConfig<'_>, 65 | size: Size, 66 | ) -> crate::Result { 67 | let mut mdhd = Self::default(); 68 | 69 | let (version, flags) = head::parse_full(reader)?; 70 | mdhd.version = version; 71 | mdhd.flags = flags; 72 | 73 | match version { 74 | 0 => { 75 | expect_size("Media header (mdhd) version 0", size, HEADER_SIZE_V0 as u64)?; 76 | 77 | let mut buf = MdhdBufV0::default(); 78 | reader.read_exact(buf.bytes_mut())?; 79 | mdhd.timescale = u32::from_be_bytes(buf.timescale); 80 | mdhd.duration = u32::from_be_bytes(buf.duration) as u64; 81 | } 82 | 1 => { 83 | expect_size("Media header (mdhd) version 1", size, HEADER_SIZE_V1 as u64)?; 84 | 85 | let mut buf = MdhdBufV1::default(); 86 | reader.read_exact(buf.bytes_mut())?; 87 | mdhd.timescale = u32::from_be_bytes(buf.timescale); 88 | mdhd.duration = u64::from_be_bytes(buf.duration); 89 | } 90 | _ => { 91 | return unknown_version("media header (mdhd)", version); 92 | } 93 | } 94 | 95 | Ok(mdhd) 96 | } 97 | } 98 | 99 | impl AtomSize for Mdhd { 100 | fn size(&self) -> Size { 101 | match self.version { 102 | 0 => Size::from(HEADER_SIZE_V0 as u64), 103 | 1 => Size::from(HEADER_SIZE_V1 as u64), 104 | _ => Size::from(0), 105 | } 106 | } 107 | } 108 | 109 | impl WriteAtom for Mdhd { 110 | fn write_atom(&self, writer: &mut impl Write, _changes: &[Change<'_>]) -> crate::Result<()> { 111 | self.write_head(writer)?; 112 | head::write_full(writer, self.version, self.flags)?; 113 | 114 | match self.version { 115 | 0 => { 116 | let mut buf = MdhdBufV0 { 117 | timescale: u32::to_be_bytes(self.timescale), 118 | duration: u32::to_be_bytes(self.duration as u32), 119 | language: u16::to_be_bytes(UNSPECIFIED_LANGUAGE), 120 | ..Default::default() 121 | }; 122 | writer.write_all(buf.bytes_mut())?; 123 | } 124 | 1 => { 125 | let mut buf = MdhdBufV1 { 126 | timescale: u32::to_be_bytes(self.timescale), 127 | duration: u64::to_be_bytes(self.duration), 128 | language: u16::to_be_bytes(UNSPECIFIED_LANGUAGE), 129 | ..Default::default() 130 | }; 131 | writer.write_all(buf.bytes_mut())?; 132 | } 133 | v => { 134 | return Err(crate::Error::new( 135 | crate::ErrorKind::UnknownVersion(self.version), 136 | format!("Unknown media header (mdhd) version {v}"), 137 | )); 138 | } 139 | } 140 | 141 | Ok(()) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/atom/chpl.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub const DEFAULT_TIMESCALE: NonZeroU32 = NonZeroU32::new(10_000_000).unwrap(); 4 | 5 | pub const HEADER_SIZE_V0: u64 = 5; 6 | pub const HEADER_SIZE_V1: u64 = 9; 7 | pub const ITEM_HEADER_SIZE: u64 = 9; 8 | 9 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 10 | pub struct Chpl<'a> { 11 | pub state: State, 12 | pub data: ChplData<'a>, 13 | } 14 | 15 | #[derive(Clone, Debug, PartialEq, Eq)] 16 | pub enum ChplData<'a> { 17 | Owned(Vec), 18 | Borrowed(u32, &'a [Chapter]), 19 | } 20 | 21 | impl Default for ChplData<'_> { 22 | fn default() -> Self { 23 | ChplData::Owned(Vec::new()) 24 | } 25 | } 26 | 27 | #[derive(Clone, Debug, PartialEq, Eq)] 28 | pub struct ChplItem { 29 | pub start: u64, 30 | pub title: String, 31 | } 32 | 33 | impl Atom for Chpl<'_> { 34 | const FOURCC: Fourcc = CHAPTER_LIST; 35 | } 36 | 37 | impl ParseAtom for Chpl<'_> { 38 | fn parse_atom( 39 | reader: &mut (impl Read + Seek), 40 | _cfg: &ParseConfig<'_>, 41 | size: Size, 42 | ) -> crate::Result { 43 | let bounds = find_bounds(reader, size)?; 44 | let (version, _) = head::parse_full(reader)?; 45 | let header_size = match version { 46 | 0 => HEADER_SIZE_V0, 47 | 1 => { 48 | reader.skip(4)?; // ??? 49 | HEADER_SIZE_V1 50 | } 51 | _ => { 52 | return unknown_version("chapter list (chpl)", version); 53 | } 54 | }; 55 | 56 | expect_min_size("Chapter list (chpl)", size, header_size)?; 57 | 58 | let num_entries = reader.read_u8()?; 59 | let table_size = size.content_len() - header_size; 60 | let mut buf = vec![0; table_size as usize]; 61 | reader.read_exact(&mut buf)?; 62 | 63 | let mut cursor = std::io::Cursor::new(buf); 64 | 65 | let mut chpl = Vec::with_capacity(num_entries as usize); 66 | for _ in 0..num_entries { 67 | let start = cursor.read_be_u64()?; 68 | 69 | let str_len = cursor.read_u8()?; 70 | let title = cursor.read_utf8(str_len as u64)?; 71 | 72 | chpl.push(ChplItem { start, title }); 73 | } 74 | 75 | Ok(Self { 76 | state: State::Existing(bounds), 77 | data: ChplData::Owned(chpl), 78 | }) 79 | } 80 | } 81 | 82 | impl AtomSize for Chpl<'_> { 83 | fn size(&self) -> Size { 84 | let data_len = match &self.data { 85 | ChplData::Owned(v) => { 86 | v.iter().map(|c| ITEM_HEADER_SIZE + title_len(&c.title) as u64).sum::() 87 | } 88 | ChplData::Borrowed(_, v) => { 89 | v.iter().map(|c| ITEM_HEADER_SIZE + title_len(&c.title) as u64).sum::() 90 | } 91 | }; 92 | let content_len = HEADER_SIZE_V0 + data_len; 93 | Size::from(content_len) 94 | } 95 | } 96 | 97 | impl WriteAtom for Chpl<'_> { 98 | fn write_atom(&self, writer: &mut impl Write, _changes: &[Change<'_>]) -> crate::Result<()> { 99 | self.write_head(writer)?; 100 | head::write_full(writer, 0, [0; 3])?; 101 | 102 | match &self.data { 103 | ChplData::Owned(v) => { 104 | writer.write_u8(v.len() as u8)?; 105 | for c in v.iter() { 106 | writer.write_be_u64(c.start)?; 107 | 108 | let title_len = title_len(&c.title); 109 | writer.write_u8(title_len as u8)?; 110 | writer.write_utf8(&c.title[..title_len])?; 111 | } 112 | } 113 | ChplData::Borrowed(timescale, chapters) => { 114 | writer.write_u8(chapters.len() as u8)?; 115 | for c in chapters.iter() { 116 | let start = unscale_duration(*timescale, c.start); 117 | writer.write_be_u64(start)?; 118 | 119 | let title_len = title_len(&c.title); 120 | writer.write_u8(title_len as u8)?; 121 | writer.write_utf8(&c.title[..title_len])?; 122 | } 123 | } 124 | } 125 | 126 | Ok(()) 127 | } 128 | } 129 | 130 | impl LeafAtomCollectChanges for Chpl<'_> { 131 | fn state(&self) -> &State { 132 | &self.state 133 | } 134 | 135 | fn atom_ref(&self) -> AtomRef<'_> { 136 | AtomRef::Chpl(self) 137 | } 138 | } 139 | 140 | impl Chpl<'_> { 141 | pub fn into_owned(self) -> Option> { 142 | match self.data { 143 | ChplData::Owned(v) => Some(v), 144 | ChplData::Borrowed(_, _) => None, 145 | } 146 | } 147 | } 148 | 149 | fn title_len(title: &str) -> usize { 150 | title.len().min(u8::MAX as usize) 151 | } 152 | -------------------------------------------------------------------------------- /src/atom/tkhd.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | pub const HEADER_SIZE_V0: usize = 84; 4 | pub const HEADER_SIZE_V1: usize = 96; 5 | const BUF_SIZE_V0: usize = HEADER_SIZE_V0 - 4; 6 | const BUF_SIZE_V1: usize = HEADER_SIZE_V1 - 4; 7 | 8 | const_assert!(std::mem::size_of::() == BUF_SIZE_V0); 9 | const_assert!(std::mem::size_of::() == BUF_SIZE_V1); 10 | 11 | const MATRIX: [[[u8; 4]; 3]; 3] = [ 12 | [u32::to_be_bytes(1 << 16), u32::to_be_bytes(0), u32::to_be_bytes(0)], 13 | [u32::to_be_bytes(0), u32::to_be_bytes(1 << 16), u32::to_be_bytes(0)], 14 | [u32::to_be_bytes(0), u32::to_be_bytes(0), u32::to_be_bytes(1 << 30)], 15 | ]; 16 | 17 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 18 | pub struct Tkhd { 19 | pub version: u8, 20 | pub flags: [u8; 3], 21 | pub id: u32, 22 | /// The duration in mvhd timescale units 23 | pub duration: u64, 24 | } 25 | 26 | #[derive(Default)] 27 | #[repr(C)] 28 | struct TkhdBufV0 { 29 | creation_time: [u8; 4], 30 | modification_time: [u8; 4], 31 | id: [u8; 4], 32 | reserved0: [u8; 4], 33 | duration: [u8; 4], 34 | reserved1: [u8; 8], 35 | layer: [u8; 2], 36 | alternate_group: [u8; 2], 37 | volume: [u8; 2], 38 | reserved2: [u8; 2], 39 | matrix: [[[u8; 4]; 3]; 3], 40 | track_width: [u8; 4], 41 | track_height: [u8; 4], 42 | } 43 | 44 | impl TkhdBufV0 { 45 | fn bytes_mut(&mut self) -> &mut [u8; BUF_SIZE_V0] { 46 | // SAFETY: alignment and size match because all fields are byte arrays 47 | unsafe { std::mem::transmute(self) } 48 | } 49 | } 50 | 51 | #[derive(Default)] 52 | #[repr(C)] 53 | struct TkhdBufV1 { 54 | creation_time: [u8; 8], 55 | modification_time: [u8; 8], 56 | id: [u8; 4], 57 | reserved0: [u8; 4], 58 | duration: [u8; 8], 59 | reserved1: [u8; 8], 60 | layer: [u8; 2], 61 | alternate_group: [u8; 2], 62 | volume: [u8; 2], 63 | reserved2: [u8; 2], 64 | matrix: [[[u8; 4]; 3]; 3], 65 | track_width: [u8; 4], 66 | track_height: [u8; 4], 67 | } 68 | 69 | impl TkhdBufV1 { 70 | fn bytes_mut(&mut self) -> &mut [u8; BUF_SIZE_V1] { 71 | // SAFETY: alignment and size match because all fields are byte arrays 72 | unsafe { std::mem::transmute(self) } 73 | } 74 | } 75 | 76 | impl Atom for Tkhd { 77 | const FOURCC: Fourcc = TRACK_HEADER; 78 | } 79 | 80 | impl ParseAtom for Tkhd { 81 | fn parse_atom( 82 | reader: &mut (impl Read + Seek), 83 | _cfg: &ParseConfig<'_>, 84 | size: Size, 85 | ) -> crate::Result { 86 | let mut tkhd = Self::default(); 87 | 88 | let (version, flags) = head::parse_full(reader)?; 89 | tkhd.version = version; 90 | tkhd.flags = flags; 91 | 92 | match version { 93 | 0 => { 94 | expect_size("Track header (tkhd) version 0", size, HEADER_SIZE_V0 as u64)?; 95 | 96 | let mut buf = TkhdBufV0::default(); 97 | reader.read_exact(buf.bytes_mut())?; 98 | tkhd.id = u32::from_be_bytes(buf.id); 99 | tkhd.duration = u32::from_be_bytes(buf.duration) as u64; 100 | } 101 | 1 => { 102 | expect_size("Track header (tkhd) version 1", size, HEADER_SIZE_V1 as u64)?; 103 | 104 | let mut buf = TkhdBufV1::default(); 105 | reader.read_exact(buf.bytes_mut())?; 106 | tkhd.id = u32::from_be_bytes(buf.id); 107 | tkhd.duration = u64::from_be_bytes(buf.duration); 108 | } 109 | _ => { 110 | return unknown_version("track header (tkhd)", version); 111 | } 112 | } 113 | 114 | Ok(tkhd) 115 | } 116 | } 117 | 118 | impl AtomSize for Tkhd { 119 | fn size(&self) -> Size { 120 | match self.version { 121 | 0 => Size::from(HEADER_SIZE_V0 as u64), 122 | 1 => Size::from(HEADER_SIZE_V1 as u64), 123 | _ => Size::from(0), 124 | } 125 | } 126 | } 127 | 128 | impl WriteAtom for Tkhd { 129 | fn write_atom(&self, writer: &mut impl Write, _changes: &[Change<'_>]) -> crate::Result<()> { 130 | self.write_head(writer)?; 131 | head::write_full(writer, self.version, self.flags)?; 132 | 133 | match self.version { 134 | 0 => { 135 | let mut buf = TkhdBufV0 { 136 | id: u32::to_be_bytes(self.id), 137 | duration: u32::to_be_bytes(self.duration as u32), 138 | matrix: MATRIX, 139 | ..Default::default() 140 | }; 141 | writer.write_all(buf.bytes_mut())?; 142 | } 143 | 1 => { 144 | let mut buf = TkhdBufV1 { 145 | id: u32::to_be_bytes(self.id), 146 | duration: u64::to_be_bytes(self.duration), 147 | matrix: MATRIX, 148 | ..Default::default() 149 | }; 150 | writer.write_all(buf.bytes_mut())?; 151 | } 152 | v => { 153 | return Err(crate::Error::new( 154 | crate::ErrorKind::UnknownVersion(self.version), 155 | format!("Unknown track header (tkhd) version {v}"), 156 | )); 157 | } 158 | } 159 | 160 | Ok(()) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/atom/head.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | /// A struct storing size of an atom and whether it is extended. 4 | /// 5 | /// ```md 6 | /// 4 bytes standard length 7 | /// 4 bytes identifier 8 | /// 8 bytes optional extended length 9 | /// ``` 10 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] 11 | pub struct Size { 12 | ext: bool, 13 | len: u64, 14 | } 15 | 16 | impl Size { 17 | pub const fn new(ext: bool, content_len: u64) -> Self { 18 | let len = if ext { content_len + Head::EXT_SIZE } else { content_len + Head::NORMAL_SIZE }; 19 | Self { ext, len } 20 | } 21 | 22 | pub const fn from(content_len: u64) -> Self { 23 | let ext = content_len + Head::NORMAL_SIZE > u32::MAX as u64; 24 | Self::new(ext, content_len) 25 | } 26 | 27 | /// Whether the head is of standard size (8 bytes) with a 32 bit length or extended (16 bytes) 28 | /// with a 64 bit length. 29 | pub const fn ext(&self) -> bool { 30 | self.ext 31 | } 32 | 33 | /// The length including the atom's head. 34 | pub const fn len(&self) -> u64 { 35 | self.len 36 | } 37 | 38 | /// The length of the atom's head. 39 | pub const fn head_len(&self) -> u64 { 40 | match self.ext { 41 | true => Head::EXT_SIZE, 42 | false => Head::NORMAL_SIZE, 43 | } 44 | } 45 | 46 | /// The length excluding the atom's head. 47 | pub const fn content_len(&self) -> u64 { 48 | self.len - self.head_len() 49 | } 50 | } 51 | 52 | /// A head specifying the size and type of an atom. 53 | /// 54 | /// ```md 55 | /// 4 bytes standard length 56 | /// 4 bytes identifier 57 | /// 8 bytes optional extended length 58 | /// ``` 59 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 60 | pub struct Head { 61 | size: Size, 62 | fourcc: Fourcc, 63 | } 64 | 65 | impl Deref for Head { 66 | type Target = Size; 67 | 68 | fn deref(&self) -> &Self::Target { 69 | &self.size 70 | } 71 | } 72 | 73 | impl Head { 74 | pub const NORMAL_SIZE: u64 = 8; 75 | pub const EXT_SIZE: u64 = 16; 76 | 77 | pub const fn new(ext: bool, len: u64, fourcc: Fourcc) -> Self { 78 | Self { size: Size { ext, len }, fourcc } 79 | } 80 | 81 | pub const fn from(size: Size, fourcc: Fourcc) -> Self { 82 | Self { size, fourcc } 83 | } 84 | 85 | pub const fn size(&self) -> Size { 86 | self.size 87 | } 88 | 89 | pub const fn fourcc(&self) -> Fourcc { 90 | self.fourcc 91 | } 92 | } 93 | 94 | /// Attempts to parse the atom's head containing a 32 bit unsigned integer determining the size of 95 | /// the atom in bytes and the following 4 byte identifier from the reader. If the 32 bit length is 96 | /// set to 1 an extended 64 bit length is read. 97 | /// 98 | /// ```md 99 | /// 4 bytes standard length 100 | /// 4 bytes identifier 101 | /// 8 bytes optional extended length 102 | /// ``` 103 | pub fn parse(reader: &mut impl Read, remaining_bytes: u64) -> crate::Result { 104 | let mut buf = [[0u8; 4]; 2]; 105 | 106 | // SAFETY: the buffer has the same size and alignment 107 | let byte_buf: &mut [u8; 8] = unsafe { std::mem::transmute(&mut buf) }; 108 | 109 | reader 110 | .read_exact(byte_buf) 111 | .map_err(|e| crate::Error::new(ErrorKind::Io(e), "Error reading atom head"))?; 112 | 113 | let mut len = u32::from_be_bytes(buf[0]) as u64; 114 | let fourcc = Fourcc(buf[1]); 115 | 116 | let ext = if len == 1 { 117 | match reader.read_be_u64() { 118 | Ok(ext_len) if ext_len < 16 => { 119 | return Err(crate::Error::new( 120 | crate::ErrorKind::InvalidAtomSize, 121 | format!( 122 | "Read extended length of '{fourcc}' which is less than 16 bytes: {ext_len}" 123 | ), 124 | )); 125 | } 126 | Ok(ext_len) => len = ext_len, 127 | Err(e) => { 128 | return Err(crate::Error::new( 129 | ErrorKind::Io(e), 130 | "Error reading extended atom length", 131 | )); 132 | } 133 | } 134 | true 135 | } else if len < 8 { 136 | return Err(crate::Error::new( 137 | crate::ErrorKind::InvalidAtomSize, 138 | format!("Read length of '{fourcc}' which is less than 8 bytes: {len}"), 139 | )); 140 | } else { 141 | false 142 | }; 143 | 144 | if len > remaining_bytes { 145 | return Err(crate::Error::new( 146 | ErrorKind::AtomSizeOutOfBounds, 147 | format!( 148 | "Atom size {len} of {fourcc} out larger than the remaining number of bytes {remaining_bytes}" 149 | ), 150 | )); 151 | } 152 | 153 | Ok(Head::new(ext, len, fourcc)) 154 | } 155 | 156 | pub fn write(writer: &mut impl Write, head: Head) -> crate::Result<()> { 157 | if head.ext { 158 | writer.write_be_u32(1)?; 159 | writer.write_all(&*head.fourcc)?; 160 | writer.write_be_u64(head.len())?; 161 | } else { 162 | writer.write_be_u32(head.len() as u32)?; 163 | writer.write_all(&*head.fourcc)?; 164 | } 165 | Ok(()) 166 | } 167 | 168 | /// Attempts to parse a full atom head. 169 | /// 170 | /// ```md 171 | /// 1 byte version 172 | /// 3 bytes flags 173 | /// ``` 174 | pub fn parse_full(reader: &mut impl Read) -> crate::Result<(u8, [u8; 3])> { 175 | let mut buf = [0; 4]; 176 | reader.read_exact(&mut buf).map_err(|e| { 177 | crate::Error::new( 178 | crate::ErrorKind::Io(e), 179 | "Error reading version and flags of full atom head", 180 | ) 181 | })?; 182 | let [version, flags @ ..] = buf; 183 | Ok((version, flags)) 184 | } 185 | 186 | pub fn write_full(writer: &mut impl Write, version: u8, flags: [u8; 3]) -> crate::Result<()> { 187 | writer.write_all(&[version])?; 188 | writer.write_all(&flags)?; 189 | Ok(()) 190 | } 191 | 192 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 193 | pub struct AtomBounds { 194 | pos: u64, 195 | size: Size, 196 | } 197 | 198 | impl Deref for AtomBounds { 199 | type Target = Size; 200 | 201 | fn deref(&self) -> &Self::Target { 202 | &self.size 203 | } 204 | } 205 | 206 | impl AtomBounds { 207 | pub const fn pos(&self) -> u64 { 208 | self.pos 209 | } 210 | 211 | pub fn content_pos(&self) -> u64 { 212 | self.pos + self.head_len() 213 | } 214 | 215 | pub fn end(&self) -> u64 { 216 | self.pos + self.len() 217 | } 218 | } 219 | 220 | pub fn find_bounds(reader: &mut impl Seek, size: Size) -> crate::Result { 221 | let pos = reader.stream_position()? - size.head_len(); 222 | Ok(AtomBounds { pos, size }) 223 | } 224 | -------------------------------------------------------------------------------- /src/atom/stbl.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 4 | pub struct Stbl { 5 | pub state: State, 6 | pub stsd: Option, 7 | pub stts: Option, 8 | pub stsc: Option, 9 | pub stsz: Option, 10 | pub stco: Option, 11 | pub co64: Option, 12 | } 13 | 14 | #[derive(Clone, Debug, PartialEq, Eq)] 15 | pub enum Table { 16 | Shallow { pos: u64, num_entries: u32 }, 17 | Full(Vec), 18 | } 19 | 20 | impl Default for Table { 21 | fn default() -> Self { 22 | Self::Full(Vec::new()) 23 | } 24 | } 25 | 26 | impl Table { 27 | pub fn len(&self) -> usize { 28 | match self { 29 | Table::Shallow { num_entries, .. } => *num_entries as usize, 30 | Table::Full(items) => items.len(), 31 | } 32 | } 33 | } 34 | 35 | impl Table { 36 | pub fn get_or_read<'a>( 37 | &'a self, 38 | reader: &mut (impl Read + Seek), 39 | ) -> Result, crate::Error> { 40 | match self { 41 | &Table::Shallow { pos, num_entries } => { 42 | reader.seek(SeekFrom::Start(pos))?; 43 | let items = Self::read_items(reader, num_entries)?; 44 | Ok(Cow::Owned(items)) 45 | } 46 | Table::Full(items) => Ok(Cow::Borrowed(items)), 47 | } 48 | } 49 | 50 | pub fn read_items(reader: &mut impl Read, num_entries: u32) -> Result, crate::Error> { 51 | let mut items = Vec::with_capacity(num_entries as usize); 52 | for _ in 0..num_entries { 53 | items.push(T::read_item(reader)?); 54 | } 55 | Ok(items) 56 | } 57 | } 58 | 59 | pub trait ReadItem: Sized + Clone { 60 | fn read_item(reader: &mut impl Read) -> std::io::Result; 61 | } 62 | 63 | impl ReadItem for u32 { 64 | fn read_item(reader: &mut impl Read) -> std::io::Result { 65 | reader.read_be_u32() 66 | } 67 | } 68 | 69 | impl ReadItem for u64 { 70 | fn read_item(reader: &mut impl Read) -> std::io::Result { 71 | reader.read_be_u64() 72 | } 73 | } 74 | 75 | impl ReadItem for SttsItem { 76 | fn read_item(reader: &mut impl Read) -> std::io::Result { 77 | Ok(SttsItem { 78 | sample_count: reader.read_be_u32()?, 79 | sample_duration: reader.read_be_u32()?, 80 | }) 81 | } 82 | } 83 | 84 | impl ReadItem for StscItem { 85 | fn read_item(reader: &mut impl Read) -> std::io::Result { 86 | Ok(StscItem { 87 | first_chunk: reader.read_be_u32()?, 88 | samples_per_chunk: reader.read_be_u32()?, 89 | sample_description_id: reader.read_be_u32()?, 90 | }) 91 | } 92 | } 93 | 94 | impl Atom for Stbl { 95 | const FOURCC: Fourcc = SAMPLE_TABLE; 96 | } 97 | 98 | impl ParseAtom for Stbl { 99 | fn parse_atom( 100 | reader: &mut (impl Read + Seek), 101 | cfg: &ParseConfig<'_>, 102 | size: Size, 103 | ) -> crate::Result { 104 | let bounds = find_bounds(reader, size)?; 105 | let mut stbl = Self { 106 | state: State::Existing(bounds), 107 | ..Default::default() 108 | }; 109 | let mut parsed_bytes = 0; 110 | 111 | while parsed_bytes < size.content_len() { 112 | let remaining_bytes = size.content_len() - parsed_bytes; 113 | let head = head::parse(reader, remaining_bytes)?; 114 | 115 | match head.fourcc() { 116 | SAMPLE_TABLE_SAMPLE_DESCRIPTION if cfg.write || cfg.cfg.read_audio_info => { 117 | stbl.stsd = Some(Stsd::parse(reader, cfg, head.size())?) 118 | } 119 | SAMPLE_TABLE_TIME_TO_SAMPLE if cfg.cfg.read_chapter_track => { 120 | stbl.stts = Some(Stts::parse(reader, cfg, head.size())?) 121 | } 122 | SAMPLE_TABLE_SAMPLE_TO_CHUNK if cfg.cfg.read_chapter_track => { 123 | stbl.stsc = Some(Stsc::parse(reader, cfg, head.size())?) 124 | } 125 | SAMPLE_TABLE_SAMPLE_SIZE if cfg.cfg.read_chapter_track => { 126 | stbl.stsz = Some(Stsz::parse(reader, cfg, head.size())?) 127 | } 128 | SAMPLE_TABLE_CHUNK_OFFSET if cfg.write || cfg.cfg.read_chapter_track => { 129 | stbl.stco = Some(Stco::parse(reader, cfg, head.size())?) 130 | } 131 | SAMPLE_TABLE_CHUNK_OFFSET_64 if cfg.write || cfg.cfg.read_chapter_track => { 132 | stbl.co64 = Some(Co64::parse(reader, cfg, head.size())?) 133 | } 134 | _ => reader.skip(head.content_len() as i64)?, 135 | } 136 | 137 | parsed_bytes += head.len(); 138 | } 139 | 140 | Ok(stbl) 141 | } 142 | } 143 | 144 | impl AtomSize for Stbl { 145 | fn size(&self) -> Size { 146 | let content_len = self.stsd.len_or_zero() 147 | + self.stts.len_or_zero() 148 | + self.stsc.len_or_zero() 149 | + self.stsz.len_or_zero() 150 | + self.stco.len_or_zero() 151 | + self.co64.len_or_zero(); 152 | Size::from(content_len) 153 | } 154 | } 155 | 156 | impl WriteAtom for Stbl { 157 | fn write_atom(&self, writer: &mut impl Write, changes: &[Change<'_>]) -> crate::Result<()> { 158 | self.write_head(writer)?; 159 | if let Some(a) = &self.stsd { 160 | a.write(writer, changes)?; 161 | } 162 | if let Some(a) = &self.stts { 163 | a.write(writer, changes)?; 164 | } 165 | if let Some(a) = &self.stsc { 166 | a.write(writer, changes)?; 167 | } 168 | if let Some(a) = &self.stsz { 169 | a.write(writer, changes)?; 170 | } 171 | if let Some(a) = &self.stco { 172 | a.write(writer, changes)?; 173 | } 174 | if let Some(a) = &self.co64 { 175 | a.write(writer, changes)?; 176 | } 177 | Ok(()) 178 | } 179 | } 180 | 181 | impl SimpleCollectChanges for Stbl { 182 | fn state(&self) -> &State { 183 | &self.state 184 | } 185 | 186 | fn existing<'a>( 187 | &'a self, 188 | level: u8, 189 | bounds: &'a AtomBounds, 190 | changes: &mut Vec>, 191 | ) -> i64 { 192 | self.stsd.collect_changes(bounds.end(), level, changes) 193 | + self.stts.collect_changes(bounds.end(), level, changes) 194 | + self.stsc.collect_changes(bounds.end(), level, changes) 195 | + self.stsz.collect_changes(bounds.end(), level, changes) 196 | + self.stco.collect_changes(bounds.end(), level, changes) 197 | + self.co64.collect_changes(bounds.end(), level, changes) 198 | } 199 | 200 | fn atom_ref(&self) -> AtomRef<'_> { 201 | AtomRef::Stbl(self) 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/tag/userdata/genre.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::{Data, Userdata, ident}; 4 | 5 | /// A list of standard genre codes and values found in the `gnre` atom. The codes are equivalent to 6 | /// the ID3v1 genre codes plus 1. 7 | pub const STANDARD_GENRES: [&str; 80] = [ 8 | "Blues", 9 | "Classic rock", 10 | "Country", 11 | "Dance", 12 | "Disco", 13 | "Funk", 14 | "Grunge", 15 | "Hip,-Hop", 16 | "Jazz", 17 | "Metal", 18 | "New Age", 19 | "Oldies", 20 | "Other", 21 | "Pop", 22 | "Rhythm and Blues", 23 | "Rap", 24 | "Reggae", 25 | "Rock", 26 | "Techno", 27 | "Industrial", 28 | "Alternative", 29 | "Ska", 30 | "Death metal", 31 | "Pranks", 32 | "Soundtrack", 33 | "Euro-Techno", 34 | "Ambient", 35 | "Trip-Hop", 36 | "Vocal", 37 | "Jazz & Funk", 38 | "Fusion", 39 | "Trance", 40 | "Classical", 41 | "Instrumental", 42 | "Acid", 43 | "House", 44 | "Game", 45 | "Sound clip", 46 | "Gospel", 47 | "Noise", 48 | "Alternative Rock", 49 | "Bass", 50 | "Soul", 51 | "Punk", 52 | "Space", 53 | "Meditative", 54 | "Instrumental Pop", 55 | "Instrumental Rock", 56 | "Ethnic", 57 | "Gothic", 58 | "Darkwave", 59 | "Techno-Industrial", 60 | "Electronic", 61 | "Pop-Folk", 62 | "Eurodance", 63 | "Dream", 64 | "Southern Rock", 65 | "Comedy", 66 | "Cult", 67 | "Gangsta", 68 | "Top 41", 69 | "Christian Rap", 70 | "Pop/Funk", 71 | "Jungle", 72 | "Native US", 73 | "Cabaret", 74 | "New Wave", 75 | "Psychedelic", 76 | "Rave", 77 | "Show tunes", 78 | "Trailer", 79 | "Lo,-Fi", 80 | "Tribal", 81 | "Acid Punk", 82 | "Acid Jazz", 83 | "Polka", 84 | "Retro", 85 | "Musical", 86 | "Rock ’n’ Roll", 87 | "Hard Rock", 88 | ]; 89 | 90 | /// ### Standard genre 91 | impl Userdata { 92 | /// Returns all standard genres (`gnre`). 93 | pub fn standard_genres(&self) -> impl Iterator + '_ { 94 | self.bytes_of(&ident::STANDARD_GENRE).filter_map(|v| { 95 | let &[b0, b1] = v else { return None }; 96 | Some(u16::from_be_bytes([b0, b1])) 97 | }) 98 | } 99 | 100 | /// Returns the first standard genre (`gnre`). 101 | pub fn standard_genre(&self) -> Option { 102 | self.standard_genres().next() 103 | } 104 | 105 | /// Sets the standard genre (`gnre`). This will remove all other standard genres. 106 | pub fn set_standard_genre(&mut self, genre_code: u16) { 107 | let vec: Vec = genre_code.to_be_bytes().to_vec(); 108 | self.set_data(ident::STANDARD_GENRE, Data::Reserved(vec)); 109 | } 110 | 111 | /// Sets all standard genres (`gnre`). This will remove all other standard genres. 112 | pub fn set_standard_genres(&mut self, genre_codes: impl IntoIterator) { 113 | let data = genre_codes.into_iter().map(|c| Data::Reserved(c.to_be_bytes().to_vec())); 114 | self.set_all_data(ident::STANDARD_GENRE, data); 115 | } 116 | 117 | /// Adds a standard genre (`gnre`). 118 | pub fn add_standard_genre(&mut self, genre_code: u16) { 119 | let vec: Vec = genre_code.to_be_bytes().to_vec(); 120 | self.add_data(ident::STANDARD_GENRE, Data::Reserved(vec)) 121 | } 122 | 123 | /// Adds all standard genres (`gnre`). 124 | pub fn add_standard_genres(&mut self, genre_codes: impl IntoIterator) { 125 | let data = genre_codes.into_iter().map(|c| Data::Reserved(c.to_be_bytes().to_vec())); 126 | self.add_all_data(ident::STANDARD_GENRE, data) 127 | } 128 | 129 | /// Removes all standard genres (`gnre`). 130 | pub fn remove_standard_genres(&mut self) { 131 | self.remove_data_of(&ident::STANDARD_GENRE); 132 | } 133 | } 134 | 135 | /// ### Genre 136 | /// 137 | /// These are convenience methods that operate on values of both standard genres (`gnre`) and 138 | /// custom genres (`©gen`). 139 | impl Userdata { 140 | /// Returns all genres, first the standard genres (`gnre`) then custom ones (`©gen`). 141 | pub fn genres(&self) -> impl Iterator + '_ { 142 | #[allow(clippy::redundant_closure)] 143 | self.standard_genres().filter_map(|c| standard_genre(c)).chain(self.custom_genres()) 144 | } 145 | 146 | /// Returns the first genre (`gnre` or `©gen`). 147 | pub fn genre(&self) -> Option<&str> { 148 | if let Some(g) = self.standard_genre().and_then(standard_genre) { 149 | return Some(g); 150 | } 151 | 152 | self.custom_genre() 153 | } 154 | 155 | /// Removes all custom genres (`©gen`) and returns all genres, first standard genres (`gnre`) 156 | /// then custom ones (`©gen`). 157 | pub fn take_genres(&mut self) -> impl Iterator + '_ { 158 | self.standard_genres() 159 | .filter_map(standard_genre) 160 | .map(str::to_owned) 161 | .collect::>() 162 | .into_iter() 163 | .chain(self.take_custom_genres()) 164 | } 165 | 166 | /// Removes all custom genres (`©gen`) and returns the first genre (`gnre` or `©gen`). 167 | pub fn take_genre(&mut self) -> Option { 168 | if let Some(g) = self.standard_genre().and_then(standard_genre) { 169 | return Some(g.to_owned()); 170 | } 171 | 172 | self.take_custom_genre() 173 | } 174 | 175 | /// Sets the custom genre (`©gen`). This will remove all other standard or custom genres. 176 | pub fn set_genre(&mut self, genre: impl Into) { 177 | self.set_custom_genre(genre.into()); 178 | self.remove_standard_genres(); 179 | } 180 | 181 | /// Sets the custom genre (`©gen`). This will remove all other standard or custom genres. 182 | pub fn set_genres(&mut self, genres: impl IntoIterator) { 183 | self.set_custom_genres(genres); 184 | self.remove_standard_genres(); 185 | } 186 | 187 | /// Adds a custom genre (`©gen`). 188 | pub fn add_genre(&mut self, genre: impl Into) { 189 | self.add_custom_genre(genre.into()); 190 | } 191 | 192 | /// Removes the genre (`gnre` or `©gen`). 193 | pub fn remove_genres(&mut self) { 194 | self.remove_standard_genres(); 195 | self.remove_custom_genres(); 196 | } 197 | 198 | /// Returns all genres formatted in an easily readable way. 199 | pub(crate) fn format_genres(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 200 | if self.genres().count() > 1 { 201 | writeln!(f, "genres:")?; 202 | for v in self.genres() { 203 | writeln!(f, " {v}")?; 204 | } 205 | } else if let Some(s) = self.genre() { 206 | writeln!(f, "genre: {s}")?; 207 | } 208 | Ok(()) 209 | } 210 | } 211 | 212 | fn standard_genre(code: u16) -> Option<&'static str> { 213 | let c = code as usize; 214 | if c > 0 && c <= STANDARD_GENRES.len() { 215 | return Some(STANDARD_GENRES[c - 1]); 216 | } 217 | 218 | None 219 | } 220 | -------------------------------------------------------------------------------- /src/atom/mp4a.rs: -------------------------------------------------------------------------------- 1 | //! mp4a atom 2 | //! 3 | //! ```md 4 | //! 4 bytes ? 5 | //! 2 bytes ? 6 | //! 2 bytes data reference index 7 | //! 8 bytes ? 8 | //! 2 bytes channel count 9 | //! 2 bytes sample size 10 | //! 4 bytes ? 11 | //! 4 bytes sample rate 12 | //! │ 13 | //! └─ esds atom 14 | //! 4 bytes len 15 | //! 4 bytes ident 16 | //! 1 byte version 17 | //! 3 bytes flags 18 | //! │ 19 | //! └─ elementary stream descriptor 20 | //! 1 byte tag (0x03) 21 | //! 1~4 bytes len 22 | //! 2 bytes id 23 | //! 1 byte flag 24 | //! │ 25 | //! ├─ decoder config descriptor 26 | //! │ 1 byte tag (0x04) 27 | //! │ 1~4 bytes len 28 | //! │ 1 byte object type indication 29 | //! │ 1 byte stream type 30 | //! │ 3 bytes buffer size 31 | //! │ 4 bytes maximum bitrate 32 | //! │ 4 bytes average bitrate 33 | //! │ │ 34 | //! │ └─ decoder specific descriptor 35 | //! │ 1 byte tag (0x05) 36 | //! │ 1~4 bytes len 37 | //! │ 5 bits profile 38 | //! │ 4 bits frequency index 39 | //! │ 4 bits channel config 40 | //! │ 3 bits ? 41 | //! │ 42 | //! └─ sl config descriptor 43 | //! 1 byte tag (0x06) 44 | //! 1~4 bytes len 45 | //! 1 byte ? 46 | //! ``` 47 | 48 | use std::cmp::min; 49 | 50 | use crate::{ChannelConfig, SampleRate}; 51 | 52 | use super::*; 53 | 54 | pub const HEADER_SIZE: u64 = 28; 55 | 56 | /// Es descriptor tag 57 | const ELEMENTARY_STREAM_DESCRIPTOR: u8 = 0x03; 58 | /// Decoder config descriptor tag 59 | const DECODER_CONFIG_DESCRIPTOR: u8 = 0x04; 60 | /// Decoder specific descriptor tag 61 | const DECODER_SPECIFIC_DESCRIPTOR: u8 = 0x05; 62 | 63 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 64 | pub struct Mp4a { 65 | pub channel_config: Option, 66 | pub sample_rate: Option, 67 | pub max_bitrate: Option, 68 | pub avg_bitrate: Option, 69 | } 70 | 71 | impl Atom for Mp4a { 72 | const FOURCC: Fourcc = MP4_AUDIO; 73 | } 74 | 75 | impl ParseAtom for Mp4a { 76 | fn parse_atom( 77 | reader: &mut (impl Read + Seek), 78 | _cfg: &ParseConfig<'_>, 79 | size: Size, 80 | ) -> crate::Result { 81 | let bounds = find_bounds(reader, size)?; 82 | let mut mp4a = Self::default(); 83 | 84 | // use cursor over a buffer to avoid syscalls 85 | let mut buf = vec![0; bounds.content_len() as usize]; 86 | reader.read_exact(&mut buf)?; 87 | 88 | let mut cursor = std::io::Cursor::new(&mut buf); 89 | cursor.skip(HEADER_SIZE as i64)?; 90 | 91 | let remaining_bytes = size.content_len() - HEADER_SIZE; 92 | let head = head::parse(&mut cursor, remaining_bytes)?; 93 | if head.fourcc() != ELEMENTARY_STREAM_DESCRIPTION { 94 | return Err(crate::Error::new( 95 | crate::ErrorKind::AtomNotFound(ELEMENTARY_STREAM_DESCRIPTION), 96 | "Missing esds atom", 97 | )); 98 | } 99 | 100 | parse_esds(&mut cursor, &mut mp4a, head.size())?; 101 | 102 | Ok(mp4a) 103 | } 104 | } 105 | 106 | /// esds atom 107 | /// 108 | /// ```md 109 | /// 4 bytes len 110 | /// 4 bytes ident 111 | /// 1 byte version 112 | /// 3 bytes flags 113 | /// │ 114 | /// └──elementary stream descriptor 115 | /// │ 116 | /// ├──decoder config descriptor 117 | /// │ │ 118 | /// │ └──decoder specific descriptor 119 | /// │ 120 | /// └──sl config descriptor 121 | /// ``` 122 | fn parse_esds(reader: &mut (impl Read + Seek), info: &mut Mp4a, size: Size) -> crate::Result<()> { 123 | let (version, _) = head::parse_full(reader)?; 124 | 125 | if version != 0 { 126 | return Err(crate::Error::new( 127 | crate::ErrorKind::UnknownVersion(version), 128 | "Unknown MPEG-4 audio (mp4a) version", 129 | )); 130 | } 131 | 132 | let (tag, head_len, desc_len) = parse_desc_head(reader)?; 133 | if tag != ELEMENTARY_STREAM_DESCRIPTOR { 134 | return Err(crate::Error::new( 135 | crate::ErrorKind::DescriptorNotFound(ELEMENTARY_STREAM_DESCRIPTOR), 136 | "Missing elementary stream descriptor", 137 | )); 138 | } 139 | 140 | let max_len = size.content_len() - 4 - head_len; 141 | parse_es_desc(reader, info, min(desc_len, max_len))?; 142 | 143 | Ok(()) 144 | } 145 | 146 | /// elementary stream descriptor 147 | /// 148 | /// ```md 149 | /// 1 byte tag (0x03) 150 | /// 1~4 bytes len 151 | /// 2 bytes id 152 | /// 1 byte flag 153 | /// │ 154 | /// ├──decoder config descriptor 155 | /// │ │ 156 | /// │ └──decoder specific descriptor 157 | /// │ 158 | /// └──sl config descriptor 159 | /// ``` 160 | fn parse_es_desc(reader: &mut (impl Read + Seek), info: &mut Mp4a, len: u64) -> crate::Result<()> { 161 | reader.skip(3)?; 162 | 163 | let mut parsed_bytes = 3; 164 | while parsed_bytes < len { 165 | let (tag, head_len, desc_len) = parse_desc_head(reader)?; 166 | 167 | match tag { 168 | DECODER_CONFIG_DESCRIPTOR => parse_dc_desc(reader, info, desc_len)?, 169 | _ => reader.skip(desc_len as i64)?, 170 | } 171 | 172 | parsed_bytes += head_len + desc_len; 173 | } 174 | 175 | Ok(()) 176 | } 177 | 178 | /// decoder config descriptor 179 | /// 180 | /// ```md 181 | /// 1 byte tag (0x04) 182 | /// 1~4 bytes len 183 | /// 1 byte object type indication 184 | /// 1 byte stream type 185 | /// 3 bytes buffer size 186 | /// 4 bytes maximum bitrate 187 | /// 4 bytes average bitrate 188 | /// │ 189 | /// └──decoder specific descriptor 190 | /// ``` 191 | fn parse_dc_desc(reader: &mut (impl Read + Seek), info: &mut Mp4a, len: u64) -> crate::Result<()> { 192 | reader.skip(5)?; 193 | info.max_bitrate = Some(reader.read_be_u32()?); 194 | info.avg_bitrate = Some(reader.read_be_u32()?); 195 | 196 | let mut parsed_bytes = 13; 197 | while parsed_bytes < len { 198 | let (tag, head_len, desc_len) = parse_desc_head(reader)?; 199 | 200 | match tag { 201 | DECODER_SPECIFIC_DESCRIPTOR => parse_ds_desc(reader, info, desc_len)?, 202 | _ => { 203 | reader.skip(desc_len as i64)?; 204 | } 205 | } 206 | 207 | parsed_bytes += head_len + desc_len; 208 | } 209 | 210 | Ok(()) 211 | } 212 | 213 | /// decoder specific descriptor 214 | /// 215 | /// ```md 216 | /// 1 byte tag (0x05) 217 | /// 1~4 bytes len 218 | /// 5 bits profile 219 | /// 4 bits frequency index 220 | /// 4 bits channel config 221 | /// 3 bits ? 222 | /// ``` 223 | fn parse_ds_desc(reader: &mut (impl Read + Seek), info: &mut Mp4a, len: u64) -> crate::Result<()> { 224 | let num = reader.read_be_u16()?; 225 | 226 | let freq_index = ((num >> 7) & 0x0F) as u8; 227 | info.sample_rate = SampleRate::try_from(freq_index).ok(); 228 | 229 | let channel_config = ((num >> 3) & 0x0F) as u8; 230 | info.channel_config = ChannelConfig::try_from(channel_config).ok(); 231 | 232 | reader.skip((len - 2) as i64)?; 233 | Ok(()) 234 | } 235 | 236 | fn parse_desc_head(reader: &mut impl Read) -> crate::Result<(u8, u64, u64)> { 237 | let tag = reader.read_u8()?; 238 | 239 | let mut head_len = 1; 240 | let mut len = 0; 241 | while head_len < 5 { 242 | let b = reader.read_u8()?; 243 | len = (len << 7) | (b & 0x7F) as u64; 244 | head_len += 1; 245 | if b & 0x80 == 0 { 246 | break; 247 | } 248 | } 249 | 250 | Ok((tag, head_len, len)) 251 | } 252 | -------------------------------------------------------------------------------- /src/atom/util.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Read, Seek, SeekFrom, Write}; 2 | use std::time::Duration; 3 | 4 | use crate::ErrorKind; 5 | use crate::atom::head::Size; 6 | 7 | pub trait ReadUtil: Read { 8 | /// Attempts to read an unsigned 8 bit integer from the reader. 9 | fn read_u8(&mut self) -> io::Result { 10 | let mut buf = [0]; 11 | self.read_exact(&mut buf)?; 12 | Ok(buf[0]) 13 | } 14 | 15 | /// Attempts to read an unsigned 16 bit big endian integer from the reader. 16 | fn read_be_u16(&mut self) -> io::Result { 17 | let mut buf = [0; 2]; 18 | self.read_exact(&mut buf)?; 19 | Ok(u16::from_be_bytes(buf)) 20 | } 21 | 22 | /// Attempts to read an unsigned 32 bit big endian integer from the reader. 23 | fn read_be_u32(&mut self) -> io::Result { 24 | let mut buf = [0; 4]; 25 | self.read_exact(&mut buf)?; 26 | Ok(u32::from_be_bytes(buf)) 27 | } 28 | 29 | /// Attempts to read an unsigned 64 bit big endian integer from the reader. 30 | fn read_be_u64(&mut self) -> io::Result { 31 | let mut buf = [0; 8]; 32 | self.read_exact(&mut buf)?; 33 | Ok(u64::from_be_bytes(buf)) 34 | } 35 | 36 | /// Attempts to read 8 bit unsigned integers from the reader to a vector of size length. 37 | fn read_u8_vec(&mut self, len: u64) -> io::Result> { 38 | let mut buf = vec![0; len as usize]; 39 | self.read_exact(&mut buf)?; 40 | Ok(buf) 41 | } 42 | 43 | /// Attempts to read a utf-8 string from the reader. 44 | fn read_utf8(&mut self, len: u64) -> crate::Result { 45 | let data = self.read_u8_vec(len)?; 46 | 47 | String::from_utf8(data) 48 | .map_err(|_| crate::Error::new(ErrorKind::Utf8StringDecoding, "invalid utf-8 data")) 49 | } 50 | 51 | /// Attempts to read a big endian utf-16 string from the reader. 52 | fn read_be_utf16(&mut self, len: u64) -> crate::Result { 53 | let data = self.read_u8_vec(len)?; 54 | let code_units = data.chunks_exact(2).map(|c| u16::from_be_bytes([c[0], c[1]])); 55 | let mut ret = String::with_capacity(data.len() / 2); 56 | decode_utf16(&mut ret, code_units)?; 57 | Ok(ret) 58 | } 59 | 60 | /// Attempts to read a little endian utf-16 string from the reader. 61 | fn read_le_utf16(&mut self, len: u64) -> crate::Result { 62 | let data = self.read_u8_vec(len)?; 63 | let code_units = data.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])); 64 | let mut ret = String::with_capacity(data.len() / 2); 65 | decode_utf16(&mut ret, code_units)?; 66 | Ok(ret) 67 | } 68 | } 69 | 70 | fn decode_utf16(buf: &mut String, code_units: impl Iterator) -> crate::Result<()> { 71 | for c in char::decode_utf16(code_units) { 72 | if let Ok(c) = c { 73 | buf.push(c); 74 | } else { 75 | return Err(crate::Error::new(ErrorKind::Utf16StringDecoding, "invalid utf-16 data")); 76 | } 77 | } 78 | Ok(()) 79 | } 80 | 81 | impl ReadUtil for T {} 82 | 83 | pub trait SeekUtil: Seek { 84 | fn skip(&mut self, offset: i64) -> io::Result<()> { 85 | self.seek(SeekFrom::Current(offset))?; 86 | Ok(()) 87 | } 88 | } 89 | 90 | impl SeekUtil for T {} 91 | 92 | pub trait WriteUtil: Write { 93 | fn write_u8(&mut self, val: u8) -> io::Result<()> { 94 | self.write_all(&[val]) 95 | } 96 | 97 | fn write_be_u16(&mut self, val: u16) -> io::Result<()> { 98 | self.write_all(&val.to_be_bytes()) 99 | } 100 | 101 | fn write_be_u32(&mut self, val: u32) -> io::Result<()> { 102 | self.write_all(&val.to_be_bytes()) 103 | } 104 | 105 | fn write_be_u64(&mut self, val: u64) -> io::Result<()> { 106 | self.write_all(&val.to_be_bytes()) 107 | } 108 | 109 | fn write_utf8(&mut self, string: &str) -> io::Result<()> { 110 | self.write_all(string.as_bytes()) 111 | } 112 | 113 | fn write_be_utf16(&mut self, string: &str) -> io::Result<()> { 114 | for c in string.encode_utf16() { 115 | self.write_be_u16(c)?; 116 | } 117 | Ok(()) 118 | } 119 | } 120 | 121 | pub fn expect_size(name: &str, head_size: Size, content_size: u64) -> crate::Result<()> { 122 | let head_content_size = head_size.content_len(); 123 | if content_size != head_content_size { 124 | return Err(crate::Error::new( 125 | ErrorKind::SizeMismatch, 126 | format!( 127 | "{name} size from atom head {head_content_size} differs from the content size {content_size}", 128 | ), 129 | )); 130 | } 131 | Ok(()) 132 | } 133 | 134 | pub fn expect_min_size(name: &str, head_size: Size, min_size: u64) -> crate::Result<()> { 135 | let head_content_size = head_size.content_len(); 136 | if head_content_size < min_size { 137 | return Err(crate::Error::new( 138 | ErrorKind::InvalidAtomSize, 139 | format!( 140 | "{name} size from atom head {head_content_size} is smaller than the minimum size {min_size}", 141 | ), 142 | )); 143 | } 144 | Ok(()) 145 | } 146 | 147 | pub fn unknown_version(name: &str, version: u8) -> crate::Result { 148 | Err(crate::Error::new( 149 | crate::ErrorKind::UnknownVersion(version), 150 | format!("Unknown {name} atom version {version}"), 151 | )) 152 | } 153 | 154 | impl WriteUtil for T {} 155 | 156 | pub fn scale_duration(timescale: u32, duration: u64) -> Duration { 157 | let secs = duration / timescale as u64; 158 | let nanos = (duration % timescale as u64) * 1_000_000_000 / timescale as u64; 159 | Duration::new(secs, nanos as u32) 160 | } 161 | 162 | pub fn unscale_duration(timescale: u32, duration: Duration) -> u64 { 163 | let secs = duration.as_secs() * timescale as u64; 164 | let nanos = duration.subsec_nanos() as u64 * timescale as u64 / 1_000_000_000; 165 | secs + nanos 166 | } 167 | 168 | /// Attempts to read a big endian integer at the specified index from a byte slice. 169 | macro_rules! be_int { 170 | ($bytes:expr, $index:expr, $type:ty) => {{ 171 | use std::convert::TryFrom; 172 | 173 | const SIZE: usize = std::mem::size_of::<$type>(); 174 | let bytes_start = ($index); 175 | let bytes_end = ($index) + SIZE; 176 | 177 | if $bytes.len() < bytes_end { 178 | None 179 | } else { 180 | let be_bytes = <[u8; SIZE]>::try_from(&$bytes[bytes_start..bytes_end]); 181 | 182 | match be_bytes { 183 | Ok(b) => Some(<$type>::from_be_bytes(b)), 184 | Err(_) => None, 185 | } 186 | } 187 | }}; 188 | } 189 | 190 | /// Attempts to write a big endian integer at the specified index to a byte vector. 191 | macro_rules! set_be_int { 192 | ($bytes:expr, $index:expr, $value:expr, $type:ty) => {{ 193 | const SIZE: usize = std::mem::size_of::<$type>(); 194 | let bytes_start = ($index); 195 | let bytes_end = ($index) + SIZE; 196 | 197 | let be_bytes = <$type>::to_be_bytes($value); 198 | 199 | if $bytes.len() < bytes_end { 200 | $bytes.resize(bytes_end, 0); 201 | } 202 | 203 | for i in 0..SIZE { 204 | $bytes[bytes_start + i] = be_bytes[i]; 205 | } 206 | }}; 207 | } 208 | 209 | #[macro_export] 210 | macro_rules! const_assert { 211 | ($x:expr $(,)?) => { 212 | #[allow(unknown_lints, clippy::eq_op)] 213 | const _: [(); 0 - !{ 214 | const ASSERT: bool = $x; 215 | ASSERT 216 | } as usize] = []; 217 | }; 218 | } 219 | 220 | #[cfg(test)] 221 | mod test { 222 | #[test] 223 | fn be_int() { 224 | let bytes = [0x00, 0x00, 0x00, 0x00, 0x2D, 0x34, 0xD0, 0x5E]; 225 | let int = be_int!(bytes, 4, u32); 226 | assert_eq!(int, Some(758435934u32)); 227 | } 228 | 229 | #[test] 230 | fn set_be_int() { 231 | let mut bytes = vec![0, 0, 0, 0, 0, 0, 0, 0]; 232 | set_be_int!(bytes, 4, 524, u16); 233 | assert_eq!(bytes[4], 2); 234 | assert_eq!(bytes[5], 12); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/tag/userdata/tuple.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::{Data, Userdata, ident}; 4 | 5 | /// ### Track 6 | /// 7 | /// The track number and total number of tracks are stored in a tuple. If only one is present the 8 | /// other is represented as 0 and will be treated as if nonexistent. 9 | impl Userdata { 10 | /// Returns the track number and the total number of tracks (`trkn`). 11 | pub fn track(&self) -> (Option, Option) { 12 | let vec = match self.bytes_of(&ident::TRACK_NUMBER).next() { 13 | Some(v) => v, 14 | None => return (None, None), 15 | }; 16 | 17 | (number(vec), total(vec)) 18 | } 19 | 20 | /// Returns the track number (`trkn`). 21 | pub fn track_number(&self) -> Option { 22 | let vec = self.bytes_of(&ident::TRACK_NUMBER).next()?; 23 | number(vec) 24 | } 25 | 26 | /// Returns the total number of tracks (`trkn`). 27 | pub fn total_tracks(&self) -> Option { 28 | let vec = self.bytes_of(&ident::TRACK_NUMBER).next()?; 29 | total(vec) 30 | } 31 | 32 | fn set_new_track(&mut self, track_number: u16, total_tracks: u16) { 33 | let vec = new(track_number, total_tracks); 34 | self.set_data(ident::TRACK_NUMBER, Data::Reserved(vec)); 35 | } 36 | 37 | /// Sets the track number and the total number of tracks (`trkn`). 38 | pub fn set_track(&mut self, track_number: u16, total_tracks: u16) { 39 | let vec = self.bytes_mut_of(&ident::TRACK_NUMBER).next(); 40 | match vec { 41 | Some(v) => { 42 | set_total(v, total_tracks); 43 | set_number(v, track_number); 44 | } 45 | None => self.set_new_track(track_number, total_tracks), 46 | } 47 | } 48 | 49 | /// Sets the track number (`trkn`). 50 | pub fn set_track_number(&mut self, track_number: u16) { 51 | let vec = self.bytes_mut_of(&ident::TRACK_NUMBER).next(); 52 | match vec { 53 | Some(v) => set_number(v, track_number), 54 | None => self.set_new_track(track_number, 0), 55 | } 56 | } 57 | 58 | /// Sets the total number of tracks (`trkn`). 59 | pub fn set_total_tracks(&mut self, total_tracks: u16) { 60 | let vec = self.bytes_mut_of(&ident::TRACK_NUMBER).next(); 61 | match vec { 62 | Some(v) => set_total(v, total_tracks), 63 | None => self.set_new_track(0, total_tracks), 64 | } 65 | } 66 | 67 | /// Removes the track number and the total number of tracks (`trkn`). 68 | pub fn remove_track(&mut self) { 69 | self.remove_data_of(&ident::TRACK_NUMBER); 70 | } 71 | 72 | /// Removes the track number, preserving the total number of tracks if present (`trkn`). 73 | pub fn remove_track_number(&mut self) { 74 | let vec = self.bytes_mut_of(&ident::TRACK_NUMBER).next(); 75 | match vec { 76 | Some(v) if total(v) != Some(0) => set_number(v, 0), 77 | _ => self.remove_track(), 78 | } 79 | } 80 | 81 | /// Removes the total number of tracks, preserving the track number if present (`trkn`). 82 | pub fn remove_total_tracks(&mut self) { 83 | let vec = self.bytes_mut_of(&ident::TRACK_NUMBER).next(); 84 | match vec { 85 | Some(v) if number(v) != Some(0) => set_total(v, 0), 86 | _ => self.remove_track(), 87 | } 88 | } 89 | 90 | /// Returns the track numer and total number of tracks formatted in an easily readable way. 91 | pub(crate) fn format_track(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 92 | match self.track() { 93 | (Some(n), Some(t)) => writeln!(f, "track: {n} of {t}"), 94 | (Some(n), None) => writeln!(f, "track: {n}"), 95 | (None, Some(t)) => writeln!(f, "track: ? of {t}"), 96 | (None, None) => Ok(()), 97 | } 98 | } 99 | } 100 | 101 | /// ### Disc 102 | /// 103 | /// The disc number and total number of discs are stored in a tuple. If only one is present the 104 | /// other is represented as 0 and will be treated as if nonexistent. 105 | impl Userdata { 106 | /// Returns the disc number and total number of discs (`disk`). 107 | pub fn disc(&self) -> (Option, Option) { 108 | let vec = match self.bytes_of(&ident::DISC_NUMBER).next() { 109 | Some(v) => v, 110 | None => return (None, None), 111 | }; 112 | 113 | (number(vec), total(vec)) 114 | } 115 | 116 | /// Returns the disc number (`disk`). 117 | pub fn disc_number(&self) -> Option { 118 | let vec = self.bytes_of(&ident::DISC_NUMBER).next()?; 119 | number(vec) 120 | } 121 | 122 | /// Returns the total number of discs (`disk`). 123 | pub fn total_discs(&self) -> Option { 124 | let vec = self.bytes_of(&ident::DISC_NUMBER).next()?; 125 | total(vec) 126 | } 127 | 128 | fn set_new_disc(&mut self, disc_number: u16, total_discs: u16) { 129 | let vec = new(disc_number, total_discs); 130 | self.set_data(ident::DISC_NUMBER, Data::Reserved(vec)); 131 | } 132 | 133 | /// Sets the disc number and the total number of discs (`disk`). 134 | pub fn set_disc(&mut self, disc_number: u16, total_discs: u16) { 135 | let vec = self.bytes_mut_of(&ident::DISC_NUMBER).next(); 136 | match vec { 137 | Some(v) => { 138 | set_total(v, total_discs); 139 | set_number(v, disc_number); 140 | } 141 | None => self.set_new_disc(disc_number, total_discs), 142 | } 143 | } 144 | 145 | /// Sets the disc number (`disk`). 146 | pub fn set_disc_number(&mut self, disc_number: u16) { 147 | let vec = self.bytes_mut_of(&ident::DISC_NUMBER).next(); 148 | match vec { 149 | Some(v) => set_number(v, disc_number), 150 | None => self.set_new_disc(disc_number, 0), 151 | } 152 | } 153 | 154 | /// Sets the total number of discs (`disk`). 155 | pub fn set_total_discs(&mut self, total_discs: u16) { 156 | let vec = self.bytes_mut_of(&ident::DISC_NUMBER).next(); 157 | match vec { 158 | Some(v) => set_total(v, total_discs), 159 | None => self.set_new_disc(0, total_discs), 160 | } 161 | } 162 | 163 | /// Removes the disc number and the total number of discs (`disk`). 164 | pub fn remove_disc(&mut self) { 165 | self.remove_data_of(&ident::DISC_NUMBER); 166 | } 167 | 168 | /// Removes the disc number, preserving the total number of discs if present (`disk`). 169 | pub fn remove_disc_number(&mut self) { 170 | let vec = self.bytes_mut_of(&ident::DISC_NUMBER).next(); 171 | match vec { 172 | Some(v) if total(v) != Some(0) => set_number(v, 0), 173 | _ => self.remove_disc(), 174 | } 175 | } 176 | 177 | /// Removes the total number of discs, preserving the disc number if present (`disk`). 178 | pub fn remove_total_discs(&mut self) { 179 | let vec = self.bytes_mut_of(&ident::DISC_NUMBER).next(); 180 | match vec { 181 | Some(v) if number(v) != Some(0) => set_total(v, 0), 182 | _ => self.remove_disc(), 183 | } 184 | } 185 | 186 | /// Returns the disc numer and total number of discs formatted in an easily readable way. 187 | pub(crate) fn format_disc(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 188 | match self.disc() { 189 | (Some(d), Some(t)) => writeln!(f, "disc: {d} of {t}"), 190 | (Some(d), None) => writeln!(f, "disc: {d}"), 191 | (None, Some(t)) => writeln!(f, "disc: ? of {t}"), 192 | (None, None) => Ok(()), 193 | } 194 | } 195 | } 196 | 197 | fn number(vec: &[u8]) -> Option { 198 | be_int!(vec, 2, u16).and_then(|n| if n == 0 { None } else { Some(n) }) 199 | } 200 | 201 | fn total(vec: &[u8]) -> Option { 202 | be_int!(vec, 4, u16).and_then(|n| if n == 0 { None } else { Some(n) }) 203 | } 204 | 205 | fn set_number(vec: &mut Vec, number: u16) { 206 | set_be_int!(vec, 2, number, u16); 207 | check_correct_size(vec); 208 | } 209 | 210 | fn set_total(vec: &mut Vec, total: u16) { 211 | set_be_int!(vec, 4, total, u16); 212 | check_correct_size(vec); 213 | } 214 | 215 | // NOTE: iTunes/Apple Music requires the atom size to be 8 bytes for correct parsing. 216 | // Smaller sizes 6 bytes for example will cause parsing issues. 217 | fn check_correct_size(vec: &mut Vec) { 218 | if vec.len() < 8 { 219 | vec.resize(8, 0); 220 | } 221 | } 222 | 223 | fn new(number: u16, total: u16) -> Vec { 224 | let [n0, n1] = number.to_be_bytes(); 225 | let [t0, t1] = total.to_be_bytes(); 226 | vec![0, 0, n0, n1, t0, t1, 0, 0] 227 | } 228 | -------------------------------------------------------------------------------- /tests/handling.rs: -------------------------------------------------------------------------------- 1 | use mp4ameta::{Data, Img, STANDARD_GENRES, Tag, ident}; 2 | 3 | #[test] 4 | fn multiple_value_handling() { 5 | let mut tag = Tag::default(); 6 | 7 | tag.add_artist("1"); 8 | tag.add_artist("2"); 9 | tag.add_artist("3"); 10 | tag.add_artist("4"); 11 | 12 | assert_eq!(tag.artist(), Some("1")); 13 | { 14 | let mut artists = tag.artists(); 15 | assert_eq!(artists.next(), Some("1")); 16 | assert_eq!(artists.next(), Some("2")); 17 | assert_eq!(artists.next(), Some("3")); 18 | assert_eq!(artists.next(), Some("4")); 19 | assert_eq!(artists.next(), None); 20 | } 21 | 22 | tag.set_artist("5"); 23 | 24 | assert_eq!(tag.artist(), Some("5")); 25 | { 26 | let mut artists = tag.artists(); 27 | assert_eq!(artists.next(), Some("5")); 28 | assert_eq!(artists.next(), None); 29 | } 30 | 31 | tag.add_artist("6"); 32 | tag.add_artist("7"); 33 | 34 | assert_eq!(tag.artist(), Some("5")); 35 | { 36 | let mut artists = tag.artists(); 37 | assert_eq!(artists.next(), Some("5")); 38 | assert_eq!(artists.next(), Some("6")); 39 | assert_eq!(artists.next(), Some("7")); 40 | assert_eq!(artists.next(), None); 41 | } 42 | 43 | tag.remove_artists(); 44 | 45 | assert_eq!(tag.artists().next(), None); 46 | assert_eq!(tag.artist(), None); 47 | } 48 | 49 | #[test] 50 | fn genre_handling() { 51 | let mut tag = Tag::default(); 52 | assert_eq!(tag.genre(), None); 53 | assert_eq!(tag.standard_genre(), None); 54 | assert_eq!(tag.custom_genre(), None); 55 | 56 | let standard_name = STANDARD_GENRES[38]; 57 | tag.set_genre(standard_name); 58 | assert_eq!(tag.genre(), Some(standard_name)); 59 | assert_eq!(tag.standard_genre(), None); 60 | assert_eq!(tag.custom_genre(), Some(standard_name)); 61 | 62 | tag.set_genre("CUSTOM GENRE"); 63 | assert_eq!(tag.genre(), Some("CUSTOM GENRE")); 64 | assert_eq!(tag.standard_genre(), None); 65 | assert_eq!(tag.custom_genre(), Some("CUSTOM GENRE")); 66 | 67 | tag.remove_genres(); 68 | assert_eq!(tag.genre(), None); 69 | assert_eq!(tag.genres().next(), None); 70 | 71 | let (code1, name1) = (7, STANDARD_GENRES[6]); 72 | let (code2, name2) = (24, STANDARD_GENRES[23]); 73 | tag.add_custom_genre("GENRE 1"); 74 | tag.add_standard_genre(code1); 75 | tag.add_custom_genre("GENRE 2"); 76 | tag.add_standard_genre(code2); 77 | 78 | { 79 | let mut genres = tag.genres(); 80 | assert_eq!(genres.next(), Some(name1)); 81 | assert_eq!(genres.next(), Some(name2)); 82 | assert_eq!(genres.next(), Some("GENRE 1")); 83 | assert_eq!(genres.next(), Some("GENRE 2")); 84 | assert_eq!(genres.next(), None); 85 | 86 | let mut standard_genres = tag.standard_genres(); 87 | assert_eq!(standard_genres.next(), Some(code1)); 88 | assert_eq!(standard_genres.next(), Some(code2)); 89 | assert_eq!(genres.next(), None); 90 | 91 | let mut custom_genres = tag.custom_genres(); 92 | assert_eq!(custom_genres.next(), Some("GENRE 1")); 93 | assert_eq!(custom_genres.next(), Some("GENRE 2")); 94 | assert_eq!(genres.next(), None); 95 | } 96 | 97 | tag.remove_standard_genres(); 98 | assert_eq!(tag.standard_genres().next(), None); 99 | assert_eq!(tag.genres().next(), Some("GENRE 1")); 100 | 101 | tag.remove_custom_genres(); 102 | assert_eq!(tag.custom_genres().next(), None); 103 | assert_eq!(tag.genres().next(), None); 104 | } 105 | 106 | #[test] 107 | fn track_disc_handling() { 108 | let track_number = 4u16; 109 | let total_tracks = 16u16; 110 | let disc_number = 2u16; 111 | let total_discs = 3u16; 112 | 113 | let mut tag = Tag::default(); 114 | assert_eq!(tag.track(), (None, None)); 115 | assert_eq!(tag.track_number(), None); 116 | assert_eq!(tag.total_tracks(), None); 117 | assert_eq!(tag.disc(), (None, None)); 118 | assert_eq!(tag.disc_number(), None); 119 | assert_eq!(tag.total_discs(), None); 120 | 121 | tag.set_track_number(track_number); 122 | tag.set_total_tracks(total_tracks); 123 | tag.set_disc_number(disc_number); 124 | tag.set_total_discs(total_discs); 125 | 126 | assert_eq!(tag.track(), (Some(track_number), Some(total_tracks))); 127 | assert_eq!(tag.track_number(), Some(track_number)); 128 | assert_eq!(tag.total_tracks(), Some(total_tracks)); 129 | assert_eq!(tag.disc(), (Some(disc_number), Some(total_discs))); 130 | assert_eq!(tag.disc_number(), Some(disc_number)); 131 | assert_eq!(tag.total_discs(), Some(total_discs)); 132 | 133 | tag.remove_track_number(); 134 | tag.remove_disc_number(); 135 | 136 | assert_eq!(tag.track(), (None, Some(total_tracks))); 137 | assert_eq!(tag.track_number(), None); 138 | assert_eq!(tag.total_tracks(), Some(total_tracks)); 139 | assert_eq!(tag.disc(), (None, Some(total_discs))); 140 | assert_eq!(tag.disc_number(), None); 141 | assert_eq!(tag.total_discs(), Some(total_discs)); 142 | 143 | tag.remove_total_tracks(); 144 | tag.remove_total_discs(); 145 | 146 | assert_eq!(tag.track(), (None, None)); 147 | assert_eq!(tag.track_number(), None); 148 | assert_eq!(tag.total_tracks(), None); 149 | assert_eq!(tag.disc(), (None, None)); 150 | assert_eq!(tag.disc_number(), None); 151 | assert_eq!(tag.total_discs(), None); 152 | 153 | // Test if track number atom is corrected to the right size when edited. 154 | tag.set_data(ident::TRACK_NUMBER, Data::Reserved(vec![0, 0, 0, 1])); 155 | tag.set_total_tracks(2); 156 | assert_eq!(tag.track(), (Some(1), Some(2))); 157 | assert_eq!( 158 | tag.data_of(&ident::TRACK_NUMBER).next(), 159 | Some(&Data::Reserved(vec![0, 0, 0, 1, 0, 2, 0, 0])) 160 | ); 161 | } 162 | 163 | #[test] 164 | fn work_movement_handling() { 165 | let movement = "TEST MOVEMENT"; 166 | let index = 1; 167 | let count = 8; 168 | let work = "TEST WORK"; 169 | 170 | let mut tag = Tag::default(); 171 | assert_eq!(tag.movement(), None); 172 | assert_eq!(tag.movement_count(), None); 173 | assert_eq!(tag.movement_index(), None); 174 | assert_eq!(tag.show_movement(), false); 175 | assert_eq!(tag.work(), None); 176 | 177 | tag.set_movement(movement); 178 | tag.set_movement_count(count); 179 | tag.set_movement_index(index); 180 | tag.set_show_movement(); 181 | tag.set_work(work); 182 | 183 | assert_eq!(tag.movement(), Some(movement)); 184 | assert_eq!(tag.movement_count(), Some(count)); 185 | assert_eq!(tag.movement_index(), Some(index)); 186 | assert_eq!(tag.show_movement(), true); 187 | assert_eq!(tag.work(), Some(work)); 188 | } 189 | 190 | #[test] 191 | fn tag_destructuring() { 192 | let mut tag = Tag::default(); 193 | 194 | tag.set_album("TEST ALBUM"); 195 | tag.set_album_artist("TEST ALBUM ARTIST"); 196 | tag.set_artist("TEST ARTIST"); 197 | tag.set_category("TEST CATEGORY"); 198 | tag.set_comment("TEST COMMENT"); 199 | tag.set_composer("TEST COMPOSER"); 200 | tag.set_copyright("TEST COPYRIGHT"); 201 | tag.set_description("TEST DESCRIPTION"); 202 | tag.set_encoder("Lavf58.29.100"); 203 | tag.set_genre("TEST GENRE"); 204 | tag.set_grouping("TEST GROUPING"); 205 | tag.set_keyword("TEST KEYWORD"); 206 | tag.set_lyrics("TEST LYRICS"); 207 | tag.set_title("TEST TITLE"); 208 | tag.set_year("2013"); 209 | tag.set_artwork(Img::png(b"TEST ARTWORK".to_vec())); 210 | 211 | assert_eq!(tag.take_album(), Some("TEST ALBUM".to_string())); 212 | assert_eq!(tag.take_album_artist(), Some("TEST ALBUM ARTIST".to_string())); 213 | assert_eq!(tag.take_artist(), Some("TEST ARTIST".to_string())); 214 | assert_eq!(tag.take_category(), Some("TEST CATEGORY".to_string())); 215 | assert_eq!(tag.take_comment(), Some("TEST COMMENT".to_string())); 216 | assert_eq!(tag.take_composer(), Some("TEST COMPOSER".to_string())); 217 | assert_eq!(tag.take_copyright(), Some("TEST COPYRIGHT".to_string())); 218 | assert_eq!(tag.take_description(), Some("TEST DESCRIPTION".to_string())); 219 | assert_eq!(tag.take_encoder(), Some("Lavf58.29.100".to_string())); 220 | assert_eq!(tag.take_genre(), Some("TEST GENRE".to_string())); 221 | assert_eq!(tag.take_grouping(), Some("TEST GROUPING".to_string())); 222 | assert_eq!(tag.take_keyword(), Some("TEST KEYWORD".to_string())); 223 | assert_eq!(tag.take_lyrics(), Some("TEST LYRICS".to_string())); 224 | assert_eq!(tag.take_title(), Some("TEST TITLE".to_string())); 225 | assert_eq!(tag.take_year(), Some("2013".to_string())); 226 | assert_eq!(tag.take_artwork(), Some(Img::png(b"TEST ARTWORK".to_vec()))); 227 | 228 | assert_eq!(tag.album(), None); 229 | assert_eq!(tag.album_artist(), None); 230 | assert_eq!(tag.artist(), None); 231 | assert_eq!(tag.category(), None); 232 | assert_eq!(tag.comment(), None); 233 | assert_eq!(tag.composer(), None); 234 | assert_eq!(tag.copyright(), None); 235 | assert_eq!(tag.description(), None); 236 | assert_eq!(tag.encoder(), None); 237 | assert_eq!(tag.genre(), None); 238 | assert_eq!(tag.grouping(), None); 239 | assert_eq!(tag.keyword(), None); 240 | assert_eq!(tag.lyrics(), None); 241 | assert_eq!(tag.title(), None); 242 | assert_eq!(tag.year(), None); 243 | assert_eq!(tag.artwork(), None); 244 | } 245 | -------------------------------------------------------------------------------- /gen/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | 3 | const INPUT: &str = include_str!("../../src/tag/userdata/generate.toml"); 4 | const OUTPUT_PATH: &str = "src/tag/userdata/generated.rs"; 5 | 6 | const HEADER: &str = "\ 7 | // IMPORTANT: This file is automatically generated! 8 | // Edit the `generate.toml` file and run the code generation from the repository root 9 | // with the following command: `cargo run --manifest-path=gen/Cargo.toml` 10 | 11 | use crate::{ident, Data, Userdata}; 12 | "; 13 | 14 | fn main() { 15 | let input: toml::Table = toml::from_str(INPUT).unwrap(); 16 | let accessors = input["accessors"].as_table().unwrap(); 17 | 18 | let mut output = String::from(HEADER); 19 | 20 | for [value_ident, atom_ident] in str_table_iter(&accessors["single_strings"]) { 21 | single_string_accessor(&mut output, value_ident, atom_ident); 22 | } 23 | for [value_ident, atom_ident] in str_table_iter(&accessors["multiple_strings"]) { 24 | multiple_strings_accessor(&mut output, value_ident, atom_ident); 25 | } 26 | for [value_ident, atom_ident] in str_table_iter(&accessors["bool_flags"]) { 27 | bool_flag_accessor(&mut output, value_ident, atom_ident); 28 | } 29 | for [value_ident, atom_ident] in str_table_iter(&accessors["u16_ints"]) { 30 | u16_int_accessor(&mut output, value_ident, atom_ident); 31 | } 32 | for [value_ident, atom_ident] in str_table_iter(&accessors["u32_ints"]) { 33 | u32_int_accessor(&mut output, value_ident, atom_ident); 34 | } 35 | 36 | std::fs::write(OUTPUT_PATH, &output).unwrap(); 37 | } 38 | 39 | fn str_table_iter(value: &toml::Value) -> impl Iterator { 40 | let table = value.as_table().unwrap(); 41 | table.iter().map(|(key, val)| { 42 | let val_str = val.as_str().unwrap(); 43 | [key, val_str] 44 | }) 45 | } 46 | 47 | fn base_values(value_ident: &str) -> (String, String, String) { 48 | let name = value_ident.replace('_', " "); 49 | 50 | let mut name_chars = name.chars(); 51 | let headline = name_chars.next().unwrap().to_uppercase().chain(name_chars).collect::(); 52 | 53 | let atom_ident = format!("ident::{}", value_ident.to_uppercase()); 54 | 55 | (name, headline, atom_ident) 56 | } 57 | 58 | pub fn single_string_accessor(output: &mut String, value_ident: &str, atom_ident_string: &str) { 59 | let (name, headline, atom_ident) = base_values(value_ident); 60 | 61 | _ = write!( 62 | output, 63 | " 64 | /// ### {hl} 65 | impl Userdata {{ 66 | /// Returns the {n} (`{ais}`). 67 | pub fn {vi}(&self) -> Option<&str> {{ 68 | self.strings_of(&{ai}).next() 69 | }} 70 | 71 | /// Removes and returns the {n} (`{ais}`). 72 | pub fn take_{vi}(&mut self) -> Option {{ 73 | self.take_strings_of(&{ai}).next() 74 | }} 75 | 76 | /// Sets the {n} (`{ais}`). 77 | pub fn set_{vi}(&mut self, {vi}: impl Into) {{ 78 | self.set_data({ai}, Data::Utf8({vi}.into())); 79 | }} 80 | 81 | /// Removes the {n} (`{ais}`). 82 | pub fn remove_{vi}(&mut self) {{ 83 | self.remove_data_of(&{ai}); 84 | }} 85 | 86 | /// Returns the {n} formatted in an easily readable way. 87 | #[allow(unused)] 88 | pub(crate) fn format_{vi}(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{ 89 | match self.{vi}() {{ 90 | Some(s) => writeln!(f, \"{n}: {{}}\", s), 91 | None => Ok(()), 92 | }} 93 | }} 94 | }} 95 | ", 96 | hl = headline, 97 | n = name, 98 | ais = atom_ident_string, 99 | vi = value_ident, 100 | ai = atom_ident, 101 | ); 102 | } 103 | 104 | pub fn multiple_strings_accessor( 105 | output: &mut String, 106 | value_ident: &str, 107 | atom_ident_string: &str, 108 | ) { 109 | let (name, headline, atom_ident) = base_values(value_ident); 110 | 111 | let mut value_ident_plural = value_ident.to_string(); 112 | if value_ident_plural.ends_with('y') { 113 | value_ident_plural.pop(); 114 | value_ident_plural.push_str("ies"); 115 | } else { 116 | value_ident_plural.push('s'); 117 | }; 118 | 119 | let name_plural = value_ident_plural.replace('_', " "); 120 | 121 | _ = write!( 122 | output, 123 | " 124 | /// ### {hl} 125 | impl Userdata {{ 126 | /// Returns all {np} (`{ais}`). 127 | pub fn {vip}(&self) -> impl Iterator {{ 128 | self.strings_of(&{ai}) 129 | }} 130 | 131 | /// Returns the first {n} (`{ais}`). 132 | pub fn {vi}(&self) -> Option<&str> {{ 133 | self.strings_of(&{ai}).next() 134 | }} 135 | 136 | /// Removes and returns all {np} (`{ais}`). 137 | pub fn take_{vip}(&mut self) -> impl Iterator + '_ {{ 138 | self.take_strings_of(&{ai}) 139 | }} 140 | 141 | /// Removes all and returns the first {n} (`{ais}`). 142 | pub fn take_{vi}(&mut self) -> Option {{ 143 | self.take_strings_of(&{ai}).next() 144 | }} 145 | 146 | /// Sets all {np} (`{ais}`). This will remove all other {np}. 147 | pub fn set_{vip}(&mut self, {vip}: impl IntoIterator) {{ 148 | let data = {vip}.into_iter().map(Data::Utf8); 149 | self.set_all_data({ai}, data); 150 | }} 151 | 152 | /// Sets the {n} (`{ais}`). This will remove all other {np}. 153 | pub fn set_{vi}(&mut self, {vi}: impl Into) {{ 154 | self.set_data({ai}, Data::Utf8({vi}.into())); 155 | }} 156 | 157 | /// Adds all {np} (`{ais}`). 158 | pub fn add_{vip}(&mut self, {vip}: impl IntoIterator) {{ 159 | let data = {vip}.into_iter().map(Data::Utf8); 160 | self.add_all_data({ai}, data); 161 | }} 162 | 163 | /// Adds an {n} (`{ais}`). 164 | pub fn add_{vi}(&mut self, {vi}: impl Into) {{ 165 | self.add_data({ai}, Data::Utf8({vi}.into())); 166 | }} 167 | 168 | /// Removes all {np} (`{ais}`). 169 | pub fn remove_{vip}(&mut self) {{ 170 | self.remove_data_of(&{ai}); 171 | }} 172 | 173 | /// Returns all {np} formatted in an easily readable way. 174 | #[allow(unused)] 175 | pub(crate) fn format_{vip}(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{ 176 | if self.{vip}().count() > 1 {{ 177 | writeln!(f, \"{np}:\")?; 178 | for s in self.{vip}() {{ 179 | writeln!(f, \" {{}}\", s)?; 180 | }} 181 | }} else if let Some(s) = self.{vi}() {{ 182 | writeln!(f, \"{n}: {{}}\", s)?; 183 | }} 184 | Ok(()) 185 | }} 186 | }} 187 | ", 188 | hl = headline, 189 | n = name, 190 | np = name_plural, 191 | ais = atom_ident_string, 192 | vi = value_ident, 193 | vip = value_ident_plural, 194 | ai = atom_ident, 195 | ); 196 | } 197 | 198 | pub fn bool_flag_accessor(output: &mut String, value_ident: &str, atom_ident_string: &str) { 199 | let (name, headline, atom_ident) = base_values(value_ident); 200 | 201 | _ = write!( 202 | output, 203 | " 204 | /// ### {hl} 205 | impl Userdata {{ 206 | /// Returns the {n} flag (`{ais}`). 207 | pub fn {vi}(&self) -> bool {{ 208 | let vec = match self.bytes_of(&{ai}).next() {{ 209 | Some(v) => v, 210 | None => return false, 211 | }}; 212 | vec.first().map(|&v| v == 1).unwrap_or(false) 213 | }} 214 | 215 | /// Sets the {n} flag to true (`{ais}`). 216 | pub fn set_{vi}(&mut self) {{ 217 | self.set_data({ai}, Data::BeSigned(vec![1])); 218 | }} 219 | 220 | /// Removes the {n} flag (`{ais}`). 221 | pub fn remove_{vi}(&mut self) {{ 222 | self.remove_data_of(&{ai}) 223 | }} 224 | 225 | /// Returns the {n} formatted in an easily readable way. 226 | #[allow(unused)] 227 | pub(crate) fn format_{vi}(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{ 228 | match self.{vi}() {{ 229 | true => writeln!(f, \"{n}\"), 230 | false => Ok(()), 231 | }} 232 | }} 233 | }} 234 | ", 235 | hl = headline, 236 | n = name, 237 | ais = atom_ident_string, 238 | vi = value_ident, 239 | ai = atom_ident, 240 | ); 241 | } 242 | 243 | pub fn u16_int_accessor(output: &mut String, value_ident: &str, atom_ident_string: &str) { 244 | let (name, headline, atom_ident) = base_values(value_ident); 245 | 246 | _ = write!( 247 | output, 248 | " 249 | /// ### {hl} 250 | impl Userdata {{ 251 | /// Returns the {n} (`{ais}`) 252 | pub fn {vi}(&self) -> Option {{ 253 | let vec = self.bytes_of(&{ai}).next()?; 254 | be_int!(vec, 0, u16) 255 | }} 256 | 257 | /// Sets the {n} (`{ais}`) 258 | pub fn set_{vi}(&mut self, {vi}: u16) {{ 259 | let vec: Vec = {vi}.to_be_bytes().to_vec(); 260 | self.set_data({ai}, Data::BeSigned(vec)); 261 | }} 262 | 263 | /// Removes the {n} (`{ais}`). 264 | pub fn remove_{vi}(&mut self) {{ 265 | self.remove_data_of(&{ai}); 266 | }} 267 | 268 | /// Returns the {n} formatted in an easily readable way. 269 | #[allow(unused)] 270 | pub(crate) fn format_{vi}(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{ 271 | match self.{vi}() {{ 272 | Some(s) => writeln!(f, \"{n}: {{}}\", s), 273 | None => Ok(()), 274 | }} 275 | }} 276 | }} 277 | ", 278 | hl = headline, 279 | n = name, 280 | ais = atom_ident_string, 281 | vi = value_ident, 282 | ai = atom_ident, 283 | ); 284 | } 285 | 286 | pub fn u32_int_accessor(output: &mut String, value_ident: &str, atom_ident_string: &str) { 287 | let (name, headline, atom_ident) = base_values(value_ident); 288 | 289 | _ = write!( 290 | output, 291 | " 292 | /// ### {hl} 293 | impl Userdata {{ 294 | /// Returns the {n} (`{ais}`) 295 | pub fn {vi}(&self) -> Option {{ 296 | let vec = self.bytes_of(&{ai}).next()?; 297 | be_int!(vec, 0, u32) 298 | }} 299 | 300 | /// Sets the {n} (`{ais}`) 301 | pub fn set_{vi}(&mut self, {vi}: u32) {{ 302 | let vec: Vec = {vi}.to_be_bytes().to_vec(); 303 | self.set_data({ai}, Data::BeSigned(vec)); 304 | }} 305 | 306 | /// Removes the {n} (`{ais}`). 307 | pub fn remove_{vi}(&mut self) {{ 308 | self.remove_data_of(&{ai}); 309 | }} 310 | 311 | /// Returns the {n} formatted in an easily readable way. 312 | #[allow(unused)] 313 | pub(crate) fn format_{vi}(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{ 314 | match self.{vi}() {{ 315 | Some(s) => writeln!(f, \"{n}: {{}}\", s), 316 | None => Ok(()), 317 | }} 318 | }} 319 | }} 320 | ", 321 | hl = headline, 322 | n = name, 323 | ais = atom_ident_string, 324 | vi = value_ident, 325 | ai = atom_ident, 326 | ); 327 | } 328 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | https://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Tobias Schmitz 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | https://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/atom/change.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use super::*; 4 | 5 | pub trait CollectChanges { 6 | /// Recursively collect changes and return the length difference when applied. 7 | fn collect_changes<'a>( 8 | &'a self, 9 | insert_pos: u64, 10 | level: u8, 11 | changes: &mut Vec>, 12 | ) -> i64; 13 | } 14 | 15 | impl CollectChanges for Option { 16 | fn collect_changes<'a>( 17 | &'a self, 18 | insert_pos: u64, 19 | level: u8, 20 | changes: &mut Vec>, 21 | ) -> i64 { 22 | self.as_ref().map_or(0, |a| a.collect_changes(insert_pos, level, changes)) 23 | } 24 | } 25 | 26 | pub trait SimpleCollectChanges: AtomSize + Atom { 27 | fn state(&self) -> &State; 28 | 29 | /// Add changes, if any, and return the length difference when applied. 30 | fn existing<'a>( 31 | &'a self, 32 | level: u8, 33 | bounds: &'a AtomBounds, 34 | changes: &mut Vec>, 35 | ) -> i64; 36 | 37 | fn atom_ref(&self) -> AtomRef<'_>; 38 | } 39 | 40 | impl CollectChanges for T { 41 | fn collect_changes<'a>( 42 | &'a self, 43 | insert_pos: u64, 44 | level: u8, 45 | changes: &mut Vec>, 46 | ) -> i64 { 47 | match &self.state() { 48 | State::Existing(bounds) => { 49 | let len_diff = self.existing(level + 1, bounds, changes); 50 | if len_diff != 0 { 51 | changes.push(Change::UpdateLen(UpdateAtomLen { 52 | bounds, 53 | fourcc: Self::FOURCC, 54 | len_diff, 55 | })); 56 | } 57 | len_diff 58 | } 59 | State::Remove(bounds) => { 60 | changes.push(Change::Remove(RemoveAtom { bounds, level: level + 1 })); 61 | -(bounds.len() as i64) 62 | } 63 | State::Replace(bounds) => { 64 | let len_diff = (self.len() as i64) - (bounds.len() as i64); 65 | let r = ReplaceAtom { bounds, atom: self.atom_ref(), level: level + 1 }; 66 | changes.push(Change::Replace(r)); 67 | len_diff 68 | } 69 | State::Insert => { 70 | changes.push(Change::Insert(InsertAtom { 71 | pos: insert_pos, 72 | atom: self.atom_ref(), 73 | level: level + 1, 74 | })); 75 | self.len() as i64 76 | } 77 | } 78 | } 79 | } 80 | 81 | pub trait LeafAtomCollectChanges: SimpleCollectChanges { 82 | fn state(&self) -> &State; 83 | 84 | fn atom_ref(&self) -> AtomRef<'_>; 85 | } 86 | 87 | impl SimpleCollectChanges for T { 88 | fn state(&self) -> &State { 89 | LeafAtomCollectChanges::state(self) 90 | } 91 | 92 | fn existing<'a>( 93 | &'a self, 94 | _level: u8, 95 | _bounds: &'a AtomBounds, 96 | _changes: &mut Vec>, 97 | ) -> i64 { 98 | 0 99 | } 100 | 101 | fn atom_ref(&self) -> AtomRef<'_> { 102 | LeafAtomCollectChanges::atom_ref(self) 103 | } 104 | } 105 | 106 | #[derive(Debug)] 107 | pub enum Change<'a> { 108 | UpdateLen(UpdateAtomLen<'a>), 109 | UpdateChunkOffset(UpdateChunkOffsets<'a>), 110 | Remove(RemoveAtom<'a>), 111 | Replace(ReplaceAtom<'a>), 112 | Insert(InsertAtom<'a>), 113 | RemoveMdat(u64, u64), 114 | AppendMdat(u64, Vec), 115 | } 116 | 117 | impl std::fmt::Display for Change<'_> { 118 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 119 | #[rustfmt::skip] 120 | match self { 121 | Change::UpdateLen(UpdateAtomLen { fourcc, .. }) => write!(f, "UpdateLen {fourcc} "), 122 | Change::UpdateChunkOffset(_) => write!(f, "UpdateChunkOffset "), 123 | Change::Remove(_) => write!(f, "RemoveAtom "), 124 | Change::Replace(r) => write!(f, "ReplaceAtom {} ", r.atom.fourcc()), 125 | Change::Insert(i) => write!(f, "InsertAtom {} ", i.atom.fourcc()), 126 | Change::RemoveMdat(..) => write!(f, "RemoveMdat "), 127 | Change::AppendMdat(..) => write!(f, "AppendMdat "), 128 | }?; 129 | write!( 130 | f, 131 | "old_pos: {:6}, old_end: {:6}, len_diff: {:6}, level: {}", 132 | self.old_pos(), 133 | self.old_end(), 134 | self.len_diff(), 135 | self.level() 136 | ) 137 | } 138 | } 139 | 140 | impl Change<'_> { 141 | pub fn old_pos(&self) -> u64 { 142 | match self { 143 | Self::UpdateLen(c) => c.bounds.pos(), 144 | Self::UpdateChunkOffset(c) => c.bounds.content_pos() + stco::HEADER_SIZE, 145 | Self::Remove(c) => c.bounds.pos(), 146 | Self::Replace(c) => c.bounds.pos(), 147 | Self::Insert(c) => c.pos, 148 | Self::RemoveMdat(pos, _) => *pos, 149 | Self::AppendMdat(pos, _) => *pos, 150 | } 151 | } 152 | 153 | pub fn old_end(&self) -> u64 { 154 | match self { 155 | Self::UpdateLen(c) => c.bounds.content_pos(), 156 | Self::UpdateChunkOffset(c) => c.bounds.end(), 157 | Self::Remove(c) => c.bounds.end(), 158 | Self::Replace(c) => c.bounds.end(), 159 | Self::Insert(c) => c.pos, 160 | Self::RemoveMdat(pos, len) => *pos + *len, 161 | Self::AppendMdat(pos, _) => *pos, 162 | } 163 | } 164 | 165 | pub fn len_diff(&self) -> i64 { 166 | match self { 167 | Self::UpdateLen(_) => 0, 168 | Self::UpdateChunkOffset(_) => 0, 169 | Self::Remove(c) => -(c.bounds.len() as i64), 170 | Self::Replace(c) => (c.atom.len() as i64) - (c.bounds.len() as i64), 171 | Self::Insert(c) => c.atom.len() as i64, 172 | Self::RemoveMdat(_, len) => -(*len as i64), 173 | Self::AppendMdat(_, d) => d.len() as i64, 174 | } 175 | } 176 | 177 | pub fn level(&self) -> u8 { 178 | match self { 179 | Self::UpdateLen(_) => 0, 180 | Self::UpdateChunkOffset(_) => 6, 181 | Self::Remove(c) => c.level, 182 | Self::Replace(c) => c.level, 183 | Self::Insert(c) => c.level, 184 | Self::RemoveMdat(_, _) => u8::MAX, 185 | Self::AppendMdat(_, _) => u8::MAX, 186 | } 187 | } 188 | } 189 | 190 | #[derive(Debug)] 191 | pub struct UpdateAtomLen<'a> { 192 | pub bounds: &'a AtomBounds, 193 | pub fourcc: Fourcc, 194 | pub len_diff: i64, 195 | } 196 | 197 | impl UpdateAtomLen<'_> { 198 | pub fn update_len(&self, writer: &mut impl Write) -> crate::Result<()> { 199 | let len = (self.bounds.len() as i64 + self.len_diff) as u64; 200 | let head = Head::new(self.bounds.ext(), len, self.fourcc); 201 | head::write(writer, head)?; 202 | Ok(()) 203 | } 204 | } 205 | 206 | #[derive(Debug)] 207 | pub struct RemoveAtom<'a> { 208 | pub bounds: &'a AtomBounds, 209 | pub level: u8, 210 | } 211 | 212 | #[derive(Debug)] 213 | pub struct ReplaceAtom<'a> { 214 | pub bounds: &'a AtomBounds, 215 | pub atom: AtomRef<'a>, 216 | pub level: u8, 217 | } 218 | 219 | #[derive(Debug)] 220 | pub struct InsertAtom<'a> { 221 | pub pos: u64, 222 | pub atom: AtomRef<'a>, 223 | pub level: u8, 224 | } 225 | 226 | #[derive(Debug)] 227 | pub struct UpdateChunkOffsets<'a> { 228 | pub bounds: &'a AtomBounds, 229 | pub offsets: ChunkOffsets<'a>, 230 | } 231 | 232 | #[derive(Debug)] 233 | pub enum ChunkOffsets<'a> { 234 | Stco(Cow<'a, [u32]>), 235 | Co64(Cow<'a, [u64]>), 236 | } 237 | 238 | impl ChunkOffsets<'_> { 239 | pub fn update_offsets( 240 | &self, 241 | writer: &mut impl Write, 242 | changes: &[Change<'_>], 243 | ) -> crate::Result<()> { 244 | match self { 245 | ChunkOffsets::Stco(offsets) => write_shifted_offsets(writer, offsets, changes), 246 | ChunkOffsets::Co64(offsets) => write_shifted_offsets(writer, offsets, changes), 247 | } 248 | } 249 | } 250 | 251 | pub trait ChunkOffsetInt: Sized + Copy + Into { 252 | fn shift(&self, shift: i64) -> Self; 253 | fn write(&self, writer: &mut impl Write) -> crate::Result<()>; 254 | } 255 | 256 | impl ChunkOffsetInt for u32 { 257 | fn shift(&self, shift: i64) -> Self { 258 | (*self as i64 + shift) as u32 259 | } 260 | 261 | fn write(&self, writer: &mut impl Write) -> crate::Result<()> { 262 | writer.write_be_u32(*self)?; 263 | Ok(()) 264 | } 265 | } 266 | impl ChunkOffsetInt for u64 { 267 | fn shift(&self, shift: i64) -> Self { 268 | (*self as i64 + shift) as u64 269 | } 270 | 271 | fn write(&self, writer: &mut impl Write) -> crate::Result<()> { 272 | writer.write_be_u64(*self)?; 273 | Ok(()) 274 | } 275 | } 276 | 277 | pub fn write_shifted_offsets( 278 | writer: &mut impl Write, 279 | offsets: &[T], 280 | changes: &[Change<'_>], 281 | ) -> crate::Result<()> { 282 | let mut changes_iter = changes.iter().peekable(); 283 | 284 | let mut mdat_shift = 0; 285 | for o in offsets.iter().copied() { 286 | while let Some(change) = changes_iter.next_if(|c| c.old_pos() < o.into()) { 287 | mdat_shift += change.len_diff(); 288 | } 289 | 290 | o.shift(mdat_shift).write(writer)?; 291 | } 292 | Ok(()) 293 | } 294 | 295 | macro_rules! write_or_ignore { 296 | (nowrite, $($write:tt)*) => { 297 | Ok(()) 298 | }; 299 | (, $($write:tt)*) => { 300 | $($write)* 301 | }; 302 | } 303 | 304 | // false positive 305 | #[allow(unused)] 306 | macro_rules! test_or_ignore { 307 | (nowrite, $($write:tt)*) => {}; 308 | (, $($write:tt)*) => { 309 | $($write)* 310 | }; 311 | } 312 | 313 | macro_rules! atom_ref { 314 | ($($name:ident $(<$lifetime:lifetime>)? $($nowrite:ident)? ,)+) => { 315 | #[derive(Debug)] 316 | pub enum AtomRef<'a> { 317 | $($name(&'a $name $(<$lifetime>)?)),+ 318 | } 319 | 320 | impl AtomRef<'_> { 321 | pub fn write(&self, writer: &mut impl Write, changes: &[Change<'_>]) -> crate::Result<()> { 322 | match self { 323 | #[allow(unused)] 324 | $(Self::$name(a) => write_or_ignore!($($nowrite)?, {a.write(writer, changes)}),)+ 325 | } 326 | } 327 | 328 | pub fn fourcc(&self) -> Fourcc { 329 | match self { 330 | $(Self::$name(_) => $name::FOURCC,)+ 331 | } 332 | } 333 | 334 | fn len(&self) -> u64 { 335 | match self { 336 | $(Self::$name(a) => a.len(),)+ 337 | } 338 | } 339 | } 340 | 341 | #[cfg(test)] 342 | mod verify_written_length { 343 | use super::*; 344 | 345 | $( 346 | test_or_ignore! { $($nowrite)?, 347 | #[test] 348 | #[allow(non_snake_case)] 349 | fn $name() { 350 | let atom = $name::default(); 351 | let changes = []; 352 | 353 | let mut buf: Vec = Vec::new(); 354 | let mut cursor = std::io::Cursor::new(&mut buf); 355 | atom.write(&mut cursor, &changes).unwrap(); 356 | 357 | let buf_size = buf.len() as u64; 358 | 359 | let mut cursor = std::io::Cursor::new(&buf); 360 | cursor.seek(SeekFrom::Start(0)).unwrap(); 361 | let head = head::parse(&mut cursor, buf_size).unwrap(); 362 | 363 | assert_eq!(atom.len(), head.len()); 364 | assert_eq!(atom.len(), buf_size); 365 | } 366 | } 367 | )+ 368 | } 369 | }; 370 | } 371 | 372 | atom_ref!( 373 | Moov<'a> nowrite, 374 | Udta<'a>, 375 | Chpl<'a>, 376 | Meta<'a>, 377 | Hdlr, 378 | Ilst<'a>, 379 | Trak, 380 | Tref, 381 | Chap, 382 | Mdia, 383 | Minf, 384 | Dinf, 385 | Dref, 386 | Url, 387 | Gmhd, 388 | Gmin, 389 | Text, 390 | Stbl, 391 | Stsd, 392 | Stts, 393 | Stsc, 394 | Stsz, 395 | Stco, 396 | Co64, 397 | ); 398 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use std::fmt; 3 | use std::time::Duration; 4 | 5 | use crate::ErrorKind; 6 | 7 | /// The iTunes media type of a file. This is stored in the `stik` atom. 8 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 9 | pub enum MediaType { 10 | /// A media type stored as 0 in the `stik` atom. 11 | Movie = 0, 12 | /// A media type stored as 1 in the `stik` atom. 13 | Normal = 1, 14 | /// A media type stored as 2 in the `stik` atom. 15 | AudioBook = 2, 16 | /// A media type stored as 5 in the `stik` atom. 17 | WhackedBookmark = 5, 18 | /// A media type stored as 6 in the `stik` atom. 19 | MusicVideo = 6, 20 | /// A media type stored as 9 in the `stik` atom. 21 | ShortFilm = 9, 22 | /// A media type stored as 10 in the `stik` atom. 23 | TvShow = 10, 24 | /// A media type stored as 11 in the `stik` atom. 25 | Booklet = 11, 26 | } 27 | 28 | impl MediaType { 29 | const MOVIE: u8 = Self::Movie as u8; 30 | const NORMAL: u8 = Self::Normal as u8; 31 | const AUDIO_BOOK: u8 = Self::AudioBook as u8; 32 | const WHACKED_BOOKMARK: u8 = Self::WhackedBookmark as u8; 33 | const MUSIC_VIDEO: u8 = Self::MusicVideo as u8; 34 | const SHORT_FILM: u8 = Self::ShortFilm as u8; 35 | const TV_SHOW: u8 = Self::TvShow as u8; 36 | const BOOKLET: u8 = Self::Booklet as u8; 37 | 38 | pub fn code(&self) -> u8 { 39 | *self as u8 40 | } 41 | } 42 | 43 | impl TryFrom for MediaType { 44 | type Error = crate::Error; 45 | 46 | fn try_from(value: u8) -> Result { 47 | match value { 48 | Self::MOVIE => Ok(Self::Movie), 49 | Self::NORMAL => Ok(Self::Normal), 50 | Self::AUDIO_BOOK => Ok(Self::AudioBook), 51 | Self::WHACKED_BOOKMARK => Ok(Self::WhackedBookmark), 52 | Self::MUSIC_VIDEO => Ok(Self::MusicVideo), 53 | Self::SHORT_FILM => Ok(Self::ShortFilm), 54 | Self::TV_SHOW => Ok(Self::TvShow), 55 | Self::BOOKLET => Ok(Self::Booklet), 56 | _ => Err(Self::Error::new(ErrorKind::UnknownMediaType(value), "Unknown media type")), 57 | } 58 | } 59 | } 60 | 61 | impl fmt::Display for MediaType { 62 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 63 | match self { 64 | Self::Movie => write!(f, "Movie"), 65 | Self::Normal => write!(f, "Normal"), 66 | Self::AudioBook => write!(f, "Audiobook"), 67 | Self::WhackedBookmark => write!(f, "Whacked Bookmark"), 68 | Self::MusicVideo => write!(f, "Music Video"), 69 | Self::ShortFilm => write!(f, "Short Film"), 70 | Self::TvShow => write!(f, "TV-Show"), 71 | Self::Booklet => write!(f, "Booklet"), 72 | } 73 | } 74 | } 75 | 76 | /// The iTunes advisory rating of a file. This is stored in the `rtng` atom. 77 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 78 | pub enum AdvisoryRating { 79 | /// An advisory rating stored as 2 in the `rtng` atom. 80 | Clean = 2, 81 | /// An advisory rating stored as 0 in the `rtng` atom. 82 | Inoffensive = 0, 83 | /// An advisory rating indicated by any other value than 0 or 2 in the `rtng` atom. 84 | Explicit = 4, 85 | } 86 | 87 | impl AdvisoryRating { 88 | const CLEAN: u8 = Self::Clean as u8; 89 | const INOFFENSIVE: u8 = Self::Inoffensive as u8; 90 | 91 | pub fn code(&self) -> u8 { 92 | *self as u8 93 | } 94 | } 95 | 96 | impl From for AdvisoryRating { 97 | fn from(rating: u8) -> Self { 98 | match rating { 99 | Self::CLEAN => Self::Clean, 100 | Self::INOFFENSIVE => Self::Inoffensive, 101 | _ => Self::Explicit, 102 | } 103 | } 104 | } 105 | 106 | impl fmt::Display for AdvisoryRating { 107 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 108 | match self { 109 | Self::Clean => write!(f, "Clean"), 110 | Self::Inoffensive => write!(f, "Inoffensive"), 111 | Self::Explicit => write!(f, "Explicit"), 112 | } 113 | } 114 | } 115 | 116 | /// The channel configuration of an MPEG-4 audio track. 117 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 118 | pub enum ChannelConfig { 119 | /// 1.0, channel: front-center. 120 | Mono = 1, 121 | /// 2.0, channels: front-left, front-right. 122 | Stereo = 2, 123 | /// 3.0, channels: front-center, front-left, front-right. 124 | Three = 3, 125 | /// 4.0, channels: front-center, front-left, front-right, back-center. 126 | Four = 4, 127 | /// 5.0, channels: front-center, front-left, front-right, back-left, back-right. 128 | Five = 5, 129 | /// 5.1, channels: front-center, front-left, front-right, back-left, back-right, LFE-channel. 130 | FiveOne = 6, 131 | /// 7.1, channels: front-center, front-left, front-right, side-left, side-right, back-left, back-right, LFE-channel. 132 | SevenOne = 7, 133 | } 134 | 135 | impl ChannelConfig { 136 | const MONO: u8 = Self::Mono as u8; 137 | const STEREO: u8 = Self::Stereo as u8; 138 | const THREE: u8 = Self::Three as u8; 139 | const FOUR: u8 = Self::Four as u8; 140 | const FIVE: u8 = Self::Five as u8; 141 | const FIVE_ONE: u8 = Self::FiveOne as u8; 142 | const SEVEN_ONE: u8 = Self::SevenOne as u8; 143 | 144 | /// Returns the number of channels. 145 | pub const fn channel_count(&self) -> u8 { 146 | match self { 147 | Self::Mono => 1, 148 | Self::Stereo => 2, 149 | Self::Three => 3, 150 | Self::Four => 4, 151 | Self::Five => 5, 152 | Self::FiveOne => 6, 153 | Self::SevenOne => 8, 154 | } 155 | } 156 | } 157 | 158 | impl TryFrom for ChannelConfig { 159 | type Error = crate::Error; 160 | 161 | fn try_from(value: u8) -> Result { 162 | match value { 163 | Self::MONO => Ok(Self::Mono), 164 | Self::STEREO => Ok(Self::Stereo), 165 | Self::THREE => Ok(Self::Three), 166 | Self::FOUR => Ok(Self::Four), 167 | Self::FIVE => Ok(Self::Five), 168 | Self::FIVE_ONE => Ok(Self::FiveOne), 169 | Self::SEVEN_ONE => Ok(Self::SevenOne), 170 | _ => Err(Self::Error::new( 171 | crate::ErrorKind::UnknownChannelConfig(value), 172 | "Unknown channel config index", 173 | )), 174 | } 175 | } 176 | } 177 | 178 | impl fmt::Display for ChannelConfig { 179 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 180 | match self { 181 | Self::Mono => write!(f, "Mono"), 182 | Self::Stereo => write!(f, "Stereo"), 183 | Self::Three => write!(f, "3.0"), 184 | Self::Four => write!(f, "4.0"), 185 | Self::Five => write!(f, "5.0"), 186 | Self::FiveOne => write!(f, "5.1"), 187 | Self::SevenOne => write!(f, "7.1"), 188 | } 189 | } 190 | } 191 | 192 | /// An enum representing the sample rate of an MPEG-4 audio track. 193 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 194 | pub enum SampleRate { 195 | /// A Sample rate of 96000Hz 196 | Hz96000 = 0, 197 | /// A Sample rate of 88200Hz 198 | Hz88200 = 1, 199 | /// A Sample rate of 64000Hz 200 | Hz64000 = 2, 201 | /// A Sample rate of 48000Hz 202 | Hz48000 = 3, 203 | /// A Sample rate of 44100Hz 204 | Hz44100 = 4, 205 | /// A Sample rate of 32000Hz 206 | Hz32000 = 5, 207 | /// A Sample rate of 24000Hz 208 | Hz24000 = 6, 209 | /// A Sample rate of 24050Hz 210 | Hz22050 = 7, 211 | /// A Sample rate of 16000Hz 212 | Hz16000 = 8, 213 | /// A Sample rate of 12000Hz 214 | Hz12000 = 9, 215 | /// A Sample rate of 11050Hz 216 | Hz11025 = 10, 217 | /// A Sample rate of 8000Hz 218 | Hz8000 = 11, 219 | /// A Sample rate of 7350Hz 220 | Hz7350 = 12, 221 | } 222 | 223 | impl SampleRate { 224 | const HZ_96000: u8 = Self::Hz96000 as u8; 225 | const HZ_88200: u8 = Self::Hz88200 as u8; 226 | const HZ_64000: u8 = Self::Hz64000 as u8; 227 | const HZ_48000: u8 = Self::Hz48000 as u8; 228 | const HZ_44100: u8 = Self::Hz44100 as u8; 229 | const HZ_32000: u8 = Self::Hz32000 as u8; 230 | const HZ_24000: u8 = Self::Hz24000 as u8; 231 | const HZ_22050: u8 = Self::Hz22050 as u8; 232 | const HZ_16000: u8 = Self::Hz16000 as u8; 233 | const HZ_12000: u8 = Self::Hz12000 as u8; 234 | const HZ_11025: u8 = Self::Hz11025 as u8; 235 | const HZ_8000: u8 = Self::Hz8000 as u8; 236 | const HZ_7350: u8 = Self::Hz7350 as u8; 237 | 238 | /// Returns the sample rate in Hz. 239 | pub const fn hz(&self) -> u32 { 240 | match self { 241 | Self::Hz96000 => 96000, 242 | Self::Hz88200 => 88200, 243 | Self::Hz64000 => 64000, 244 | Self::Hz48000 => 48000, 245 | Self::Hz44100 => 44100, 246 | Self::Hz32000 => 32000, 247 | Self::Hz24000 => 24000, 248 | Self::Hz22050 => 22050, 249 | Self::Hz16000 => 16000, 250 | Self::Hz12000 => 12000, 251 | Self::Hz11025 => 11025, 252 | Self::Hz8000 => 8000, 253 | Self::Hz7350 => 7350, 254 | } 255 | } 256 | } 257 | 258 | impl TryFrom for SampleRate { 259 | type Error = crate::Error; 260 | 261 | fn try_from(value: u8) -> Result { 262 | match value { 263 | Self::HZ_96000 => Ok(Self::Hz96000), 264 | Self::HZ_88200 => Ok(Self::Hz88200), 265 | Self::HZ_64000 => Ok(Self::Hz64000), 266 | Self::HZ_48000 => Ok(Self::Hz48000), 267 | Self::HZ_44100 => Ok(Self::Hz44100), 268 | Self::HZ_32000 => Ok(Self::Hz32000), 269 | Self::HZ_24000 => Ok(Self::Hz24000), 270 | Self::HZ_22050 => Ok(Self::Hz22050), 271 | Self::HZ_16000 => Ok(Self::Hz16000), 272 | Self::HZ_12000 => Ok(Self::Hz12000), 273 | Self::HZ_11025 => Ok(Self::Hz11025), 274 | Self::HZ_8000 => Ok(Self::Hz8000), 275 | Self::HZ_7350 => Ok(Self::Hz7350), 276 | _ => Err(Self::Error::new( 277 | crate::ErrorKind::UnknownSampleRate(value), 278 | "Unknown sample rate index", 279 | )), 280 | } 281 | } 282 | } 283 | 284 | impl fmt::Display for SampleRate { 285 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 286 | write!(f, "{}Hz", self.hz()) 287 | } 288 | } 289 | 290 | /// Audio information of an mp4 track. 291 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 292 | pub struct AudioInfo { 293 | /// The duration of the track. 294 | pub duration: Duration, 295 | /// The channel configuration of the track. 296 | pub channel_config: Option, 297 | /// The sample rate of the track. 298 | pub sample_rate: Option, 299 | /// The maximum bitrate of the track. 300 | pub max_bitrate: Option, 301 | /// The average bitrate of the track. 302 | pub avg_bitrate: Option, 303 | } 304 | 305 | /// Type alias for an image reference. 306 | pub type ImgRef<'a> = Img<&'a [u8]>; 307 | /// Type alias for a mutable image reference. 308 | pub type ImgMut<'a> = Img<&'a mut Vec>; 309 | /// Type alias for an owned image buffer. 310 | pub type ImgBuf = Img>; 311 | 312 | /// Image data with an associated format. 313 | #[derive(Clone, Debug, PartialEq, Eq)] 314 | pub struct Img { 315 | /// The image format. 316 | pub fmt: ImgFmt, 317 | /// The image data. 318 | pub data: T, 319 | } 320 | 321 | impl Img { 322 | pub const fn new(fmt: ImgFmt, data: T) -> Self { 323 | Self { fmt, data } 324 | } 325 | 326 | pub const fn bmp(data: T) -> Self { 327 | Self::new(ImgFmt::Bmp, data) 328 | } 329 | 330 | pub const fn jpeg(data: T) -> Self { 331 | Self::new(ImgFmt::Jpeg, data) 332 | } 333 | 334 | pub const fn png(data: T) -> Self { 335 | Self::new(ImgFmt::Png, data) 336 | } 337 | } 338 | 339 | /// The image format used to store images inside the userdata of an MPEG-4 file. 340 | #[derive(Clone, Debug, PartialEq, Eq)] 341 | pub enum ImgFmt { 342 | Bmp, 343 | Jpeg, 344 | Png, 345 | } 346 | 347 | impl ImgFmt { 348 | /// Returns `true` if the img fmt is [`Bmp`]. 349 | /// 350 | /// [`Bmp`]: ImgFmt::Bmp 351 | #[must_use] 352 | pub fn is_bmp(&self) -> bool { 353 | matches!(self, Self::Bmp) 354 | } 355 | 356 | /// Returns `true` if the img fmt is [`Jpeg`]. 357 | /// 358 | /// [`Jpeg`]: ImgFmt::Jpeg 359 | #[must_use] 360 | pub fn is_jpeg(&self) -> bool { 361 | matches!(self, Self::Jpeg) 362 | } 363 | 364 | /// Returns `true` if the img fmt is [`Png`]. 365 | /// 366 | /// [`Png`]: ImgFmt::Png 367 | #[must_use] 368 | pub fn is_png(&self) -> bool { 369 | matches!(self, Self::Png) 370 | } 371 | } 372 | 373 | /// A chapter. 374 | /// 375 | /// Note that chapter titles have a relatively small maximum size. 376 | /// For chapter lists this limit is 255 ([`u8::MAX`]); 377 | /// For chapter tracks this limit is 65535 ([`u16::MAX`]); 378 | /// If this limit is exceeded the title is truncated. 379 | #[derive(Clone, Debug, Default, PartialEq, Eq)] 380 | pub struct Chapter { 381 | /// The start of the chapter. 382 | pub start: Duration, 383 | /// The title of the chapter. 384 | pub title: String, 385 | } 386 | 387 | impl Chapter { 388 | pub fn new(start: Duration, title: impl Into) -> Self { 389 | Self { start, title: title.into() } 390 | } 391 | } 392 | --------------------------------------------------------------------------------