1use 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
97pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub enum ChplTimescale {
234 Fixed(NonZeroU32),
236 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#[derive(Clone, Debug, PartialEq, Eq)]
264pub struct ReadConfig {
265 pub read_meta_items: bool,
267 pub read_image_data: bool,
270 pub read_chapter_list: bool,
272 pub read_chapter_track: bool,
274 pub read_audio_info: bool,
277 pub chpl_timescale: ChplTimescale,
279}
280
281impl ReadConfig {
282 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 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 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 let mut chapter_track = Vec::new();
382 if cfg.read_chapter_track {
383 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 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#[derive(Clone, Debug, PartialEq, Eq)]
588pub struct WriteConfig {
589 pub write_meta_items: bool,
591 pub write_chapter_list: bool,
593 pub write_chapter_track: bool,
595 pub chpl_timescale: ChplTimescale,
597}
598
599impl WriteConfig {
600 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 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
638pub trait StorageFile: Read + Write + Seek {
643 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 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 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 a.level().cmp(&b.level()).reverse()
767 })
768 });
769
770 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 drop(reader);
798
799 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 for d in moved_data {
807 writer.seek(SeekFrom::Start(d.new_pos))?;
808 writer.write_all(&d.data)?;
809 }
810
811 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 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 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_track: {
877 if !cfg.write_chapter_track {
878 break 'chapter_track;
879 }
880
881 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 break 'chapter_track;
893 };
894
895 let chapter_trak = &mut moov.trak[idx];
897 chapter_trak.state.remove_existing();
898
899 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 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, b'e', b'n', b'c', b'd', 0, 0, 1, 0, ];
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 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 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 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}