mp4ameta/atom/
mod.rs

1//! Relevant structure of an mp4 file
2//!
3//! ```md
4//! ftyp
5//! mdat
6//! moov
7//! ├─ mvhd
8//! ├─ trak
9//! │  ├─ tkhd
10//! │  ├─ tref
11//! │  │  └─ chap
12//! │  └─ mdia
13//! │     ├─ mdhd
14//! │     ├─ hdlr
15//! │     └─ minf
16//! │        ├─ dinf
17//! │        │  └─ dref
18//! │        │     └─ url
19//! │        ├─ gmhd
20//! │        │  ├─ gmin
21//! │        │  └─ text
22//! │        └─ stbl
23//! │           ├─ stsd
24//! │           │  ├─ mp4a
25//! │           │  │  └─ esds
26//! │           │  └─ text
27//! │           ├─ stts
28//! │           ├─ stsc
29//! │           ├─ stsz
30//! │           ├─ stco
31//! │           └─ co64
32//! └─ udta
33//!    ├─ chpl
34//!    └─ meta
35//!       ├─ hdlr
36//!       └─ ilst
37//!          ├─ **** (any fourcc)
38//!          │  └─ data
39//!          └─ ---- (freeform fourcc)
40//!             ├─ mean
41//!             ├─ name
42//!             └─ data
43//! ```
44
45use std::borrow::Cow;
46use std::convert::TryFrom;
47use std::fs::File;
48use std::io::{BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write};
49use std::num::NonZeroU32;
50use std::ops::Deref;
51
52use crate::{AudioInfo, Chapter, ErrorKind, Tag, Userdata};
53
54use change::{
55    AtomRef, Change, ChunkOffsetInt, ChunkOffsets, CollectChanges, LeafAtomCollectChanges,
56    SimpleCollectChanges, UpdateAtomLen, UpdateChunkOffsets,
57};
58use head::{AtomBounds, Head, Size, find_bounds};
59use ident::*;
60use state::State;
61use util::*;
62
63use chap::Chap;
64use chpl::{Chpl, ChplData};
65use co64::Co64;
66use dinf::Dinf;
67use dref::Dref;
68use ftyp::Ftyp;
69use gmhd::Gmhd;
70use gmin::Gmin;
71use hdlr::Hdlr;
72use ilst::Ilst;
73use mdat::Mdat;
74use mdhd::Mdhd;
75use mdia::Mdia;
76use meta::Meta;
77use minf::Minf;
78use moov::Moov;
79use mp4a::Mp4a;
80use mvhd::Mvhd;
81use stbl::{Stbl, Table};
82use stco::Stco;
83use stsc::{Stsc, StscItem};
84use stsd::Stsd;
85use stsz::Stsz;
86use stts::{Stts, SttsItem};
87use text::Text;
88use tkhd::Tkhd;
89use trak::Trak;
90use tref::Tref;
91use udta::Udta;
92use url::*;
93
94pub use data::Data;
95pub use metaitem::MetaItem;
96
97/// A module for working with identifiers.
98pub mod ident;
99
100#[macro_use]
101mod util;
102mod change;
103mod head;
104mod state;
105
106mod chap;
107mod chpl;
108mod co64;
109mod data;
110mod dinf;
111mod dref;
112mod ftyp;
113mod gmhd;
114mod gmin;
115mod hdlr;
116mod ilst;
117mod mdat;
118mod mdhd;
119mod mdia;
120mod meta;
121mod metaitem;
122mod minf;
123mod moov;
124mod mp4a;
125mod mvhd;
126mod stbl;
127mod stco;
128mod stsc;
129mod stsd;
130mod stsz;
131mod stts;
132mod text;
133mod tkhd;
134mod trak;
135mod tref;
136mod udta;
137mod url;
138
139trait Atom: Sized {
140    const FOURCC: Fourcc;
141}
142
143trait ParseAtom: Atom {
144    fn parse(
145        reader: &mut (impl Read + Seek),
146        cfg: &ParseConfig<'_>,
147        size: Size,
148    ) -> crate::Result<Self> {
149        match Self::parse_atom(reader, cfg, size) {
150            Err(mut e) => {
151                let mut d = e.description.into_owned();
152                insert_str(&mut d, "Error parsing ", Self::FOURCC);
153                e.description = d.into();
154                Err(e)
155            }
156            a => a,
157        }
158    }
159
160    fn parse_atom(
161        reader: &mut (impl Read + Seek),
162        cfg: &ParseConfig<'_>,
163        size: Size,
164    ) -> crate::Result<Self>;
165}
166
167trait AtomSize {
168    fn len(&self) -> u64 {
169        self.size().len()
170    }
171
172    fn size(&self) -> Size;
173}
174
175trait WriteAtom: AtomSize + Atom {
176    fn write(&self, writer: &mut impl Write, changes: &[Change<'_>]) -> crate::Result<()> {
177        match self.write_atom(writer, changes) {
178            Err(mut e) => {
179                let mut d = e.description.into_owned();
180                insert_str(&mut d, "Error writing ", Self::FOURCC);
181                e.description = d.into();
182                Err(e)
183            }
184            a => a,
185        }
186    }
187
188    fn write_head(&self, writer: &mut impl Write) -> crate::Result<()> {
189        let head = Head::from(self.size(), Self::FOURCC);
190        head::write(writer, head)
191    }
192
193    fn write_atom(&self, writer: &mut impl Write, changes: &[Change<'_>]) -> crate::Result<()>;
194}
195
196fn insert_str(description: &mut String, msg: &str, fourcc: Fourcc) {
197    description.reserve(msg.len() + 6);
198    description.insert_str(0, ": ");
199    fourcc.iter().rev().for_each(|c| {
200        description.insert(0, char::from(*c));
201    });
202    description.insert_str(0, msg);
203}
204
205trait LenOrZero {
206    fn len_or_zero(&self) -> u64;
207}
208
209impl<T: AtomSize> LenOrZero for Option<T> {
210    fn len_or_zero(&self) -> u64 {
211        self.as_ref().map_or(0, |a| a.len())
212    }
213}
214
215trait PushAndGet<T> {
216    fn push_and_get(&mut self, item: T) -> &mut T;
217}
218impl<T> PushAndGet<T> for Vec<T> {
219    fn push_and_get(&mut self, item: T) -> &mut T {
220        self.push(item);
221        self.last_mut().unwrap()
222    }
223}
224
225/// The timescale which is used for the chapter list (`chpl`).
226///
227/// | library          | timescale  |
228/// |------------------|------------|
229/// | FFMpeg (default) | 10,000,000 |
230/// | mp4v2            |      1,000 |
231/// | mutagen          |       mvhd |
232#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub enum ChplTimescale {
234    /// Use a fixed timescale: the number of units that pass per second.
235    Fixed(NonZeroU32),
236    /// Use the timescale defined in the movie header (mvhd) atom.
237    Mvhd,
238}
239
240impl Default for ChplTimescale {
241    fn default() -> Self {
242        Self::DEFAULT
243    }
244}
245
246impl ChplTimescale {
247    pub const DEFAULT: Self = Self::Fixed(chpl::DEFAULT_TIMESCALE);
248
249    fn fixed_or_mvhd(self, mvhd_timescale: u32) -> u32 {
250        match self {
251            Self::Fixed(v) => v.get(),
252            Self::Mvhd => mvhd_timescale,
253        }
254    }
255}
256
257/// Configure what kind of data should be rad
258///
259/// The item list stores tags such as the artist, album, title, and also the cover art of a song.
260/// And there are two separate ways of storing chapter information:
261/// - A chapter list
262/// - A chapter track
263#[derive(Clone, Debug, PartialEq, Eq)]
264pub struct ReadConfig {
265    /// Wheter the metatdata item list will be read.
266    pub read_meta_items: bool,
267    /// Wheter image data will be read, mostly for performance reasons.
268    /// If disabled, images will still show up as empty [`Data`].
269    pub read_image_data: bool,
270    /// Wheter chapter list information will be read.
271    pub read_chapter_list: bool,
272    /// Wheter chapter track information will be read.
273    pub read_chapter_track: bool,
274    /// Wheter audio information will be read.
275    /// Even if disabled, the [`AudioInfo::duration`] will be read.
276    pub read_audio_info: bool,
277    /// The timescale that is used to scale time for chapter list (chpl) atoms.
278    pub chpl_timescale: ChplTimescale,
279}
280
281impl ReadConfig {
282    /// The default configuration for reading tags.
283    pub const DEFAULT: ReadConfig = ReadConfig {
284        read_meta_items: true,
285        read_image_data: true,
286        read_chapter_list: true,
287        read_chapter_track: true,
288        read_audio_info: true,
289        chpl_timescale: ChplTimescale::DEFAULT,
290    };
291
292    /// A configuration that would read no data at all.
293    ///
294    /// ```
295    /// use mp4ameta::ReadConfig;
296    ///
297    /// let cfg = ReadConfig {
298    ///     read_meta_items: true,
299    ///     read_image_data: true,
300    ///     ..ReadConfig::NONE
301    /// };
302    /// ```
303    pub const NONE: ReadConfig = ReadConfig {
304        read_meta_items: false,
305        read_image_data: false,
306        read_chapter_list: false,
307        read_chapter_track: false,
308        read_audio_info: false,
309        chpl_timescale: ChplTimescale::DEFAULT,
310    };
311}
312
313impl Default for ReadConfig {
314    fn default() -> Self {
315        Self::DEFAULT.clone()
316    }
317}
318
319pub struct ParseConfig<'a> {
320    cfg: &'a ReadConfig,
321    write: bool,
322}
323
324pub(crate) fn read_tag(reader: &mut (impl Read + Seek), cfg: &ReadConfig) -> crate::Result<Tag> {
325    let parse_cfg = ParseConfig { cfg, write: false };
326
327    let file_len = reader.seek(SeekFrom::End(0))?;
328    reader.seek(SeekFrom::Start(0))?;
329
330    let ftyp = Ftyp::parse(reader, file_len)?;
331
332    let mut parsed_bytes = ftyp.size.len();
333    let mut moov = loop {
334        if parsed_bytes >= file_len {
335            return Err(crate::Error::new(
336                ErrorKind::AtomNotFound(MOVIE),
337                "Missing necessary data, no movie (moov) atom found",
338            ));
339        }
340
341        let remaining_bytes = file_len - parsed_bytes;
342        let head = head::parse(reader, remaining_bytes)?;
343        if head.fourcc() == MOVIE {
344            break Moov::parse(reader, &parse_cfg, head.size())?;
345        }
346
347        reader.skip(head.content_len() as i64)?;
348        parsed_bytes += head.len();
349    };
350
351    let mvhd = moov.mvhd;
352    let duration = scale_duration(mvhd.timescale, mvhd.duration);
353
354    let meta_items = moov
355        .udta
356        .as_mut()
357        .and_then(|a| a.meta.take())
358        .and_then(|a| a.ilst)
359        .map(|a| a.data.into_owned())
360        .unwrap_or_default();
361
362    // chapter list atom
363    let mut chapter_list = Vec::new();
364    if cfg.read_chapter_list {
365        if let Some(mut chpl) = moov.udta.and_then(|a| a.chpl).and_then(|a| a.into_owned()) {
366            let chpl_timescale = cfg.chpl_timescale.fixed_or_mvhd(mvhd.timescale);
367
368            chpl.sort_by_key(|c| c.start);
369            chapter_list.reserve(chpl.len());
370
371            for c in chpl {
372                chapter_list.push(Chapter {
373                    start: scale_duration(chpl_timescale, c.start),
374                    title: c.title,
375                });
376            }
377        }
378    }
379
380    // chapter tracks
381    let mut chapter_track = Vec::new();
382    if cfg.read_chapter_track {
383        // https://developer.apple.com/documentation/quicktime-file-format/chapter_lists
384        // > If more than one enabled track includes a 'chap' track reference,
385        // > QuickTime uses the first chapter list that it finds.
386        let traks = &moov.trak;
387        let chapter_trak = traks.iter().find_map(|trak| {
388            let chap = trak.tref.as_ref().and_then(|tref| tref.chap.as_ref())?;
389            traks.iter().find(|trak| chap.chapter_ids.contains(&trak.tkhd.id))
390        });
391        if let Some(trak) = chapter_trak {
392            let Some(mdia) = &trak.mdia else {
393                return Err(crate::Error::new(
394                    ErrorKind::AtomNotFound(MEDIA),
395                    "Media (mdia) atom of chapter track not found",
396                ));
397            };
398            let Some(stbl) = mdia.minf.as_ref().and_then(|a| a.stbl.as_ref()) else {
399                return Err(crate::Error::new(
400                    ErrorKind::AtomNotFound(SAMPLE_TABLE),
401                    "Sample table (stbl) of chapter track not found",
402                ));
403            };
404            let Some(stsc) = &stbl.stsc else {
405                return Err(crate::Error::new(
406                    ErrorKind::AtomNotFound(SAMPLE_TABLE_SAMPLE_TO_CHUNK),
407                    "Sample table sample to chunk (stsc) atom of chapter track not found",
408                ));
409            };
410            let Some(stsz) = &stbl.stsz else {
411                return Err(crate::Error::new(
412                    ErrorKind::AtomNotFound(SAMPLE_TABLE_SAMPLE_SIZE),
413                    "Sample table sample size (stsz) atom of chapter track not found",
414                ));
415            };
416            let Some(stts) = &stbl.stts else {
417                return Err(crate::Error::new(
418                    ErrorKind::AtomNotFound(SAMPLE_TABLE_TIME_TO_SAMPLE),
419                    "Sample table time to sample (stts) atom of chapter track not found",
420                ));
421            };
422            let timescale = mdia.mdhd.timescale;
423
424            let stsc_items = stsc.items.get_or_read(reader)?;
425            let stsz_sizes = stsz.sizes.get_or_read(reader)?;
426            let stts_items = stts.items.get_or_read(reader)?;
427
428            chapter_track.reserve(stsz_sizes.len());
429
430            if let Some(co64) = &stbl.co64 {
431                let co64_offsets = co64.offsets.get_or_read(reader)?;
432
433                read_track_chapters(
434                    reader,
435                    &mut chapter_track,
436                    timescale,
437                    &co64_offsets,
438                    &stsc_items,
439                    stsz.uniform_sample_size,
440                    &stsz_sizes,
441                    &stts_items,
442                )
443                .map_err(|mut e| {
444                    let mut desc = e.description.into_owned();
445                    desc.insert_str(0, "Error reading chapters: ");
446                    e.description = desc.into();
447                    e
448                })?;
449            } else if let Some(stco) = &stbl.stco {
450                let stco_offsets = stco.offsets.get_or_read(reader)?;
451
452                chapter_track.reserve(stco.offsets.len());
453                read_track_chapters(
454                    reader,
455                    &mut chapter_track,
456                    timescale,
457                    &stco_offsets,
458                    stsc_items.as_ref(),
459                    stsz.uniform_sample_size,
460                    stsz_sizes.as_ref(),
461                    stts_items.as_ref(),
462                )
463                .map_err(|mut e| {
464                    let mut desc = e.description.into_owned();
465                    desc.insert_str(0, "Error reading chapters: ");
466                    e.description = desc.into();
467                    e
468                })?;
469            }
470        }
471    }
472
473    let mut info = AudioInfo { duration, ..Default::default() };
474    if cfg.read_audio_info {
475        let mp4a = moov.trak.into_iter().find_map(|trak| {
476            trak.mdia
477                .and_then(|a| a.minf)
478                .and_then(|a| a.stbl)
479                .and_then(|a| a.stsd)
480                .and_then(|a| a.mp4a)
481        });
482        if let Some(i) = mp4a {
483            info.channel_config = i.channel_config;
484            info.sample_rate = i.sample_rate;
485            info.max_bitrate = i.max_bitrate;
486            info.avg_bitrate = i.avg_bitrate;
487        }
488    }
489
490    let userdata = Userdata { meta_items, chapter_list, chapter_track };
491    Ok(Tag { ftyp: ftyp.string, info, userdata })
492}
493
494fn read_track_chapters<T: ChunkOffsetInt>(
495    reader: &mut (impl Read + Seek),
496    chapters: &mut Vec<Chapter>,
497    timescale: u32,
498    offsets: &[T],
499    stsc: &[StscItem],
500    stsz_uniform_size: u32,
501    stsz_sizes: &[u32],
502    stts: &[SttsItem],
503) -> crate::Result<()> {
504    let mut time = 0;
505    let mut stco_idx = 0;
506    let mut stsz_iter = stsz_sizes.iter();
507    let mut stts_iter = stts.iter().flat_map(|stts_item| {
508        std::iter::repeat_n(stts_item.sample_duration, stts_item.sample_count as usize)
509    });
510
511    for (stsc_idx, stsc_item) in stsc.iter().enumerate() {
512        let stco_end_idx = match stsc.get(stsc_idx + 1) {
513            Some(next_stsc_item) => {
514                let end_idx = next_stsc_item.first_chunk as usize;
515                if end_idx > offsets.len() {
516                    return Err(crate::Error::new(
517                        ErrorKind::InvalidSampleTable,
518                        "Sample table sample to chunk (stsc) first chunk index is out of bounds",
519                    ));
520                }
521                end_idx
522            }
523            None => offsets.len(),
524        };
525
526        for o in offsets[stco_idx..stco_end_idx].iter().copied() {
527            let mut current_offset = o.into();
528
529            for _ in 0..stsc_item.samples_per_chunk {
530                let size = if stsz_uniform_size != 0 {
531                    stsz_uniform_size
532                } else {
533                    let Some(size) = stsz_iter.next() else {
534                        return Err(crate::Error::new(
535                            ErrorKind::InvalidSampleTable,
536                            "Missing sample table sample size (stsz) item",
537                        ));
538                    };
539                    *size
540                };
541                let Some(duration) = stts_iter.next() else {
542                    return Err(crate::Error::new(
543                        ErrorKind::InvalidSampleTable,
544                        "Missing sample time to sample (stts) duration",
545                    ));
546                };
547
548                let title = read_chapter_title(reader, current_offset)?;
549                chapters.push(Chapter { start: scale_duration(timescale, time), title });
550
551                time += duration as u64;
552
553                current_offset += size as u64;
554            }
555        }
556
557        stco_idx = stco_end_idx;
558    }
559
560    Ok(())
561}
562
563fn read_chapter_title(reader: &mut (impl Read + Seek), offset: u64) -> crate::Result<String> {
564    reader.seek(SeekFrom::Start(offset))?;
565    let len = reader.read_be_u16()?;
566    let bom = reader.read_be_u16()?;
567
568    // check BOM (byte order mark) for encoding
569    let title = match bom {
570        0xfeff => reader.read_be_utf16(len as u64 - 2)?,
571        0xfffe => reader.read_le_utf16(len as u64 - 2)?,
572        _ => {
573            reader.skip(-2)?;
574            reader.read_utf8(len as u64)?
575        }
576    };
577
578    Ok(title)
579}
580
581/// Configure which metadata is (over)written.
582///
583/// The item list stores tags such as the artist, album, title, and also the cover art of a song.
584/// And there are two separate ways of storing chapter information:
585/// - A chapter list
586/// - A chapter track
587#[derive(Clone, Debug, PartialEq, Eq)]
588pub struct WriteConfig {
589    /// Whether to overwrite the metadata item list.
590    pub write_meta_items: bool,
591    /// Whether to overwrite chapter list information.
592    pub write_chapter_list: bool,
593    /// Whether to overwrite chapter track information.
594    pub write_chapter_track: bool,
595    /// The timescale that is used to scale time for chapter list (chpl) atoms.
596    pub chpl_timescale: ChplTimescale,
597}
598
599impl WriteConfig {
600    /// The default configuration for writing tags.
601    pub const DEFAULT: WriteConfig = WriteConfig {
602        write_meta_items: true,
603        write_chapter_list: true,
604        write_chapter_track: true,
605        chpl_timescale: ChplTimescale::DEFAULT,
606    };
607
608    /// A configuration that would write no data at all.
609    ///
610    /// ```
611    /// use mp4ameta::WriteConfig;
612    ///
613    /// let cfg = WriteConfig {
614    ///     write_meta_items: true,
615    ///     ..WriteConfig::NONE
616    /// };
617    /// ```
618    pub const NONE: WriteConfig = WriteConfig {
619        write_meta_items: false,
620        write_chapter_list: false,
621        write_chapter_track: false,
622        chpl_timescale: ChplTimescale::DEFAULT,
623    };
624}
625
626impl Default for WriteConfig {
627    fn default() -> Self {
628        Self::DEFAULT.clone()
629    }
630}
631
632#[derive(Debug)]
633struct MovedData {
634    new_pos: u64,
635    data: Vec<u8>,
636}
637
638/// A trait representing a file-like reader/writer.
639///
640/// This trait is the combination of the [`std::io`]
641/// stream traits with an additional method to resize the file.
642pub trait StorageFile: Read + Write + Seek {
643    /// Resize the file. This method behaves the same as
644    /// [`File::set_len`](std::fs::File::set_len).
645    fn set_len(&mut self, new_size: u64) -> crate::Result<()>;
646}
647
648impl StorageFile for File {
649    fn set_len(&mut self, new_size: u64) -> crate::Result<()> {
650        Ok(std::fs::File::set_len(self, new_size)?)
651    }
652}
653
654impl StorageFile for Cursor<Vec<u8>> {
655    fn set_len(&mut self, new_size: u64) -> crate::Result<()> {
656        self.get_mut().resize(new_size as usize, 0);
657        Ok(())
658    }
659}
660
661pub(crate) fn write_tag(
662    file: &mut impl StorageFile,
663    cfg: &WriteConfig,
664    userdata: &Userdata,
665) -> crate::Result<()> {
666    let mut reader = BufReader::new(&mut *file);
667
668    let old_file_len = reader.seek(SeekFrom::End(0))?;
669    reader.seek(SeekFrom::Start(0))?;
670
671    let ftyp = Ftyp::parse(&mut reader, old_file_len)?;
672
673    let mut moov = None;
674    let mut mdat_bounds = None;
675    {
676        let read_cfg = ReadConfig {
677            read_meta_items: cfg.write_meta_items,
678            read_chapter_list: cfg.write_chapter_list,
679            read_chapter_track: cfg.write_chapter_track,
680            read_audio_info: false,
681            read_image_data: false,
682            chpl_timescale: ChplTimescale::default(),
683        };
684
685        let mut parsed_bytes = ftyp.size.len();
686        while parsed_bytes < old_file_len {
687            let remaining_bytes = old_file_len - parsed_bytes;
688            let head = head::parse(&mut reader, remaining_bytes)?;
689            let parse_cfg = ParseConfig { cfg: &read_cfg, write: true };
690            match head.fourcc() {
691                MOVIE => moov = Some(Moov::parse(&mut reader, &parse_cfg, head.size())?),
692                MEDIA_DATA => mdat_bounds = Some(Mdat::read_bounds(&mut reader, head.size())?),
693                _ => reader.skip(head.content_len() as i64)?,
694            }
695
696            parsed_bytes += head.len();
697        }
698    }
699
700    let Some(mut moov) = moov else {
701        return Err(crate::Error::new(
702            crate::ErrorKind::AtomNotFound(MOVIE),
703            "Missing necessary data, no movie (moov) atom found",
704        ));
705    };
706    let Some(mdat_bounds) = mdat_bounds else {
707        return Err(crate::Error::new(
708            crate::ErrorKind::AtomNotFound(MEDIA_DATA),
709            "Missing necessary data, no media data (mdat) atom found",
710        ));
711    };
712
713    // update atom hierarchy
714    let mut changes = Vec::new();
715    if cfg.write_meta_items || cfg.write_chapter_list || cfg.write_chapter_track {
716        update_userdata(&mut reader, &mut changes, &mut moov, &mdat_bounds, userdata, cfg)?;
717    }
718
719    for trak in moov.trak.iter() {
720        if !trak.state.is_existing() {
721            continue;
722        }
723
724        let Some(stbl) = (trak.mdia.as_ref())
725            .filter(|mdia| mdia.state.is_existing())
726            .and_then(|mdia| mdia.minf.as_ref())
727            .filter(|minf| minf.state.is_existing())
728            .and_then(|minf| minf.stbl.as_ref())
729            .filter(|stbl| stbl.state.is_existing())
730        else {
731            continue;
732        };
733
734        if let Some(co64) = &stbl.co64 {
735            if let State::Existing(bounds) = &co64.state {
736                let offsets = co64.offsets.get_or_read(&mut reader)?;
737                let offsets = ChunkOffsets::Co64(offsets);
738                let update = UpdateChunkOffsets { bounds, offsets };
739                changes.push(Change::UpdateChunkOffset(update));
740            }
741        }
742        if let Some(stco) = &stbl.stco {
743            if let State::Existing(bounds) = &stco.state {
744                let offsets = stco.offsets.get_or_read(&mut reader)?;
745                let offsets = ChunkOffsets::Stco(offsets);
746                let update = UpdateChunkOffsets { bounds, offsets };
747                changes.push(Change::UpdateChunkOffset(update));
748            }
749        }
750    }
751
752    // collect changes
753    moov.collect_changes(0, 0, &mut changes);
754
755    changes.sort_by(|a, b| {
756        a.old_pos().cmp(&b.old_pos()).then_with(|| {
757            // Fix sorting of zero-sized changes in child atoms.
758            // ```md
759            // moov
760            // ├─ trak
761            // │  └─ tkhd (to be inserted)
762            // └─ udta    (to be inserted)
763            // ```
764            // Given the hierarchy above, if the changes would order the insertion of the `udta`
765            // atom before the `tkhd` the hierarchy would be invalid.
766            a.level().cmp(&b.level()).reverse()
767        })
768    });
769
770    // read moved data
771    let old_file_len = reader.seek(SeekFrom::End(0))?;
772    let mut moved_data = Vec::new();
773    let len_diff = {
774        let mut current_shift: i64 = 0;
775        let mut changes_iter = changes.iter().peekable();
776
777        while let Some(change) = changes_iter.next() {
778            current_shift += change.len_diff();
779
780            let data_pos = change.old_end();
781            let data_end = changes_iter.peek().map_or(old_file_len, |next| next.old_pos());
782            let data_len = data_end - data_pos;
783
784            if data_len > 0 && current_shift != 0 {
785                let new_pos = (data_pos as i64 + current_shift) as u64;
786                let mut data = vec![0; data_len as usize];
787                reader.seek(SeekFrom::Start(data_pos))?;
788                reader.read_exact(&mut data)?;
789
790                moved_data.push(MovedData { new_pos, data });
791            }
792        }
793        current_shift
794    };
795
796    // no more reading from here on
797    drop(reader);
798
799    // adjust the file length
800    let new_file_len = (old_file_len as i64 + len_diff) as u64;
801    file.set_len(new_file_len)?;
802
803    let writer = &mut BufWriter::new(file);
804
805    // write moved data
806    for d in moved_data {
807        writer.seek(SeekFrom::Start(d.new_pos))?;
808        writer.write_all(&d.data)?;
809    }
810
811    // write changes
812    let append_idx = changes.iter().position(|c| matches!(c, Change::AppendMdat(..)));
813    let end = append_idx.unwrap_or(changes.len());
814    let shifting_changes = &changes[..end];
815
816    let mut pos_shift = 0;
817    for c in changes.iter() {
818        let new_pos = c.old_pos() as i64 + pos_shift;
819        writer.seek(SeekFrom::Start(new_pos as u64))?;
820
821        match c {
822            Change::UpdateLen(u) => u.update_len(writer)?,
823            Change::UpdateChunkOffset(u) => u.offsets.update_offsets(writer, shifting_changes)?,
824            Change::Remove(_) => (),
825            Change::Replace(r) => r.atom.write(writer, shifting_changes)?,
826            Change::Insert(i) => i.atom.write(writer, shifting_changes)?,
827            Change::RemoveMdat(_, _) => (),
828            Change::AppendMdat(_, d) => writer.write_all(d)?,
829        }
830
831        pos_shift += c.len_diff();
832    }
833
834    writer.flush()?;
835
836    Ok(())
837}
838
839fn update_userdata<'a>(
840    reader: &mut (impl Read + Seek),
841    changes: &mut Vec<Change<'a>>,
842    moov: &mut Moov<'a>,
843    mdat_bounds: &'a AtomBounds,
844    userdata: &'a Userdata,
845    cfg: &WriteConfig,
846) -> crate::Result<()> {
847    let udta = moov.udta.get_or_insert_default();
848
849    // item list (ilst)
850    if cfg.write_meta_items {
851        let meta = udta.meta.get_or_insert_default();
852        meta.hdlr.get_or_insert_with(Hdlr::meta);
853
854        let ilst = meta.ilst.get_or_insert_default();
855        ilst.state.replace_existing();
856        ilst.data = Cow::Borrowed(&userdata.meta_items);
857    }
858
859    // chapter list
860    if cfg.write_chapter_list {
861        match udta.chpl.as_mut() {
862            None if userdata.chapter_list.is_empty() => (),
863            Some(chpl) if userdata.chapter_list.is_empty() => {
864                chpl.state.remove_existing();
865            }
866            _ => {
867                let chpl_timescale = cfg.chpl_timescale.fixed_or_mvhd(moov.mvhd.timescale);
868                let chpl = udta.chpl.get_or_insert_default();
869                chpl.state.replace_existing();
870                chpl.data = ChplData::Borrowed(chpl_timescale, &userdata.chapter_list);
871            }
872        }
873    }
874
875    // chapter tracks
876    'chapter_track: {
877        if !cfg.write_chapter_track {
878            break 'chapter_track;
879        }
880
881        // https://developer.apple.com/documentation/quicktime-file-format/chapter_lists
882        // > If more than one enabled track includes a 'chap' track reference,
883        // > QuickTime uses the first chapter list that it finds.
884        let chapter_trak_idx = moov.trak.iter().find_map(|trak| {
885            let chap = trak.tref.as_ref().and_then(|tref| tref.chap.as_ref())?;
886            moov.trak.iter().position(|trak| chap.chapter_ids.contains(&trak.tkhd.id))
887        });
888
889        if userdata.chapter_track.is_empty() {
890            let Some(idx) = chapter_trak_idx else {
891                // avoid doing redundant work
892                break 'chapter_track;
893            };
894
895            // remove chapter track
896            let chapter_trak = &mut moov.trak[idx];
897            chapter_trak.state.remove_existing();
898
899            // remove all chap track references
900            for trak in moov.trak.iter_mut() {
901                let Some(tref) = &mut trak.tref else {
902                    continue;
903                };
904                let State::Existing(tref_bounds) = &tref.state else {
905                    continue;
906                };
907
908                let Some(chap) = &mut tref.chap else {
909                    continue;
910                };
911                let State::Existing(chap_bounds) = &chap.state else {
912                    continue;
913                };
914
915                if tref_bounds.content_len() == chap_bounds.len() {
916                    tref.state.remove_existing();
917                } else {
918                    chap.state.remove_existing();
919                }
920            }
921
922            break 'chapter_track;
923        }
924
925        // generate chapter track sample table
926        let mut new_chapter_media_data = Vec::new();
927        let duration = moov.mvhd.duration;
928        let chapter_timescale = moov.mvhd.timescale;
929        let chunk_offsets = vec![mdat_bounds.end()];
930        let mut sample_sizes = Vec::with_capacity(userdata.chapter_track.len());
931        let mut time_to_samples = Vec::with_capacity(userdata.chapter_track.len());
932        let mut chapters_iter = userdata.chapter_track.iter().peekable();
933        while let Some(c) = chapters_iter.next() {
934            let c_duration = match chapters_iter.peek() {
935                Some(next) => {
936                    let c_duration = next.start.saturating_sub(c.start);
937                    unscale_duration(chapter_timescale, c_duration)
938                }
939                None => {
940                    let start = unscale_duration(chapter_timescale, c.start);
941                    duration.saturating_sub(start)
942                }
943            };
944
945            time_to_samples.push(SttsItem {
946                sample_count: 1,
947                sample_duration: c_duration as u32,
948            });
949
950            const ENCD: [u8; 12] = [
951                0, 0, 0, 12, // size
952                b'e', b'n', b'c', b'd', // fourcc
953                0, 0, 1, 0, // content
954            ];
955            let title_len = c.title.len().min(u16::MAX as usize);
956            let sample_size = 2 + title_len + ENCD.len();
957            sample_sizes.push(sample_size as u32);
958
959            new_chapter_media_data.write_be_u16(title_len as u16).ok();
960            new_chapter_media_data.write_utf8(&c.title[..title_len]).ok();
961            new_chapter_media_data.extend(ENCD);
962        }
963
964        let chapter_trak = match chapter_trak_idx {
965            Some(idx) => &mut moov.trak[idx],
966            None => {
967                let new_id = moov.trak.iter().map(|t| t.tkhd.id).max().unwrap() + 1;
968
969                // add chap track reference to all other tracks
970                for trak in moov.trak.iter_mut() {
971                    let tref = trak.tref.get_or_insert_default();
972                    let chap = tref.chap.get_or_insert_default();
973                    chap.state.replace_existing();
974                    chap.chapter_ids = vec![new_id];
975                }
976
977                // add chapter track
978                moov.trak.push_and_get(Trak {
979                    state: State::Insert,
980                    tkhd: Tkhd { version: 0, flags: [0, 0, 0], id: new_id, duration },
981                    ..Default::default()
982                })
983            }
984        };
985
986        let mdia = chapter_trak.mdia.get_or_insert_with(|| Mdia {
987            state: State::Insert,
988            mdhd: Mdhd {
989                timescale: chapter_timescale,
990                duration,
991                ..Default::default()
992            },
993            ..Default::default()
994        });
995
996        mdia.hdlr.get_or_insert_with(Hdlr::text_mdia);
997        let minf = mdia.minf.get_or_insert_default();
998
999        let gmhd = minf.gmhd.get_or_insert_default();
1000        gmhd.gmin.get_or_insert_with(Gmin::chapter);
1001        gmhd.text.get_or_insert_with(Text::media_information_chapter);
1002
1003        let dinf = minf.dinf.get_or_insert_default();
1004        let dref = dinf.dref.get_or_insert_default();
1005        dref.url.get_or_insert_with(Url::track);
1006
1007        let stbl = minf.stbl.get_or_insert_default();
1008        let stsd = stbl.stsd.get_or_insert_default();
1009        stsd.text.get_or_insert_with(Text::media_chapter);
1010
1011        let stts = stbl.stts.get_or_insert_default();
1012        stts.state.replace_existing();
1013        stts.items = Table::Full(time_to_samples);
1014
1015        let stsc = stbl.stsc.get_or_insert_default();
1016        stsc.state.replace_existing();
1017        let prev_stsc = std::mem::replace(
1018            &mut stsc.items,
1019            Table::Full(vec![StscItem {
1020                first_chunk: 1,
1021                samples_per_chunk: sample_sizes.len() as u32,
1022                sample_description_id: 1,
1023            }]),
1024        );
1025
1026        let stsz = stbl.stsz.get_or_insert_default();
1027        stsz.state.replace_existing();
1028        let prev_stsz_uniform_sample_size = std::mem::replace(&mut stsz.uniform_sample_size, 0);
1029        let prev_stsz_sizes = std::mem::replace(&mut stsz.sizes, Table::Full(sample_sizes));
1030
1031        let prev_stsc = prev_stsc.get_or_read(reader)?;
1032        let prev_stsz_sizes = prev_stsz_sizes.get_or_read(reader)?;
1033
1034        let prev_stco = stbl.stco.as_mut().map(|stco| {
1035            stco.state.remove_existing();
1036            std::mem::take(&mut stco.offsets)
1037        });
1038
1039        let co64 = stbl.co64.get_or_insert_default();
1040        co64.state.replace_existing();
1041        let prev_co64 = std::mem::replace(&mut co64.offsets, Table::Full(chunk_offsets));
1042
1043        // remove previous chapter data from the mdat atom
1044        if co64.state.has_existed() {
1045            let prev_co64 = prev_co64.get_or_read(reader)?;
1046            remove_chapter_media_data(
1047                changes,
1048                &prev_co64,
1049                &prev_stsc,
1050                prev_stsz_uniform_sample_size,
1051                &prev_stsz_sizes,
1052            )?;
1053        } else if let Some(prev_stco) = prev_stco {
1054            let prev_stco = prev_stco.get_or_read(reader)?;
1055            remove_chapter_media_data(
1056                changes,
1057                &prev_stco,
1058                &prev_stsc,
1059                prev_stsz_uniform_sample_size,
1060                &prev_stsz_sizes,
1061            )?;
1062        }
1063
1064        if !new_chapter_media_data.is_empty() {
1065            changes.push(Change::AppendMdat(mdat_bounds.end(), new_chapter_media_data));
1066        }
1067
1068        let len_diff = changes.iter().map(|c| c.len_diff()).sum();
1069        if len_diff != 0 {
1070            changes.push(Change::UpdateLen(UpdateAtomLen {
1071                bounds: mdat_bounds,
1072                fourcc: MEDIA_DATA,
1073                len_diff,
1074            }));
1075        }
1076    }
1077
1078    Ok(())
1079}
1080
1081fn remove_chapter_media_data<T: ChunkOffsetInt>(
1082    changes: &mut Vec<Change<'_>>,
1083    offsets: &[T],
1084    stsc: &[StscItem],
1085    stsz_uniform_size: u32,
1086    stsz_sizes: &[u32],
1087) -> crate::Result<()> {
1088    let mut stco_idx = 0;
1089    let mut stsz_iter = stsz_sizes.iter();
1090
1091    for (stsc_idx, stsc_item) in stsc.iter().enumerate() {
1092        let stco_end_idx = match stsc.get(stsc_idx + 1) {
1093            Some(next_stsc_item) => {
1094                let end_idx = next_stsc_item.first_chunk as usize;
1095                if end_idx > offsets.len() {
1096                    return Err(crate::Error::new(
1097                        ErrorKind::InvalidSampleTable,
1098                        "Sample table sample to chunk (stsc) first chunk index is out of bounds",
1099                    ));
1100                }
1101                end_idx
1102            }
1103            None => offsets.len(),
1104        };
1105
1106        for o in offsets[stco_idx..stco_end_idx].iter().copied() {
1107            let offset = o.into();
1108            let chunk_size = if stsz_uniform_size != 0 {
1109                stsc_item.samples_per_chunk as u64 * stsz_uniform_size as u64
1110            } else {
1111                let mut chunk_size = 0;
1112                for _ in 0..stsc_item.samples_per_chunk {
1113                    let Some(size) = stsz_iter.next() else {
1114                        return Err(crate::Error::new(
1115                            ErrorKind::InvalidSampleTable,
1116                            "Missing sample table sample size (stsz) item",
1117                        ));
1118                    };
1119                    chunk_size += *size as u64;
1120                }
1121                chunk_size
1122            };
1123
1124            changes.push(Change::RemoveMdat(offset, chunk_size));
1125        }
1126
1127        stco_idx = stco_end_idx;
1128    }
1129
1130    Ok(())
1131}