Skip to main content

timed_metadata/convert/
emsg_convert.rs

1//! emsg version 0 ↔ version 1 conversion.
2//!
3//! Both versions encode the same Movie-timeline instant T (ISO/IEC 23009-1
4//! §5.10.3.3):
5//!
6//! ```text
7//! T = presentation_time (v1)   ==   EPT + presentation_time_delta (v0)
8//! ```
9//!
10//! where EPT is the carrying segment's earliest presentation time.
11//!
12//! **Presentation-time offset** (`InbandEventStream@presentationTimeOffset`,
13//! PTO) is carried in [`SegmentTiming`] for Movie↔Period alignment but does
14//! *not* enter into the delta↔presentation_time relationship within a single
15//! Representation.  Both `presentation_time` (v1) and `presentation_time_delta`
16//! (v0) are relative to the same `timescale`, and the conversion is
17//! PTO-independent — a round-trip v0↔v1↔v0 with any PTO reproduces the same
18//! box bytes (the PTO is unused in the arithmetic, only documented for
19//! higher-layer Period-level adjustment).
20use crate::error::{Error, Result};
21use mp4_emsg::{EmsgBox, PresentationTime};
22
23/// Segment-level timing parameters needed for emsg version conversion.
24///
25/// All time fields share a single `timescale` (ticks/second), which MUST equal
26/// the emsg's `timescale`.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct SegmentTiming {
29    /// The carrying segment's earliest presentation time on the Movie
30    /// timeline, in `timescale` ticks.
31    pub earliest_presentation_time: u64,
32    /// `InbandEventStream@presentationTimeOffset`: the Movie→Period mapping
33    /// adjustment, in `timescale` ticks.  Documented here for higher-layer
34    /// use; not incorporated into the delta conversion (the arithmetic within
35    /// one Representation is PTO-independent).
36    pub presentation_time_offset: u64,
37    /// Ticks per second; MUST equal the emsg's `timescale`.
38    pub timescale: u32,
39}
40
41/// Convert an emsg to **version 1** (absolute `presentation_time` on the
42/// Movie timeline).
43///
44/// Returns the emsg unchanged if it is already version 1.
45pub fn emsg_to_v1<'a>(emsg: &EmsgBox<'a>, timing: &SegmentTiming) -> Result<EmsgBox<'a>> {
46    validate_timescale(emsg.timescale, timing.timescale)?;
47
48    let t = movie_timeline_t(emsg, timing);
49
50    Ok(EmsgBox {
51        scheme_id_uri: emsg.scheme_id_uri,
52        value: emsg.value,
53        timescale: emsg.timescale,
54        presentation_time: PresentationTime::Absolute(t),
55        event_duration: emsg.event_duration,
56        id: emsg.id,
57        message_data: emsg.message_data,
58    })
59}
60
61/// Convert an emsg to **version 0** (segment-relative
62/// `presentation_time_delta`).
63///
64/// Returns the emsg unchanged if it is already version 0.
65pub fn emsg_to_v0<'a>(emsg: &EmsgBox<'a>, timing: &SegmentTiming) -> Result<EmsgBox<'a>> {
66    validate_timescale(emsg.timescale, timing.timescale)?;
67
68    let t = movie_timeline_t(emsg, timing);
69
70    let delta = t
71        .checked_sub(timing.earliest_presentation_time)
72        .ok_or(Error::EmsgPresentationTimeBeforeEpt)?;
73
74    if delta > u64::from(u32::MAX) {
75        return Err(Error::EmsgDeltaOverflow(delta));
76    }
77
78    Ok(EmsgBox {
79        scheme_id_uri: emsg.scheme_id_uri,
80        value: emsg.value,
81        timescale: emsg.timescale,
82        presentation_time: PresentationTime::Delta(delta as u32),
83        event_duration: emsg.event_duration,
84        id: emsg.id,
85        message_data: emsg.message_data,
86    })
87}
88
89/// Compute the Movie-timeline instant `T` from either emsg version.
90fn movie_timeline_t(emsg: &EmsgBox<'_>, timing: &SegmentTiming) -> u64 {
91    match emsg.presentation_time {
92        PresentationTime::Absolute(pt) => pt,
93        PresentationTime::Delta(d) => timing.earliest_presentation_time + u64::from(d),
94        #[allow(unreachable_patterns)]
95        _ => unreachable!("non_exhaustive PresentationTime"),
96    }
97}
98
99/// Validate that the emsg timescale matches the SegmentTiming timescale.
100fn validate_timescale(emsg: u32, timing: u32) -> Result<()> {
101    if emsg != timing {
102        return Err(Error::EmsgTimescaleMismatch { emsg, timing });
103    }
104    Ok(())
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn v0_to_v1_to_v0_round_trips() {
113        let emsg = EmsgBox {
114            scheme_id_uri: "urn:scte:scte35:2013:bin",
115            value: "1",
116            timescale: 90_000,
117            presentation_time: PresentationTime::Delta(0),
118            event_duration: 2_160_000,
119            id: 1,
120            message_data: &[0xFC, 0x30, 0x21],
121        };
122        let timing = SegmentTiming {
123            earliest_presentation_time: 500_000,
124            presentation_time_offset: 0,
125            timescale: 90_000,
126        };
127        let v1 = emsg_to_v1(&emsg, &timing).unwrap();
128        assert_eq!(v1.presentation_time, PresentationTime::Absolute(500_000));
129        let v0_back = emsg_to_v0(&v1, &timing).unwrap();
130        assert_eq!(v0_back, emsg);
131    }
132
133    #[test]
134    fn v1_to_v0_to_v1_round_trips() {
135        let emsg = EmsgBox {
136            scheme_id_uri: "urn:scte:scte35:2013:bin",
137            value: "2",
138            timescale: 90_000,
139            presentation_time: PresentationTime::Absolute(1_000_000),
140            event_duration: 0,
141            id: 42,
142            message_data: &[],
143        };
144        let timing = SegmentTiming {
145            earliest_presentation_time: 900_000,
146            presentation_time_offset: 0,
147            timescale: 90_000,
148        };
149        let v0 = emsg_to_v0(&emsg, &timing).unwrap();
150        assert_eq!(v0.presentation_time, PresentationTime::Delta(100_000));
151        let v1_back = emsg_to_v1(&v0, &timing).unwrap();
152        assert_eq!(v1_back, emsg);
153    }
154
155    #[test]
156    fn timescale_mismatch_returns_error() {
157        let emsg = EmsgBox {
158            scheme_id_uri: "urn:scte:scte35:2013:bin",
159            value: "",
160            timescale: 90_000,
161            presentation_time: PresentationTime::Delta(0),
162            event_duration: 0,
163            id: 0,
164            message_data: &[],
165        };
166        let timing = SegmentTiming {
167            earliest_presentation_time: 0,
168            presentation_time_offset: 0,
169            timescale: 48_000,
170        };
171        assert!(matches!(
172            emsg_to_v1(&emsg, &timing),
173            Err(Error::EmsgTimescaleMismatch { .. })
174        ));
175        assert!(matches!(
176            emsg_to_v0(&emsg, &timing),
177            Err(Error::EmsgTimescaleMismatch { .. })
178        ));
179    }
180
181    #[test]
182    fn v0_event_before_ept_errors() {
183        let emsg = EmsgBox {
184            scheme_id_uri: "urn:scte:scte35:2013:bin",
185            value: "",
186            timescale: 90_000,
187            presentation_time: PresentationTime::Delta(10),
188            event_duration: 0,
189            id: 0,
190            message_data: &[],
191        };
192        let timing = SegmentTiming {
193            earliest_presentation_time: 1_000,
194            presentation_time_offset: 0,
195            timescale: 90_000,
196        };
197
198        // v1 with presentation_time < EPT: v1→v0 should fail.
199        let v1_before = EmsgBox {
200            presentation_time: PresentationTime::Absolute(500),
201            ..emsg
202        };
203        assert!(matches!(
204            emsg_to_v0(&v1_before, &timing),
205            Err(Error::EmsgPresentationTimeBeforeEpt)
206        ));
207    }
208}