Skip to main content

mp4_atom/moov/trak/edts/
elst.rs

1use crate::*;
2
3ext! {
4    name: Elst,
5    versions: [0, 1],
6    flags: {}
7}
8
9#[derive(Debug, Clone, PartialEq, Eq, Default)]
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11pub struct Elst {
12    pub entries: Vec<ElstEntry>,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, Default)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17pub struct ElstEntry {
18    pub segment_duration: u64,
19    /// Start time within the media of this edit, in media-timescale units
20    /// (composition time). Signed on the wire (ISO/IEC 14496-12 §8.6.6): `None`
21    /// is the `-1` "empty edit" sentinel -- a dwell with no media, used to signal
22    /// an initial presentation offset -- and `Some(t)` is a real, non-negative
23    /// media time. `t` must fit in an `i64` (the on-wire field is signed) to encode.
24    pub media_time: Option<u64>,
25    // Signed 16.16 fixed-point playback rate (int(16).int(16) per §8.6.6). Always
26    // present -- there is no "absent" sentinel; media_rate == 0 is a meaningful value
27    // (a dwell, i.e. a frozen frame) rather than absence -- so it is not optional.
28    pub media_rate: FixedPoint<i16>,
29}
30
31impl AtomExt for Elst {
32    type Ext = ElstExt;
33
34    const KIND_EXT: FourCC = FourCC::new(b"elst");
35
36    fn decode_body_ext<B: Buf>(buf: &mut B, ext: ElstExt) -> Result<Self> {
37        let entry_count = u32::decode(buf)?;
38
39        let mut entries = Vec::new();
40        for _ in 0..entry_count {
41            // media_time is signed; decode it as i32/i64 so the -1 empty-edit
42            // sentinel sign-extends instead of becoming +4294967295.
43            let (segment_duration, media_time) = match ext.version {
44                ElstVersion::V1 => (u64::decode(buf)?, i64::decode(buf)?),
45                ElstVersion::V0 => (u32::decode(buf)? as u64, i32::decode(buf)? as i64),
46            };
47
48            // -1 is the only defined negative (empty edit); real media times are
49            // non-negative. Anything below -1 is out of spec.
50            let media_time = match media_time {
51                -1 => None,
52                t if t >= 0 => Some(t as u64),
53                _ => {
54                    return Err(Error::Unsupported(
55                        "elst media_time must be -1 or non-negative",
56                    ))
57                }
58            };
59
60            entries.push(ElstEntry {
61                segment_duration,
62                media_time,
63                media_rate: FixedPoint::decode(buf)?,
64            });
65        }
66
67        Ok(Elst { entries })
68    }
69
70    fn encode_body_ext<B: BufMut>(&self, buf: &mut B) -> Result<ElstExt> {
71        // On the wire media_time is signed: None is the -1 empty edit, and a real
72        // media time must fit the signed 64-bit field.
73        fn wire_media_time(media_time: Option<u64>) -> Result<i64> {
74            match media_time {
75                None => Ok(-1),
76                Some(t) => i64::try_from(t)
77                    .map_err(|_| Error::Unsupported("elst media_time exceeds i64::MAX")),
78            }
79        }
80
81        // Prefer version 0 (32-bit) when every value fits: it matches what muxers
82        // typically emit (so a V0 source round-trips byte-for-byte) and keeps the box
83        // compact. segment_duration is unsigned int(32) but media_time is signed
84        // int(32), so their ceilings differ. Out-of-range media_time is validated (and
85        // -1 / None handled) below in the encode pass.
86        let use_v0 = !self.entries.iter().any(|e| {
87            e.segment_duration > u32::MAX as u64
88                || e.media_time.is_some_and(|t| t > i32::MAX as u64)
89        });
90
91        (self.entries.len() as u32).encode(buf)?;
92
93        for entry in &self.entries {
94            let media_time = wire_media_time(entry.media_time)?;
95            if use_v0 {
96                (entry.segment_duration as u32).encode(buf)?;
97                (media_time as i32).encode(buf)?;
98            } else {
99                entry.segment_duration.encode(buf)?;
100                media_time.encode(buf)?;
101            }
102            entry.media_rate.encode(buf)?;
103        }
104
105        Ok(if use_v0 {
106            ElstVersion::V0.into()
107        } else {
108            ElstVersion::V1.into()
109        })
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_elst32() {
119        let expected = Elst {
120            entries: vec![ElstEntry {
121                segment_duration: 634634,
122                media_time: Some(0),
123                media_rate: 1.into(),
124            }],
125        };
126        let mut buf = Vec::new();
127        expected.encode(&mut buf).unwrap();
128        assert_eq!(buf[8], 0, "values within 32 bits encode as version 0");
129
130        let mut buf = buf.as_ref();
131        let decoded = Elst::decode(&mut buf).unwrap();
132        assert_eq!(decoded, expected);
133    }
134
135    #[test]
136    fn test_elst64() {
137        let expected = Elst {
138            entries: vec![ElstEntry {
139                segment_duration: 5_000_000_000,
140                media_time: Some(5_000_000_000),
141                media_rate: 1.into(),
142            }],
143        };
144        let mut buf = Vec::new();
145        expected.encode(&mut buf).unwrap();
146        assert_eq!(buf[8], 1, "values beyond 32 bits force version 1");
147
148        let mut buf = buf.as_ref();
149        let decoded = Elst::decode(&mut buf).unwrap();
150        assert_eq!(decoded, expected);
151    }
152
153    // media_time is a *signed* int(32) in version 0, so any value above i32::MAX (up to
154    // i64::MAX) must fall back to version 1 -- encoding it as a 32-bit int would overflow
155    // into a negative media_time. i32::MAX + 1 is the smallest such value.
156    #[test]
157    fn test_elst_media_time_above_i32_forces_v1() {
158        let expected = Elst {
159            entries: vec![ElstEntry {
160                segment_duration: 0,
161                media_time: Some(i32::MAX as u64 + 1),
162                media_rate: 1.into(),
163            }],
164        };
165        let mut buf = Vec::new();
166        expected.encode(&mut buf).unwrap();
167        assert_eq!(buf[8], 1, "media_time above i32::MAX must use version 1");
168
169        let decoded = Elst::decode(&mut buf.as_ref()).unwrap();
170        assert_eq!(decoded, expected);
171    }
172
173    // Regression: the "empty edit" (media_time = -1) must round-trip as `None`.
174    // Decoding the 32-bit form as unsigned (the old bug) turned -1 into +4294967295,
175    // which shifted the track by 2^32 media ticks and left video/audio on disjoint
176    // timelines -- a black screen in the browser's Media Source Extensions.
177    #[test]
178    fn test_elst_empty_edit_sentinel() {
179        let expected = Elst {
180            entries: vec![
181                ElstEntry {
182                    segment_duration: 23,
183                    media_time: None,
184                    media_rate: 1.into(),
185                },
186                ElstEntry {
187                    segment_duration: 0,
188                    media_time: Some(0),
189                    media_rate: 1.into(),
190                },
191            ],
192        };
193        let mut buf = Vec::new();
194        expected.encode(&mut buf).unwrap();
195        let decoded = Elst::decode(&mut buf.as_ref()).unwrap();
196        assert_eq!(decoded, expected);
197    }
198
199    // Decode a real version-0 edit list (as written by ffmpeg / L-SMASH etc.) whose
200    // media_time is the 32-bit -1 (0xFFFFFFFF), and confirm we recover `None` and
201    // preserve it through a re-encode (the decode -> re-encode path a repackager takes).
202    #[test]
203    fn test_elst_v0_neg1_decode_reencode() {
204        let raw: &[u8] = &[
205            0x00, 0x00, 0x00, 0x1C, // box size = 28
206            b'e', b'l', b's', b't', //
207            0x00, 0x00, 0x00, 0x00, // version 0, flags 0
208            0x00, 0x00, 0x00, 0x01, // entry_count = 1
209            0x00, 0x00, 0x03, 0xE8, // segment_duration = 1000
210            0xFF, 0xFF, 0xFF, 0xFF, // media_time = -1 (32-bit)
211            0x00, 0x01, 0x00, 0x00, // media_rate = 1.0
212        ];
213        let decoded = Elst::decode(&mut &raw[..]).unwrap();
214        assert_eq!(decoded.entries[0].media_time, None);
215
216        // A version-0 input round-trips byte-for-byte: the empty edit stays a 32-bit
217        // -1, not a re-widened (and previously corrupted) 64-bit value.
218        let mut buf = Vec::new();
219        decoded.encode(&mut buf).unwrap();
220        assert_eq!(buf.as_slice(), raw);
221    }
222
223    // A media_time below -1 is out of spec: decoding must fail rather than invent a
224    // value. Covered for both on-wire widths (version 0 int(32), version 1 int(64)).
225    #[test]
226    fn test_elst_negative_media_time_rejected_v0() {
227        let raw: &[u8] = &[
228            0x00, 0x00, 0x00, 0x1C, // box size = 28
229            b'e', b'l', b's', b't', //
230            0x00, 0x00, 0x00, 0x00, // version 0, flags 0
231            0x00, 0x00, 0x00, 0x01, // entry_count = 1
232            0x00, 0x00, 0x03, 0xE8, // segment_duration = 1000
233            0xFF, 0xFF, 0xFF, 0xFE, // media_time = -2 (invalid, 32-bit)
234            0x00, 0x01, 0x00, 0x00, // media_rate = 1.0
235        ];
236        assert!(Elst::decode(&mut &raw[..]).is_err());
237    }
238
239    #[test]
240    fn test_elst_negative_media_time_rejected_v1() {
241        let raw: &[u8] = &[
242            0x00, 0x00, 0x00, 0x24, // box size = 36
243            b'e', b'l', b's', b't', //
244            0x01, 0x00, 0x00, 0x00, // version 1, flags 0
245            0x00, 0x00, 0x00, 0x01, // entry_count = 1
246            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
247            0xE8, // segment_duration = 1000 (64-bit)
248            0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
249            0xFE, // media_time = -2 (invalid, 64-bit)
250            0x00, 0x01, 0x00, 0x00, // media_rate = 1.0
251        ];
252        assert!(Elst::decode(&mut &raw[..]).is_err());
253    }
254
255    // A media_time that does not fit the signed on-wire field cannot be encoded.
256    #[test]
257    fn test_elst_media_time_too_large_rejected() {
258        let elst = Elst {
259            entries: vec![ElstEntry {
260                segment_duration: 0,
261                media_time: Some(u64::MAX),
262                media_rate: 1.into(),
263            }],
264        };
265        let mut buf = Vec::new();
266        assert!(elst.encode(&mut buf).is_err());
267    }
268}