Skip to main content

mp4_edit/atom/leaf/
mdhd.rs

1use bon::Builder;
2use std::{fmt, time::Duration};
3
4use crate::{
5    atom::{
6        util::{mp4_timestamp_now, scaled_duration, unscaled_duration},
7        FourCC,
8    },
9    parser::ParseAtomData,
10    writer::SerializeAtom,
11    ParseError,
12};
13
14pub const MDHD: FourCC = FourCC::new(b"mdhd");
15
16macro_rules! define_language_code_enum {
17    ($( #[$meta:meta] )* $name:ident { $( $( #[$tag:meta] )* $variant:ident => $chars:literal ),+ $(,)? }) => {
18        $(#[$meta])*
19        pub enum $name {
20            $( $( #[$tag] )* $variant ),+,
21            Other([u8; 3]),
22        }
23
24        impl $name {
25            fn from_chars(chars: &[u8; 3]) -> Self {
26                match chars {
27                    $( $chars => Self::$variant ),+,
28                    _ => Self::Other(*chars),
29                }
30            }
31
32            fn as_chars(&self) -> &[u8; 3] {
33                match self {
34                    $( Self::$variant => $chars ),+,
35                    Self::Other(chars) => chars,
36                }
37            }
38        }
39
40        impl fmt::Display for $name {
41            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42                let chars = match self {
43                    $( Self::$variant => $chars ),+,
44                    Self::Other(chars) => &[chars[0] as u8, chars[1] as u8, chars[2] as u8],
45                };
46                write!(f, "{}{}{}", chars[0] as char, chars[1] as char, chars[2] as char)
47            }
48        }
49    };
50}
51
52define_language_code_enum!(
53    /// Language code (ISO 639-2/T language code)
54    #[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
55    #[non_exhaustive]
56    LanguageCode {
57        English => b"eng",
58        Spanish => b"spa",
59        French => b"fra",
60        German => b"deu",
61        Italian => b"ita",
62        Japanese => b"jpn",
63        Korean => b"kor",
64        Chinese => b"chi",
65        Russian => b"rus",
66        Arabic => b"ara",
67        Portuguese => b"por",
68        #[default]
69        Undetermined => b"und",
70    }
71);
72
73impl From<u16> for LanguageCode {
74    fn from(packed: u16) -> Self {
75        let char1 = (((packed >> 10) & 0x1F) + 0x60) as u8;
76        let char2 = (((packed >> 5) & 0x1F) + 0x60) as u8;
77        let char3 = ((packed & 0x1F) + 0x60) as u8;
78
79        let lang: [u8; 3] = [char1, char2, char3];
80
81        Self::from_chars(&lang)
82    }
83}
84
85impl From<LanguageCode> for u16 {
86    fn from(value: LanguageCode) -> Self {
87        let chars = value.as_chars();
88
89        let char1_bits = (chars[0] - 0x60) & 0x1F;
90        let char2_bits = (chars[1] - 0x60) & 0x1F;
91        let char3_bits = (chars[2] - 0x60) & 0x1F;
92
93        (u16::from(char1_bits) << 10) | (u16::from(char2_bits) << 5) | u16::from(char3_bits)
94    }
95}
96
97#[derive(Default, Debug, Clone, Builder)]
98pub struct MediaHeaderAtom {
99    /// Version of the mdhd atom format (0 or 1)
100    #[builder(default = 0)]
101    pub version: u8,
102    /// Flags for the mdhd atom (usually all zeros)
103    #[builder(default = [0u8; 3])]
104    pub flags: [u8; 3],
105    /// Creation time (seconds since midnight, Jan. 1, 1904, UTC)
106    #[builder(default = mp4_timestamp_now())]
107    pub creation_time: u64,
108    /// Modification time (seconds since midnight, Jan. 1, 1904, UTC)
109    #[builder(default = mp4_timestamp_now())]
110    pub modification_time: u64,
111    /// Media timescale (number of time units per second)
112    pub timescale: u32,
113    /// Duration of media (in timescale units)
114    pub duration: u64,
115    /// Language code (ISO 639-2/T language code)
116    #[builder(default = LanguageCode::Undetermined)]
117    pub language: LanguageCode,
118    /// Pre-defined value (should be 0)
119    #[builder(default = 0)]
120    pub pre_defined: u16,
121}
122
123impl MediaHeaderAtom {
124    pub fn duration(&self) -> Duration {
125        unscaled_duration(self.duration, u64::from(self.timescale))
126    }
127
128    pub fn update_duration<F>(&mut self, mut closure: F) -> &mut Self
129    where
130        F: FnMut(Duration) -> Duration,
131    {
132        self.duration = scaled_duration(closure(self.duration()), u64::from(self.timescale));
133        self
134    }
135}
136
137impl ParseAtomData for MediaHeaderAtom {
138    fn parse_atom_data(atom_type: FourCC, input: &[u8]) -> Result<Self, ParseError> {
139        crate::atom::util::parser::assert_atom_type!(atom_type, MDHD);
140        use crate::atom::util::parser::stream;
141        use winnow::Parser;
142        Ok(parser::parse_mdhd_data.parse(stream(input))?)
143    }
144}
145
146impl SerializeAtom for MediaHeaderAtom {
147    fn atom_type(&self) -> FourCC {
148        MDHD
149    }
150
151    fn into_body_bytes(self) -> Vec<u8> {
152        serializer::serialize_mdhd_atom(self)
153    }
154}
155
156mod serializer {
157    use super::MediaHeaderAtom;
158
159    pub fn serialize_mdhd_atom(mdhd: MediaHeaderAtom) -> Vec<u8> {
160        let mut data = Vec::new();
161
162        let version: u8 = if mdhd.version == 1
163            || mdhd.creation_time > u64::from(u32::MAX)
164            || mdhd.modification_time > u64::from(u32::MAX)
165            || mdhd.duration > u64::from(u32::MAX)
166        {
167            1
168        } else {
169            0
170        };
171
172        let be_u32_or_u64 = |v: u64| match version {
173            0 => u32::try_from(v).unwrap().to_be_bytes().to_vec(),
174            1 => v.to_be_bytes().to_vec(),
175            _ => unreachable!(),
176        };
177
178        data.extend(version.to_be_bytes());
179        data.extend(mdhd.flags);
180        data.extend(be_u32_or_u64(mdhd.creation_time));
181        data.extend(be_u32_or_u64(mdhd.modification_time));
182        data.extend(mdhd.timescale.to_be_bytes());
183        data.extend(be_u32_or_u64(mdhd.duration));
184        data.extend(u16::from(mdhd.language).to_be_bytes());
185        data.extend(mdhd.pre_defined.to_be_bytes());
186
187        data
188    }
189}
190
191mod parser {
192    use winnow::{
193        binary::{be_u16, be_u32, be_u64},
194        combinator::{seq, trace},
195        error::{StrContext, StrContextValue},
196        ModalResult, Parser,
197    };
198
199    use super::{LanguageCode, MediaHeaderAtom};
200    use crate::atom::util::parser::{be_u32_as_u64, flags3, version, Stream};
201
202    pub fn parse_mdhd_data(input: &mut Stream<'_>) -> ModalResult<MediaHeaderAtom> {
203        let be_u32_or_u64 = |version: u8| {
204            let be_u64_type_fix =
205                |input: &mut Stream<'_>| -> ModalResult<u64> { be_u64.parse_next(input) };
206            match version {
207                0 => be_u32_as_u64,
208                1 => be_u64_type_fix,
209                _ => unreachable!(),
210            }
211        };
212
213        trace(
214            "mdhd",
215            seq!(MediaHeaderAtom {
216                version: version
217                    .verify(|version| *version <= 1)
218                    .context(StrContext::Expected(StrContextValue::Description(
219                        "expected version 0 or 1"
220                    ))),
221                flags: flags3,
222                creation_time: be_u32_or_u64(version),
223                modification_time: be_u32_or_u64(version),
224                timescale: be_u32,
225                duration: be_u32_or_u64(version),
226                language: be_u16.map(LanguageCode::from),
227                pre_defined: be_u16,
228            })
229            .context(StrContext::Label("mdhd")),
230        )
231        .parse_next(input)
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use crate::atom::test_utils::test_atom_roundtrip;
239
240    /// Test round-trip for all available mdhd test data files
241    #[test]
242    fn test_mdhd_roundtrip() {
243        test_atom_roundtrip::<MediaHeaderAtom>(MDHD);
244    }
245}