Skip to main content

mp4_edit/atom/leaf/
elst.rs

1use std::time::Duration;
2
3use bon::bon;
4
5use crate::{
6    atom::{util::scaled_duration, FourCC},
7    parser::ParseAtomData,
8    writer::SerializeAtom,
9    ParseError,
10};
11
12pub const ELST: FourCC = FourCC::new(b"elst");
13
14#[derive(Default, Debug, Clone)]
15pub struct EditListAtom {
16    /// Version of the elst atom format (0 or 1)
17    pub version: u8,
18    /// Flags for the elst atom (usually all zeros)
19    pub flags: [u8; 3],
20    /// List of edit entries
21    pub entries: Vec<EditEntry>,
22}
23
24impl EditListAtom {
25    pub fn new(entries: impl Into<Vec<EditEntry>>) -> Self {
26        Self {
27            entries: entries.into(),
28            ..Default::default()
29        }
30    }
31
32    pub fn replace_entries(&mut self, entries: impl Into<Vec<EditEntry>>) -> &mut Self {
33        self.entries = entries.into();
34        self
35    }
36}
37
38pub struct MediaTime {
39    /// None implies -1, or a gap
40    start_offset: Option<Duration>,
41}
42
43impl Default for MediaTime {
44    fn default() -> Self {
45        Self {
46            start_offset: Some(Duration::from_secs(0)),
47        }
48    }
49}
50
51impl MediaTime {
52    /// media time starting at a specified offset
53    pub fn new(start_offset: Duration) -> Self {
54        Self {
55            start_offset: Some(start_offset),
56        }
57    }
58
59    /// media time representing a gap in playback
60    pub fn new_empty() -> Self {
61        Self { start_offset: None }
62    }
63
64    pub fn scaled(&self, movie_timescale: u64) -> i64 {
65        match self.start_offset {
66            Some(start_offset) if start_offset.is_zero() => 0,
67            Some(start_offset) => i64::try_from(scaled_duration(start_offset, movie_timescale))
68                .expect("scaled duration should fit in i64"),
69            None => -1,
70        }
71    }
72}
73
74#[derive(Debug, Clone)]
75pub struct EditEntry {
76    /// Duration of this edit segment (in movie timescale units)
77    pub segment_duration: u64,
78    /// Starting time within the media (in media timescale units)
79    /// -1 indicates an empty edit (no media displayed)
80    pub media_time: i64,
81    /// Playback rate for this segment (1.0 = normal speed)
82    pub media_rate: f32,
83}
84
85#[bon]
86impl EditEntry {
87    #[builder]
88    pub fn new(
89        movie_timescale: u64,
90        segment_duration: Duration,
91        #[builder(default = Default::default())] media_time: MediaTime,
92        #[builder(default = 1.0)] media_rate: f32,
93    ) -> Self {
94        Self {
95            segment_duration: scaled_duration(segment_duration, movie_timescale),
96            media_time: media_time.scaled(movie_timescale),
97            media_rate,
98        }
99    }
100}
101
102impl ParseAtomData for EditListAtom {
103    fn parse_atom_data(atom_type: FourCC, input: &[u8]) -> Result<Self, ParseError> {
104        crate::atom::util::parser::assert_atom_type!(atom_type, ELST);
105        use crate::atom::util::parser::stream;
106        use winnow::Parser;
107        Ok(parser::parse_elst_data.parse(stream(input))?)
108    }
109}
110
111impl SerializeAtom for EditListAtom {
112    fn atom_type(&self) -> FourCC {
113        ELST
114    }
115
116    fn into_body_bytes(self) -> Vec<u8> {
117        serializer::serialize_elst_atom(self)
118    }
119}
120
121mod serializer {
122    use crate::atom::{elst::EditEntry, util::serializer::fixed_point_16x16};
123
124    use super::EditListAtom;
125
126    pub fn serialize_elst_atom(atom: EditListAtom) -> Vec<u8> {
127        vec![
128            version(atom.version),
129            flags(atom.flags),
130            entry_count(atom.entries.len()),
131            entries(atom.version, atom.entries),
132        ]
133        .into_iter()
134        .flatten()
135        .collect()
136    }
137
138    fn version(version: u8) -> Vec<u8> {
139        vec![version]
140    }
141
142    fn flags(flags: [u8; 3]) -> Vec<u8> {
143        flags.to_vec()
144    }
145
146    fn entry_count(count: usize) -> Vec<u8> {
147        u32::try_from(count)
148            .expect("entries len should fit in u32")
149            .to_be_bytes()
150            .to_vec()
151    }
152
153    fn entries(version: u8, entries: Vec<EditEntry>) -> Vec<u8> {
154        match version {
155            1 => entries.into_iter().flat_map(entry_64).collect(),
156            _ => entries.into_iter().flat_map(entry_32).collect(),
157        }
158    }
159
160    fn entry_32(entry: EditEntry) -> Vec<u8> {
161        vec![
162            u32::try_from(entry.segment_duration)
163                .expect("segument_duration should fit in u32")
164                .to_be_bytes()
165                .to_vec(),
166            i32::try_from(entry.media_time)
167                .expect("media_time should fit in i32")
168                .to_be_bytes()
169                .to_vec(),
170            media_rate(entry.media_rate),
171        ]
172        .into_iter()
173        .flatten()
174        .collect()
175    }
176
177    fn entry_64(entry: EditEntry) -> Vec<u8> {
178        vec![
179            entry.segment_duration.to_be_bytes().to_vec(),
180            entry.media_time.to_be_bytes().to_vec(),
181            media_rate(entry.media_rate),
182        ]
183        .into_iter()
184        .flatten()
185        .collect()
186    }
187
188    fn media_rate(media_rate: f32) -> Vec<u8> {
189        fixed_point_16x16(media_rate).to_vec()
190    }
191}
192
193mod parser {
194    use winnow::{
195        binary::{be_i64, be_u32, be_u64, length_repeat},
196        combinator::{seq, trace},
197        error::StrContext,
198        ModalResult, Parser,
199    };
200
201    use super::EditListAtom;
202    use crate::atom::{
203        elst::EditEntry,
204        util::parser::{be_i32_as, be_u32_as, fixed_point_16x16, flags3, version, Stream},
205    };
206
207    pub fn parse_elst_data(input: &mut Stream<'_>) -> ModalResult<EditListAtom> {
208        trace(
209            "elst",
210            seq!(EditListAtom {
211                version: version,
212                flags: flags3,
213                entries: length_repeat(
214                    be_u32.context(StrContext::Label("entry_count")),
215                    match version {
216                        1 => entry_64,
217                        _ => entry_32,
218                    }
219                ),
220            })
221            .context(StrContext::Label("elst")),
222        )
223        .parse_next(input)
224    }
225
226    fn entry_32(input: &mut Stream<'_>) -> ModalResult<EditEntry> {
227        trace(
228            "entry_32",
229            seq!(EditEntry {
230                segment_duration: be_u32_as.context(StrContext::Label("segment_duration")),
231                media_time: be_i32_as.context(StrContext::Label("media_time")),
232                media_rate: media_rate,
233            })
234            .context(StrContext::Label("entry")),
235        )
236        .parse_next(input)
237    }
238
239    fn entry_64(input: &mut Stream<'_>) -> ModalResult<EditEntry> {
240        trace(
241            "entry_64",
242            seq!(EditEntry {
243                segment_duration: be_u64.context(StrContext::Label("segment_duration")),
244                media_time: be_i64.context(StrContext::Label("media_time")),
245                media_rate: media_rate,
246            })
247            .context(StrContext::Label("entry")),
248        )
249        .parse_next(input)
250    }
251
252    fn media_rate(input: &mut Stream<'_>) -> ModalResult<f32> {
253        trace(
254            "media_rate",
255            fixed_point_16x16.context(StrContext::Label("media_rate")),
256        )
257        .parse_next(input)
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use crate::atom::test_utils::test_atom_roundtrip;
265
266    /// Test round-trip for all available elst test data files
267    #[test]
268    fn test_elst_roundtrip() {
269        test_atom_roundtrip::<EditListAtom>(ELST);
270    }
271}