Skip to main content

mp4_edit/atom/leaf/
tkhd.rs

1use bon::Builder;
2use std::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 TKHD: FourCC = FourCC::new(b"tkhd");
15
16const IDENTITY_MATRIX: [i32; 9] = [0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000];
17
18#[derive(Default, Debug, Clone, Builder)]
19pub struct TrackHeaderAtom {
20    /// Version of the tkhd atom format (0 or 1)
21    #[builder(default = 0)]
22    pub version: u8,
23    /// Flags for the tkhd atom (bit flags for track properties)
24    #[builder(default = [0, 0, 7])]
25    pub flags: [u8; 3],
26    /// When the track was created (seconds since Jan 1, 1904 UTC)
27    #[builder(default = mp4_timestamp_now())]
28    pub creation_time: u64,
29    /// When the track was last modified (seconds since Jan 1, 1904 UTC)
30    #[builder(default = mp4_timestamp_now())]
31    pub modification_time: u64,
32    /// Unique identifier for this track within the movie
33    pub track_id: u32,
34    /// Duration of the track in movie timescale units
35    pub duration: u64,
36    /// Playback layer (lower numbers are closer to viewer)
37    #[builder(default = 0)]
38    pub layer: i16,
39    /// Audio balance or stereo balance (-1.0 = left, 0.0 = center, 1.0 = right)
40    #[builder(default = 0)]
41    pub alternate_group: i16,
42    /// Audio volume level (1.0 = full volume, 0.0 = muted)
43    #[builder(default = 1.0)]
44    pub volume: f32,
45    /// 3x3 transformation matrix for video display positioning/rotation
46    ///
47    /// `None` if matrix is empty or is the identity matrix
48    pub matrix: Option<[i32; 9]>,
49    /// Track width in pixels (fixed-point 16.16)
50    #[builder(default = 0.0)]
51    pub width: f32,
52    /// Track height in pixels (fixed-point 16.16)
53    #[builder(default = 0.0)]
54    pub height: f32,
55}
56
57impl TrackHeaderAtom {
58    pub fn duration(&self, movie_timescale: u64) -> Duration {
59        unscaled_duration(self.duration, movie_timescale)
60    }
61
62    pub fn update_duration<F>(&mut self, movie_timescale: u64, mut closure: F) -> &mut Self
63    where
64        F: FnMut(Duration) -> Duration,
65    {
66        self.duration = scaled_duration(closure(self.duration(movie_timescale)), movie_timescale);
67        self
68    }
69}
70
71impl ParseAtomData for TrackHeaderAtom {
72    fn parse_atom_data(atom_type: FourCC, input: &[u8]) -> Result<Self, ParseError> {
73        crate::atom::util::parser::assert_atom_type!(atom_type, TKHD);
74        use crate::atom::util::parser::stream;
75        use winnow::Parser;
76        Ok(parser::parse_tkhd_data.parse(stream(input))?)
77    }
78}
79
80impl SerializeAtom for TrackHeaderAtom {
81    fn atom_type(&self) -> FourCC {
82        TKHD
83    }
84
85    fn into_body_bytes(self) -> Vec<u8> {
86        serializer::serialize_tkhd_data(self)
87    }
88}
89
90mod serializer {
91    use crate::atom::{
92        tkhd::IDENTITY_MATRIX,
93        util::serializer::{fixed_point_16x16, fixed_point_8x8},
94    };
95
96    use super::TrackHeaderAtom;
97
98    pub fn serialize_tkhd_data(tkhd: TrackHeaderAtom) -> Vec<u8> {
99        let mut data = Vec::new();
100
101        let version: u8 = if tkhd.version == 1
102            || tkhd.creation_time > u64::from(u32::MAX)
103            || tkhd.modification_time > u64::from(u32::MAX)
104            || tkhd.duration > u64::from(u32::MAX)
105        {
106            1
107        } else {
108            0
109        };
110
111        let be_u32_or_u64 = |v: u64| match version {
112            0 => u32::try_from(v).unwrap().to_be_bytes().to_vec(),
113            1 => v.to_be_bytes().to_vec(),
114            _ => unreachable!(),
115        };
116
117        data.push(version);
118        data.extend(tkhd.flags);
119        data.extend(be_u32_or_u64(tkhd.creation_time));
120        data.extend(be_u32_or_u64(tkhd.modification_time));
121        data.extend(tkhd.track_id.to_be_bytes());
122        data.extend([0u8; 4]); // reserved
123        data.extend(be_u32_or_u64(tkhd.duration));
124        data.extend([0u8; 8]); // reserved
125        data.extend(tkhd.layer.to_be_bytes());
126        data.extend(tkhd.alternate_group.to_be_bytes());
127        data.extend(fixed_point_8x8(tkhd.volume));
128        data.extend([0u8; 2]); // reserved
129        data.extend(
130            tkhd.matrix
131                .unwrap_or(IDENTITY_MATRIX)
132                .into_iter()
133                .flat_map(|v| v.to_be_bytes()),
134        );
135        data.extend(fixed_point_16x16(tkhd.width));
136        data.extend(fixed_point_16x16(tkhd.height));
137
138        data
139    }
140}
141
142mod parser {
143    use winnow::{
144        binary::{be_i16, be_i32, be_u32, be_u64},
145        combinator::{seq, trace},
146        error::{StrContext, StrContextValue},
147        ModalResult, Parser,
148    };
149
150    use super::TrackHeaderAtom;
151    use crate::atom::{
152        tkhd::IDENTITY_MATRIX,
153        util::parser::{
154            be_u32_as_u64, byte_array, fixed_array, fixed_point_16x16, fixed_point_8x8, flags3,
155            version, Stream,
156        },
157    };
158
159    pub fn parse_tkhd_data(input: &mut Stream<'_>) -> ModalResult<TrackHeaderAtom> {
160        let be_u32_or_u64 = |version: u8| {
161            let be_u64_type_fix =
162                |input: &mut Stream<'_>| -> ModalResult<u64> { be_u64.parse_next(input) };
163            match version {
164                0 => be_u32_as_u64,
165                1 => be_u64_type_fix,
166                _ => unreachable!(),
167            }
168        };
169
170        trace(
171            "tkhd",
172            seq!(TrackHeaderAtom {
173                version: version
174                    .verify(|version| *version <= 1)
175                    .context(StrContext::Expected(StrContextValue::Description(
176                        "expected version 0 or 1"
177                    ))),
178                flags: flags3,
179                creation_time: be_u32_or_u64(version).context(StrContext::Label("creation_time")),
180                modification_time: be_u32_or_u64(version).context(StrContext::Label("modification_time")),
181                track_id: be_u32.context(StrContext::Label("track_id")),
182                _: byte_array::<4>.context(StrContext::Label("reserved_1")),
183                duration: be_u32_or_u64(version),
184                _: byte_array::<8>.context(StrContext::Label("reserved_2")),
185                layer: be_i16.context(StrContext::Label("layer")),
186                alternate_group: be_i16.context(StrContext::Label("alternate_group")),
187                volume: fixed_point_8x8.context(StrContext::Label("volume")),
188                _: byte_array::<2>.context(StrContext::Label("reserved_3")),
189                matrix: matrix.context(StrContext::Label("matrix")),
190                width: fixed_point_16x16.context(StrContext::Label("width")),
191                height: fixed_point_16x16.context(StrContext::Label("height")),
192            })
193            .context(StrContext::Label("tkhd")),
194        )
195        .parse_next(input)
196    }
197
198    fn matrix(input: &mut Stream<'_>) -> ModalResult<Option<[i32; 9]>> {
199        trace(
200            "matrix",
201            fixed_array(be_i32).map(|matrix: [i32; 9]| {
202                if is_empty_matrix(&matrix) {
203                    None
204                } else {
205                    Some(matrix)
206                }
207            }),
208        )
209        .parse_next(input)
210    }
211
212    fn is_empty_matrix(matrix: &[i32; 9]) -> bool {
213        let empty = [0; 9];
214        let identity = IDENTITY_MATRIX;
215        matrix == &empty || matrix == &identity
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use crate::atom::test_utils::test_atom_roundtrip;
223
224    /// Test round-trip for all available tkhd test data files
225    #[test]
226    fn test_tkhd_roundtrip() {
227        test_atom_roundtrip::<TrackHeaderAtom>(TKHD);
228    }
229}