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 file_path(item: &str) -> PathBuf {
122        if let Some(env) = std::env::var_os("MP4_ASSETS_DIR") {
123            PathBuf::from(env).join(item)
124        } else {
125            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("../../assets/{item}"))
126        }
127    }
128
129    fn transmux_sample(sample_name: &str, skip_insta: bool) -> io::Result<()> {
130        let test_name = sample_name.split('.').next().unwrap();
131
132        let data = std::fs::read(file_path(sample_name))?;
133        let mut reader = scuffle_bytes_util::zero_copy::Slice::from(&data[..]);
134        let og_file = IsobmffFile::deserialize(&mut reader)?;
135        if !skip_insta {
136            insta::assert_debug_snapshot!(test_name, og_file);
137        }
138        assert_eq!(og_file.size(), data.len());
139
140        let mut out_data = Vec::new();
141        og_file.serialize(&mut out_data)?;
142        assert_eq!(out_data.len(), data.len());
143
144        let mut reader = scuffle_bytes_util::zero_copy::Slice::from(&out_data[..]);
145        let file = IsobmffFile::deserialize(&mut reader)?;
146        if !skip_insta {
147            insta::assert_debug_snapshot!(test_name, file);
148        }
149
150        Ok(())
151    }
152
153    #[test]
154    fn avc_aac_sample() {
155        transmux_sample("avc_aac.mp4", false).unwrap();
156    }
157
158    #[test]
159    fn avc_aac_large_sample() {
160        transmux_sample("avc_aac_large.mp4", false).unwrap();
161    }
162
163    #[test]
164    fn avc_aac_fragmented_sample() {
165        transmux_sample("avc_aac_fragmented.mp4", false).unwrap();
166    }
167
168    #[test]
169    fn avc_aac_keyframes_sample() {
170        transmux_sample("avc_aac_keyframes.mp4", false).unwrap();
171    }
172
173    #[test]
174    fn hevc_aac_fragmented_sample() {
175        // Skip the insta snapshot because it would be too big
176        transmux_sample("hevc_aac_fragmented.mp4", true).unwrap();
177    }
178
179    #[test]
180    fn av1_aac_fragmented_sample() {
181        // Skip the insta snapshot because it would be too big
182        transmux_sample("av1_aac_fragmented.mp4", true).unwrap();
183    }
184}