isobmff/
file.rs

1use std::fmt::Debug;
2
3use scuffle_bytes_util::zero_copy::{Deserialize, DeserializeSeed};
4
5use crate::boxes::{
6    ExtendedTypeBox, FileTypeBox, IdentifiedMediaDataBox, MediaDataBox, MetaBox, MovieBox, MovieFragmentBox,
7    MovieFragmentRandomAccessBox, OriginalFileTypeBox, ProducerReferenceTimeBox, ProgressiveDownloadInfoBox,
8    SegmentIndexBox, SegmentTypeBox, SubsegmentIndexBox,
9};
10use crate::{BoxHeader, BoxSize, BoxType, IsoBox, UnknownBox};
11
12/// Represents an ISO Base Media File Format (ISOBMFF) file.
13///
14/// This encapsulates all boxes that may be present in an ISOBMFF file.
15/// You can also use the boxes directly for more fine-grained control.
16#[derive(IsoBox, Debug, PartialEq, Eq)]
17#[iso_box(skip_impl(iso_box, deserialize), crate_path = crate)]
18pub struct IsobmffFile<'a> {
19    /// Optional [`FileTypeBox`].
20    ///
21    /// According to the official specification the [`FileTypeBox`] is mandatory
22    /// but in reality some files do not contain it. (e.g. recording of live streams)
23    #[iso_box(nested_box(collect))]
24    pub ftyp: Option<FileTypeBox>,
25    /// A list of [`ExtendedTypeBox`]es.
26    #[iso_box(nested_box(collect))]
27    pub etyp: Vec<ExtendedTypeBox<'a>>,
28    /// A list of [`OriginalFileTypeBox`]es.
29    #[iso_box(nested_box(collect))]
30    pub otyp: Vec<OriginalFileTypeBox<'a>>,
31    /// Optional [`ProgressiveDownloadInfoBox`].
32    #[iso_box(nested_box(collect))]
33    pub pdin: Option<ProgressiveDownloadInfoBox>,
34    /// Optional [`MovieBox`].
35    ///
36    /// According to the official specification the [`MovieBox`] is mandatory,
37    /// but in reality some files (e.g. HEIF) do not contain it.
38    /// Apparently it is possible for derived specifications to change the
39    /// rules of the base specification.
40    ///
41    /// See: <https://github.com/MPEGGroup/FileFormatConformance/issues/154>
42    #[iso_box(nested_box(collect))]
43    pub moov: Option<MovieBox<'a>>,
44    /// A list of [`MovieFragmentBox`]es.
45    #[iso_box(nested_box(collect))]
46    pub moof: Vec<MovieFragmentBox<'a>>,
47    /// A list of [`MediaDataBox`]es.
48    #[iso_box(nested_box(collect))]
49    pub mdat: Vec<MediaDataBox<'a>>,
50    /// A list of [`IdentifiedMediaDataBox`]es.
51    #[iso_box(nested_box(collect))]
52    pub imda: Vec<IdentifiedMediaDataBox<'a>>,
53    #[iso_box(nested_box(collect))]
54    /// Optional [`MetaBox`].
55    pub meta: Option<MetaBox<'a>>,
56    /// A list of [`SegmentTypeBox`]es.
57    #[iso_box(nested_box(collect))]
58    pub styp: Vec<SegmentTypeBox>,
59    /// A list of [`SegmentIndexBox`]es.
60    #[iso_box(nested_box(collect))]
61    pub sidx: Vec<SegmentIndexBox>,
62    /// A list of [`SubsegmentIndexBox`]es.
63    #[iso_box(nested_box(collect))]
64    pub ssix: Vec<SubsegmentIndexBox>,
65    /// A list of [`ProducerReferenceTimeBox`]es.
66    #[iso_box(nested_box(collect))]
67    pub prft: Vec<ProducerReferenceTimeBox>,
68    /// Any unknown boxes that were not recognized during deserialization.
69    #[iso_box(nested_box(collect_unknown))]
70    pub unknown_boxes: Vec<UnknownBox<'a>>,
71    /// Optional [`MovieFragmentRandomAccessBox`].
72    #[iso_box(nested_box(collect))]
73    pub mfra: Option<MovieFragmentRandomAccessBox>,
74}
75
76impl<'a> Deserialize<'a> for IsobmffFile<'a> {
77    fn deserialize<R>(reader: R) -> std::io::Result<Self>
78    where
79        R: scuffle_bytes_util::zero_copy::ZeroCopyReader<'a>,
80    {
81        Self::deserialize_seed(
82            reader,
83            BoxHeader {
84                size: BoxSize::ToEnd,
85                box_type: BoxType::FourCc(*b"root"),
86            },
87        )
88    }
89}
90
91// This trait is usually not implemented manually.
92// Since the file does not have a header, we need to implement it manually here.
93impl IsoBox for IsobmffFile<'_> {
94    const TYPE: BoxType = BoxType::Uuid(uuid::Uuid::nil());
95
96    fn add_header_size(payload_size: usize) -> usize {
97        // Return the payload size, because the file does not have a header
98        payload_size
99    }
100
101    fn serialize_box_header<W>(&self, _writer: W) -> std::io::Result<()>
102    where
103        W: std::io::Write,
104    {
105        // noop, because the file does not have a header
106        Ok(())
107    }
108}
109
110#[cfg(test)]
111#[cfg_attr(all(test, coverage_nightly), coverage(off))]
112mod tests {
113    use std::io;
114    use std::path::PathBuf;
115
116    use scuffle_bytes_util::zero_copy::{Deserialize, Serialize};
117
118    use super::IsobmffFile;
119    use crate::IsoSized;
120
121    fn transmux_sample(sample_name: &str, skip_insta: bool) -> io::Result<()> {
122        let test_name = sample_name.split('.').next().unwrap();
123
124        let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets");
125        let data = std::fs::read(dir.join(sample_name))?;
126        let mut reader = scuffle_bytes_util::zero_copy::Slice::from(&data[..]);
127        let og_file = IsobmffFile::deserialize(&mut reader)?;
128        if !skip_insta {
129            insta::assert_debug_snapshot!(test_name, og_file);
130        }
131        assert_eq!(og_file.size(), data.len());
132
133        let mut out_data = Vec::new();
134        og_file.serialize(&mut out_data)?;
135        assert_eq!(out_data.len(), data.len());
136
137        let mut reader = scuffle_bytes_util::zero_copy::Slice::from(&out_data[..]);
138        let file = IsobmffFile::deserialize(&mut reader)?;
139        if !skip_insta {
140            insta::assert_debug_snapshot!(test_name, file);
141        }
142
143        Ok(())
144    }
145
146    #[test]
147    fn avc_aac_sample() {
148        transmux_sample("avc_aac.mp4", false).unwrap();
149    }
150
151    #[test]
152    fn avc_aac_large_sample() {
153        transmux_sample("avc_aac_large.mp4", false).unwrap();
154    }
155
156    #[test]
157    fn avc_aac_fragmented_sample() {
158        transmux_sample("avc_aac_fragmented.mp4", false).unwrap();
159    }
160
161    #[test]
162    fn avc_aac_keyframes_sample() {
163        transmux_sample("avc_aac_keyframes.mp4", false).unwrap();
164    }
165
166    #[test]
167    fn hevc_aac_fragmented_sample() {
168        // Skip the insta snapshot because it would be too big
169        transmux_sample("hevc_aac_fragmented.mp4", true).unwrap();
170    }
171
172    #[test]
173    fn av1_aac_fragmented_sample() {
174        // Skip the insta snapshot because it would be too big
175        transmux_sample("av1_aac_fragmented.mp4", true).unwrap();
176    }
177}