Skip to main content

mp4_edit/atom/leaf/
mvhd.rs

1use bon::Builder;
2
3use std::time::Duration;
4
5use crate::{
6    atom::{
7        util::{mp4_timestamp_now, scaled_duration, unscaled_duration},
8        FourCC,
9    },
10    parser::ParseAtomData,
11    writer::SerializeAtom,
12    ParseError,
13};
14
15pub const MVHD: FourCC = FourCC::new(b"mvhd");
16
17#[derive(Debug, Clone, Builder)]
18pub struct MovieHeaderAtom {
19    /// Version of the mvhd atom format (0 or 1)
20    #[builder(default = 0)]
21    pub version: u8,
22    /// Flags for the mvhd atom (usually all zeros)
23    #[builder(default = [0u8; 3])]
24    pub flags: [u8; 3],
25    /// When the movie was created (seconds since Jan 1, 1904 UTC)
26    #[builder(default = mp4_timestamp_now())]
27    pub creation_time: u64,
28    /// When the movie was last modified (seconds since Jan 1, 1904 UTC)
29    #[builder(default = mp4_timestamp_now())]
30    pub modification_time: u64,
31    /// Number of time units per second (e.g., 90000 for 90kHz)
32    pub timescale: u32,
33    /// Duration of the movie in timescale units
34    pub duration: u64,
35    /// Playback rate (1.0 = normal speed, 2.0 = double speed)
36    #[builder(default = 1.0)]
37    pub rate: f32,
38    /// Audio volume level (1.0 = full volume, 0.0 = muted)
39    #[builder(default = 1.0)]
40    pub volume: f32,
41    #[builder(default)]
42    pub reserved: [u8; 10],
43    /// 3x3 transformation matrix for video display positioning/rotation
44    pub matrix: Option<[i32; 9]>,
45    /// Time when preview starts (in timescale units)
46    #[builder(default = 0)]
47    pub preview_time: u32,
48    /// Duration of the preview (in timescale units)
49    #[builder(default = 0)]
50    pub preview_duration: u32,
51    /// Time of poster frame to display when movie is not playing
52    #[builder(default = 0)]
53    pub poster_time: u32,
54    /// Start time of current selection (in timescale units)
55    #[builder(default = 0)]
56    pub selection_time: u32,
57    /// Duration of current selection (in timescale units)
58    #[builder(default = 0)]
59    pub selection_duration: u32,
60    /// Current playback time position (in timescale units)
61    #[builder(default = 0)]
62    pub current_time: u32,
63    /// ID to use for the next track added to this movie
64    pub next_track_id: u32,
65}
66
67impl MovieHeaderAtom {
68    pub fn update_duration<F>(&mut self, mut closure: F) -> &mut Self
69    where
70        F: FnMut(Duration) -> Duration,
71    {
72        self.duration = scaled_duration(
73            closure(unscaled_duration(self.duration, u64::from(self.timescale))),
74            u64::from(self.timescale),
75        );
76        self
77    }
78
79    pub fn duration(&self) -> Duration {
80        unscaled_duration(self.duration, u64::from(self.timescale))
81    }
82}
83
84impl ParseAtomData for MovieHeaderAtom {
85    fn parse_atom_data(atom_type: FourCC, input: &[u8]) -> Result<Self, ParseError> {
86        crate::atom::util::parser::assert_atom_type!(atom_type, MVHD);
87        use crate::atom::util::parser::stream;
88        use winnow::Parser;
89        Ok(parser::parse_mvhd_data.parse(stream(input))?)
90    }
91}
92
93impl SerializeAtom for MovieHeaderAtom {
94    fn atom_type(&self) -> FourCC {
95        MVHD
96    }
97
98    fn into_body_bytes(self) -> Vec<u8> {
99        serializer::serialize_mvhd_atom(self)
100    }
101}
102
103mod serializer {
104    use crate::atom::util::serializer::{fixed_point_16x16, fixed_point_8x8};
105
106    use super::MovieHeaderAtom;
107
108    pub fn serialize_mvhd_atom(mvhd: MovieHeaderAtom) -> Vec<u8> {
109        let mut data = Vec::new();
110
111        // Determine version based on whether values fit in 32-bit
112        let needs_64_bit = mvhd.creation_time > u64::from(u32::MAX)
113            || mvhd.modification_time > u64::from(u32::MAX)
114            || mvhd.duration > u64::from(u32::MAX);
115
116        let version: u8 = if needs_64_bit { 1 } else { 0 };
117
118        let be_u32_or_u64 = |v: u64| match version {
119            0 => u32::try_from(v).unwrap().to_be_bytes().to_vec(),
120            1 => v.to_be_bytes().to_vec(),
121            _ => unreachable!(),
122        };
123
124        data.extend(version.to_be_bytes());
125        data.extend(mvhd.flags);
126        data.extend(be_u32_or_u64(mvhd.creation_time));
127        data.extend(be_u32_or_u64(mvhd.modification_time));
128        data.extend(mvhd.timescale.to_be_bytes());
129        data.extend(be_u32_or_u64(mvhd.duration));
130        data.extend(fixed_point_16x16(mvhd.rate));
131        data.extend(fixed_point_8x8(mvhd.volume));
132        data.extend(mvhd.reserved);
133        if let Some(matrix) = mvhd.matrix {
134            for value in matrix {
135                data.extend(value.to_be_bytes());
136            }
137        }
138        data.extend_from_slice(&mvhd.preview_time.to_be_bytes());
139        data.extend_from_slice(&mvhd.preview_duration.to_be_bytes());
140        data.extend_from_slice(&mvhd.poster_time.to_be_bytes());
141        data.extend_from_slice(&mvhd.selection_time.to_be_bytes());
142        data.extend_from_slice(&mvhd.selection_duration.to_be_bytes());
143        data.extend_from_slice(&mvhd.current_time.to_be_bytes());
144        data.extend_from_slice(&mvhd.next_track_id.to_be_bytes());
145
146        data
147    }
148}
149
150mod parser {
151    use winnow::{
152        binary::{be_i32, be_u32, be_u64, u8},
153        combinator::{opt, seq, trace},
154        error::StrContext,
155        ModalResult, Parser,
156    };
157
158    use super::MovieHeaderAtom;
159    use crate::atom::util::parser::{
160        be_u32_as_u64, fixed_array, fixed_point_16x16, fixed_point_8x8, flags3, version_0_or_1,
161        Stream,
162    };
163
164    pub fn parse_mvhd_data(input: &mut Stream<'_>) -> ModalResult<MovieHeaderAtom> {
165        let be_u32_or_u64 = |version: u8| {
166            let be_u64_type_fix =
167                |input: &mut Stream<'_>| -> ModalResult<u64> { be_u64.parse_next(input) };
168            match version {
169                0 => be_u32_as_u64,
170                1 => be_u64_type_fix,
171                _ => unreachable!(),
172            }
173        };
174
175        trace(
176            "mvhd",
177            seq!(MovieHeaderAtom {
178                version: version_0_or_1,
179                flags: flags3,
180                creation_time: be_u32_or_u64(version).context(StrContext::Label("creation_time")),
181                modification_time: be_u32_or_u64(version)
182                    .context(StrContext::Label("modification_time")),
183                timescale: be_u32.context(StrContext::Label("timescale")),
184                duration: be_u32_or_u64(version).context(StrContext::Label("duration")),
185                rate: fixed_point_16x16.context(StrContext::Label("rate")),
186                volume: fixed_point_8x8.context(StrContext::Label("volume")),
187                reserved: fixed_array(u8).context(StrContext::Label("reserved")),
188                matrix: opt(fixed_array(be_i32)).context(StrContext::Label("matrix")),
189                preview_time: be_u32.context(StrContext::Label("preview_time")),
190                preview_duration: be_u32.context(StrContext::Label("preview_duration")),
191                poster_time: be_u32.context(StrContext::Label("poster_time")),
192                selection_time: be_u32.context(StrContext::Label("selection_time")),
193                selection_duration: be_u32.context(StrContext::Label("selection_duration")),
194                current_time: be_u32.context(StrContext::Label("current_time")),
195                next_track_id: be_u32.context(StrContext::Label("next_track_id")),
196            }),
197        )
198        .parse_next(input)
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use crate::atom::test_utils::test_atom_roundtrip;
206
207    /// Test round-trip for all available mvhd test data files
208    #[test]
209    fn test_mvhd_roundtrip() {
210        test_atom_roundtrip::<MovieHeaderAtom>(MVHD);
211    }
212}