Skip to main content

mp4_emsg/
emsg.rs

1//! `emsg` — MPEG-DASH Event Message Box.
2//!
3//! Field semantics: DASH-IF IOP Part 10 §6.1 / Table 6-2 (free). Box syntax
4//! (the `aligned(8) class EventMessageBox extends FullBox('emsg', version,
5//! flags = 0)` declaration, field ordering, and the null-terminated-string
6//! layout): ISO/IEC 23009-1 §5.10.3.3 (paid, **not vendored** — see the crate
7//! root caveat). Both the field set/types and the v0/v1 ordering difference are
8//! transcribed in `mp4-emsg/docs/emsg.md`.
9//!
10//! Wire layout (ISOBMFF `FullBox`):
11//!
12//! ```text
13//! size      u32      total box size in bytes (recomputed on serialize)
14//! type      'emsg'   the 4-byte box type
15//! version   u8       0 or 1
16//! flags     u24      0
17//! ── body (version 0, segment-relative) ──
18//! scheme_id_uri          null-terminated UTF-8
19//! value                  null-terminated UTF-8
20//! timescale              u32
21//! presentation_time_delta u32
22//! event_duration         u32
23//! id                     u32
24//! message_data[]         remaining bytes
25//! ── body (version 1, representation-relative) ──
26//! timescale              u32
27//! presentation_time      u64
28//! event_duration         u32
29//! id                     u32
30//! scheme_id_uri          null-terminated UTF-8
31//! value                  null-terminated UTF-8
32//! message_data[]         remaining bytes
33//! ```
34//!
35//! Note the **field ordering differs between v0 and v1** (strings first in v0;
36//! integers first / strings last in v1) per `docs/emsg.md`.
37
38use alloc::vec::Vec;
39
40use crate::error::{Error, Result};
41use crate::version::EmsgVersion;
42
43/// The 4-byte ISOBMFF box type for an Event Message Box.
44pub const EMSG_BOX_TYPE: [u8; 4] = *b"emsg";
45/// Size in bytes of the `FullBox` header: `size`(4) + `type`(4) + `version`(1)
46/// + `flags`(3).
47pub const FULLBOX_HEADER_LEN: usize = 12;
48/// `flags` value mandated for `emsg` (DASH-IF Part 10 / ISO box syntax: 0).
49pub const EMSG_FLAGS: u32 = 0;
50/// The single string terminator byte for the null-terminated UTF-8 fields.
51pub const STRING_TERMINATOR: u8 = 0x00;
52
53const U32_LEN: usize = 4;
54const U64_LEN: usize = 8;
55
56/// The version-discriminated presentation-time field (DASH-IF Part 10 Table
57/// 6-2): the *only* field whose type and reference point differ by version.
58///
59/// `version` is derived from this enum: [`PresentationTime::Delta`] ⇒ version 0
60/// (segment-relative), [`PresentationTime::Absolute`] ⇒ version 1
61/// (representation-relative).
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize))]
64#[non_exhaustive]
65pub enum PresentationTime {
66    /// **version 0**: `presentation_time_delta` (u32), relative to the
67    /// segment's earliest presentation time, in units of `timescale`.
68    Delta(u32),
69    /// **version 1**: `presentation_time` (u64), relative to `Period@start`
70    /// (adjusted by `@presentationTimeOffset`), in units of `timescale`.
71    Absolute(u64),
72}
73
74impl PresentationTime {
75    /// The `emsg` version implied by this presentation-time variant.
76    pub fn version(self) -> EmsgVersion {
77        match self {
78            PresentationTime::Delta(_) => EmsgVersion::SegmentRelative,
79            PresentationTime::Absolute(_) => EmsgVersion::RepresentationRelative,
80        }
81    }
82
83    /// Spec label for this presentation-time variant.
84    pub fn name(&self) -> &'static str {
85        match self {
86            PresentationTime::Delta(_) => "presentation_time_delta",
87            PresentationTime::Absolute(_) => "presentation_time",
88        }
89    }
90}
91
92dvb_common::impl_spec_display!(PresentationTime, Delta, Absolute);
93
94/// A parsed/owned MPEG-DASH Event Message Box (`emsg`).
95///
96/// Holds the typed `FullBox` body fields plus borrowed views of the two
97/// null-terminated UTF-8 strings and the opaque `message_data`. The box `size`
98/// and `version` are *not* stored: `size` is recomputed on serialize and
99/// `version` is derived from [`PresentationTime`], so the round-trip is driven
100/// entirely from the typed fields (no raw passthrough).
101#[derive(Debug, Clone, PartialEq, Eq)]
102#[cfg_attr(feature = "serde", derive(serde::Serialize))]
103pub struct EmsgBox<'a> {
104    /// `scheme_id_uri` (Table 6-2): the URI defining the event scheme. Stored
105    /// without the wire `0x00` terminator.
106    pub scheme_id_uri: &'a str,
107    /// `value` (Table 6-2): scheme-specific value, or the empty string when the
108    /// scheme defines none. Stored without the wire `0x00` terminator.
109    pub value: &'a str,
110    /// `timescale` (Table 6-2, u32): ticks per second for the time fields;
111    /// equal to the `mdhd` timescale of the Representation.
112    pub timescale: u32,
113    /// The presentation-time field — selects the box `version` (v0 delta vs v1
114    /// absolute).
115    pub presentation_time: PresentationTime,
116    /// `event_duration` (Table 6-2, u32): event duration in `timescale` units.
117    pub event_duration: u32,
118    /// `id` (Table 6-2, u32): unique identifier distinguishing events with the
119    /// same `scheme_id_uri`/`value` and detecting repetitions.
120    pub id: u32,
121    /// `message_data[]` (Table 6-2): the opaque scheme-specific payload (the
122    /// remaining bytes of the box). Empty when the scheme needs none.
123    #[cfg_attr(feature = "serde", serde(skip))]
124    pub message_data: &'a [u8],
125}
126
127impl<'a> EmsgBox<'a> {
128    /// The `version` of this box, derived from [`Self::presentation_time`].
129    pub fn version(&self) -> EmsgVersion {
130        self.presentation_time.version()
131    }
132
133    /// `true` if `scheme_id_uri` names a SCTE 35 scheme (`urn:scte:scte35...`),
134    /// in which case [`Self::message_data`] carries a SCTE 35
135    /// `splice_info_section` (SCTE 214-1 / ANSI/SCTE 35 — see crate root).
136    pub fn is_scte35(&self) -> bool {
137        self.scheme_id_uri.starts_with(SCTE35_SCHEME_PREFIX)
138    }
139
140    /// Total serialized size in bytes (the `size` field value).
141    pub fn serialized_len(&self) -> usize {
142        FULLBOX_HEADER_LEN + self.body_len()
143    }
144
145    /// Size in bytes of the version-specific body (everything after the
146    /// `FullBox` header).
147    fn body_len(&self) -> usize {
148        let strings = self.scheme_id_uri.len()
149            + 1 // scheme terminator
150            + self.value.len()
151            + 1; // value terminator
152        let ints = match self.presentation_time {
153            // timescale + delta + event_duration + id
154            PresentationTime::Delta(_) => U32_LEN * 4,
155            // timescale + presentation_time(64) + event_duration + id
156            PresentationTime::Absolute(_) => U32_LEN * 3 + U64_LEN,
157        };
158        strings + ints + self.message_data.len()
159    }
160
161    /// Parse an `emsg` box from the start of `data`. Requires the full box
162    /// (`size` bytes) to be present; trailing bytes beyond `size` are ignored.
163    pub fn parse(data: &'a [u8]) -> Result<Self> {
164        if data.len() < FULLBOX_HEADER_LEN {
165            return Err(Error::BufferTooShort {
166                need: FULLBOX_HEADER_LEN,
167                have: data.len(),
168                what: "emsg FullBox header",
169            });
170        }
171
172        let size = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
173        let box_type = [data[4], data[5], data[6], data[7]];
174        if box_type != EMSG_BOX_TYPE {
175            return Err(Error::NotEmsg { found: box_type });
176        }
177        let version = data[8];
178        // flags = data[9..12]; mandated 0, but parsed permissively (some muxers
179        // are sloppy) — not stored since it is recomputed as 0 on serialize.
180
181        if (size as usize) < FULLBOX_HEADER_LEN {
182            return Err(Error::InvalidSize {
183                size,
184                reason: "size smaller than the FullBox header (0/1 large-size forms unsupported)",
185            });
186        }
187        if size as usize > data.len() {
188            return Err(Error::InvalidSize {
189                size,
190                reason: "size exceeds available bytes",
191            });
192        }
193
194        let version = EmsgVersion::from_u8(version).ok_or(Error::UnsupportedVersion { version })?;
195
196        // The box body region is [FULLBOX_HEADER_LEN, size).
197        let body = &data[FULLBOX_HEADER_LEN..size as usize];
198
199        match version {
200            EmsgVersion::SegmentRelative => Self::parse_v0(body),
201            EmsgVersion::RepresentationRelative => Self::parse_v1(body),
202        }
203    }
204
205    /// version 0 body: scheme\0 value\0 timescale u32 ptd u32 dur u32 id u32
206    /// message_data[].
207    fn parse_v0(body: &'a [u8]) -> Result<Self> {
208        let (scheme_id_uri, rest) = parse_cstr(body, "scheme_id_uri")?;
209        let (value, rest) = parse_cstr(rest, "value")?;
210
211        let need = U32_LEN * 4;
212        if rest.len() < need {
213            return Err(Error::BufferTooShort {
214                need,
215                have: rest.len(),
216                what: "emsg v0 integer fields",
217            });
218        }
219        let timescale = read_u32(&rest[0..U32_LEN]);
220        let delta = read_u32(&rest[U32_LEN..U32_LEN * 2]);
221        let event_duration = read_u32(&rest[U32_LEN * 2..U32_LEN * 3]);
222        let id = read_u32(&rest[U32_LEN * 3..U32_LEN * 4]);
223        let message_data = &rest[need..];
224
225        Ok(EmsgBox {
226            scheme_id_uri,
227            value,
228            timescale,
229            presentation_time: PresentationTime::Delta(delta),
230            event_duration,
231            id,
232            message_data,
233        })
234    }
235
236    /// version 1 body: timescale u32 presentation_time u64 dur u32 id u32
237    /// scheme\0 value\0 message_data[].
238    fn parse_v1(body: &'a [u8]) -> Result<Self> {
239        let need = U32_LEN + U64_LEN + U32_LEN + U32_LEN;
240        if body.len() < need {
241            return Err(Error::BufferTooShort {
242                need,
243                have: body.len(),
244                what: "emsg v1 integer fields",
245            });
246        }
247        let timescale = read_u32(&body[0..U32_LEN]);
248        let presentation_time = read_u64(&body[U32_LEN..U32_LEN + U64_LEN]);
249        let mut off = U32_LEN + U64_LEN;
250        let event_duration = read_u32(&body[off..off + U32_LEN]);
251        off += U32_LEN;
252        let id = read_u32(&body[off..off + U32_LEN]);
253        off += U32_LEN;
254
255        let (scheme_id_uri, rest) = parse_cstr(&body[off..], "scheme_id_uri")?;
256        let (value, message_data) = parse_cstr(rest, "value")?;
257
258        Ok(EmsgBox {
259            scheme_id_uri,
260            value,
261            timescale,
262            presentation_time: PresentationTime::Absolute(presentation_time),
263            event_duration,
264            id,
265            message_data,
266        })
267    }
268
269    /// Serialize the box into `out`, recomputing the `size` field and writing
270    /// `flags = 0`. Returns the number of bytes written.
271    pub fn serialize_into(&self, out: &mut [u8]) -> Result<usize> {
272        let total = self.serialized_len();
273        if out.len() < total {
274            return Err(Error::OutputBufferTooSmall {
275                need: total,
276                have: out.len(),
277            });
278        }
279        if total > u32::MAX as usize {
280            return Err(Error::FieldTooWide {
281                what: "size",
282                value: total as u64,
283                bits: 32,
284            });
285        }
286
287        // size u32, type 'emsg', version u8, flags u24.
288        out[0..U32_LEN].copy_from_slice(&(total as u32).to_be_bytes());
289        out[4..8].copy_from_slice(&EMSG_BOX_TYPE);
290        out[8] = self.version().to_u8();
291        // flags = 0 (3 bytes).
292        out[9] = 0;
293        out[10] = 0;
294        out[11] = 0;
295
296        let mut off = FULLBOX_HEADER_LEN;
297        match self.presentation_time {
298            PresentationTime::Delta(delta) => {
299                // strings first.
300                off = write_cstr(out, off, self.scheme_id_uri);
301                off = write_cstr(out, off, self.value);
302                off = write_u32(out, off, self.timescale);
303                off = write_u32(out, off, delta);
304                off = write_u32(out, off, self.event_duration);
305                off = write_u32(out, off, self.id);
306            }
307            PresentationTime::Absolute(pt) => {
308                // integers first, strings last.
309                off = write_u32(out, off, self.timescale);
310                off = write_u64(out, off, pt);
311                off = write_u32(out, off, self.event_duration);
312                off = write_u32(out, off, self.id);
313                off = write_cstr(out, off, self.scheme_id_uri);
314                off = write_cstr(out, off, self.value);
315            }
316        }
317        out[off..off + self.message_data.len()].copy_from_slice(self.message_data);
318        off += self.message_data.len();
319        debug_assert_eq!(off, total);
320        Ok(off)
321    }
322
323    /// Serialize into a freshly allocated `Vec`.
324    pub fn to_vec(&self) -> Result<Vec<u8>> {
325        let mut out = alloc::vec![0u8; self.serialized_len()];
326        self.serialize_into(&mut out)?;
327        Ok(out)
328    }
329}
330
331/// The SCTE 35 scheme-URI prefix carried in `emsg.scheme_id_uri` (e.g.
332/// `urn:scte:scte35:2013:bin`), per SCTE 214-1 / DASH-IF Part 10 §7.3, §9.2.5.
333pub const SCTE35_SCHEME_PREFIX: &str = "urn:scte:scte35";
334
335/// Parse a null-terminated UTF-8 string from the front of `data`, returning the
336/// decoded `&str` (without the terminator) and the remaining bytes.
337fn parse_cstr<'a>(data: &'a [u8], field: &'static str) -> Result<(&'a str, &'a [u8])> {
338    let term = data
339        .iter()
340        .position(|&b| b == STRING_TERMINATOR)
341        .ok_or(Error::InvalidString {
342            field,
343            reason: "missing null terminator",
344        })?;
345    let s = core::str::from_utf8(&data[..term]).map_err(|_| Error::InvalidString {
346        field,
347        reason: "invalid UTF-8",
348    })?;
349    Ok((s, &data[term + 1..]))
350}
351
352/// Write `s` followed by a null terminator at `off`; returns the new offset.
353fn write_cstr(out: &mut [u8], off: usize, s: &str) -> usize {
354    let bytes = s.as_bytes();
355    out[off..off + bytes.len()].copy_from_slice(bytes);
356    let term = off + bytes.len();
357    out[term] = STRING_TERMINATOR;
358    term + 1
359}
360
361fn read_u32(b: &[u8]) -> u32 {
362    u32::from_be_bytes([b[0], b[1], b[2], b[3]])
363}
364
365fn read_u64(b: &[u8]) -> u64 {
366    u64::from_be_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
367}
368
369fn write_u32(out: &mut [u8], off: usize, v: u32) -> usize {
370    out[off..off + U32_LEN].copy_from_slice(&v.to_be_bytes());
371    off + U32_LEN
372}
373
374fn write_u64(out: &mut [u8], off: usize, v: u64) -> usize {
375    out[off..off + U64_LEN].copy_from_slice(&v.to_be_bytes());
376    off + U64_LEN
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382    use alloc::vec;
383
384    // v0: construct → serialize → exact wire bytes → reparse → equal.
385    #[test]
386    fn v0_exact_wire_bytes_round_trip() {
387        let msg = [0xDEu8, 0xAD, 0xBE, 0xEF];
388        let b = EmsgBox {
389            scheme_id_uri: "urn:example:scheme",
390            value: "",
391            timescale: 90_000,
392            presentation_time: PresentationTime::Delta(0x0001_0203),
393            event_duration: 0xFFFF_FFFF,
394            id: 0x0000_002A,
395            message_data: &msg,
396        };
397        assert_eq!(b.version(), EmsgVersion::SegmentRelative);
398
399        let out = b.to_vec().unwrap();
400
401        // Header.
402        assert_eq!(&out[0..4], &(out.len() as u32).to_be_bytes());
403        assert_eq!(&out[4..8], b"emsg");
404        assert_eq!(out[8], 0); // version 0
405        assert_eq!(&out[9..12], &[0, 0, 0]); // flags
406
407        // Body: strings first.
408        let scheme = b"urn:example:scheme\x00";
409        assert_eq!(&out[12..12 + scheme.len()], scheme);
410        let mut off = 12 + scheme.len();
411        assert_eq!(out[off], 0x00); // empty value terminator
412        off += 1;
413        assert_eq!(&out[off..off + 4], &90_000u32.to_be_bytes());
414        off += 4;
415        assert_eq!(&out[off..off + 4], &0x0001_0203u32.to_be_bytes()); // delta
416        off += 4;
417        assert_eq!(&out[off..off + 4], &0xFFFF_FFFFu32.to_be_bytes());
418        off += 4;
419        assert_eq!(&out[off..off + 4], &0x0000_002Au32.to_be_bytes());
420        off += 4;
421        assert_eq!(&out[off..], &msg);
422
423        assert_eq!(EmsgBox::parse(&out).unwrap(), b);
424    }
425
426    // v1: construct → serialize → exact wire bytes → reparse → equal.
427    #[test]
428    fn v1_exact_wire_bytes_round_trip() {
429        let msg = [0x01u8, 0x02];
430        let b = EmsgBox {
431            scheme_id_uri: "urn:scte:scte35:2013:bin",
432            value: "1001",
433            timescale: 1000,
434            presentation_time: PresentationTime::Absolute(0x0102_0304_0506_0708),
435            event_duration: 250,
436            id: 7,
437            message_data: &msg,
438        };
439        assert_eq!(b.version(), EmsgVersion::RepresentationRelative);
440        assert!(b.is_scte35());
441
442        let out = b.to_vec().unwrap();
443
444        assert_eq!(out[8], 1); // version 1
445                               // Body: integers first.
446        let mut off = 12;
447        assert_eq!(&out[off..off + 4], &1000u32.to_be_bytes());
448        off += 4;
449        assert_eq!(&out[off..off + 8], &0x0102_0304_0506_0708u64.to_be_bytes());
450        off += 8;
451        assert_eq!(&out[off..off + 4], &250u32.to_be_bytes());
452        off += 4;
453        assert_eq!(&out[off..off + 4], &7u32.to_be_bytes());
454        off += 4;
455        // Then strings.
456        let scheme = b"urn:scte:scte35:2013:bin\x00";
457        assert_eq!(&out[off..off + scheme.len()], scheme);
458        off += scheme.len();
459        let value = b"1001\x00";
460        assert_eq!(&out[off..off + value.len()], value);
461        off += value.len();
462        assert_eq!(&out[off..], &msg);
463
464        assert_eq!(EmsgBox::parse(&out).unwrap(), b);
465    }
466
467    // The v0/v1 field-order difference: the SAME logical fields produce
468    // DIFFERENT body byte orderings, yet both round-trip.
469    #[test]
470    fn v0_v1_field_order_differs_but_both_round_trip() {
471        let msg = [0xAAu8, 0xBB];
472        let v0 = EmsgBox {
473            scheme_id_uri: "s",
474            value: "v",
475            timescale: 0x1111_1111,
476            presentation_time: PresentationTime::Delta(0x2222_2222),
477            event_duration: 0x3333_3333,
478            id: 0x4444_4444,
479            message_data: &msg,
480        };
481        let v1 = EmsgBox {
482            scheme_id_uri: "s",
483            value: "v",
484            timescale: 0x1111_1111,
485            presentation_time: PresentationTime::Absolute(0x2222_2222),
486            event_duration: 0x3333_3333,
487            id: 0x4444_4444,
488            message_data: &msg,
489        };
490
491        let o0 = v0.to_vec().unwrap();
492        let o1 = v1.to_vec().unwrap();
493
494        // v0 has the strings right after the header; v1 has the timescale.
495        assert_eq!(&o0[12..14], b"s\x00");
496        assert_eq!(&o1[12..16], &0x1111_1111u32.to_be_bytes());
497        // The two encodings of "the same logical fields" are NOT byte-equal.
498        assert_ne!(o0, o1);
499
500        // Both round-trip to their originals.
501        assert_eq!(EmsgBox::parse(&o0).unwrap(), v0);
502        assert_eq!(EmsgBox::parse(&o1).unwrap(), v1);
503    }
504
505    // Mutation bite: changing a field changes the wire bytes.
506    #[test]
507    fn mutating_a_field_changes_wire_bytes() {
508        let mk = |id: u32| EmsgBox {
509            scheme_id_uri: "urn:x",
510            value: "",
511            timescale: 48_000,
512            presentation_time: PresentationTime::Delta(10),
513            event_duration: 20,
514            id,
515            message_data: &[],
516        };
517        let a = mk(1).to_vec().unwrap();
518        let b = mk(2).to_vec().unwrap();
519        assert_ne!(a, b);
520        // Same length (id is a fixed field), but differing bytes.
521        assert_eq!(a.len(), b.len());
522    }
523
524    #[test]
525    fn rejects_non_emsg_type() {
526        let mut data = vec![0u8; FULLBOX_HEADER_LEN];
527        data[0..4].copy_from_slice(&(FULLBOX_HEADER_LEN as u32).to_be_bytes());
528        data[4..8].copy_from_slice(b"moof");
529        assert!(matches!(EmsgBox::parse(&data), Err(Error::NotEmsg { .. })));
530    }
531
532    #[test]
533    fn rejects_unsupported_version() {
534        let b = EmsgBox {
535            scheme_id_uri: "x",
536            value: "",
537            timescale: 1,
538            presentation_time: PresentationTime::Delta(0),
539            event_duration: 0,
540            id: 0,
541            message_data: &[],
542        };
543        let mut out = b.to_vec().unwrap();
544        out[8] = 9; // bad version
545        assert!(matches!(
546            EmsgBox::parse(&out),
547            Err(Error::UnsupportedVersion { version: 9 })
548        ));
549    }
550
551    #[test]
552    fn rejects_size_overrun() {
553        let mut data = vec![0u8; FULLBOX_HEADER_LEN];
554        data[0..4].copy_from_slice(&0xFFFF_FFFFu32.to_be_bytes()); // huge size
555        data[4..8].copy_from_slice(b"emsg");
556        assert!(matches!(
557            EmsgBox::parse(&data),
558            Err(Error::InvalidSize { .. })
559        ));
560    }
561
562    #[test]
563    fn rejects_missing_terminator() {
564        // version 0, but the scheme string has no terminator before the box ends.
565        let mut data = vec![0u8; 16];
566        let size = data.len() as u32;
567        data[0..4].copy_from_slice(&size.to_be_bytes());
568        data[4..8].copy_from_slice(b"emsg");
569        data[8] = 0; // version 0
570        data[12..16].copy_from_slice(b"abcd"); // no 0x00
571        assert!(matches!(
572            EmsgBox::parse(&data),
573            Err(Error::InvalidString { .. })
574        ));
575    }
576
577    #[test]
578    fn is_scte35_recognises_prefix() {
579        let b = EmsgBox {
580            scheme_id_uri: "urn:scte:scte35:2013:bin",
581            value: "",
582            timescale: 1,
583            presentation_time: PresentationTime::Delta(0),
584            event_duration: 0,
585            id: 0,
586            message_data: &[],
587        };
588        assert!(b.is_scte35());
589        let nb = EmsgBox {
590            scheme_id_uri: "urn:mpeg:dash:event:2012",
591            ..b.clone()
592        };
593        assert!(!nb.is_scte35());
594    }
595}