Skip to main content

timed_metadata/
event.rs

1//! Canonical timed-metadata event (the hub of the hub-and-spoke model).
2use crate::error::Result;
3use alloc::{string::String, vec::Vec};
4use scte35_splice::{commands::AnyCommand, SpliceInfoSection};
5
6/// A media-timeline instant in 90 kHz ticks, wrap-unrolled.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9pub struct MediaTime(pub u64);
10
11/// A duration in 90 kHz ticks.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub struct MediaDuration(pub u64);
15
16impl MediaDuration {
17    /// The duration in seconds.
18    pub fn as_seconds_f64(self) -> f64 {
19        self.0 as f64 / crate::PTS_HZ as f64
20    }
21}
22
23/// The abstracted meaning of an event, independent of carriage format.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26#[non_exhaustive]
27pub enum EventKind {
28    /// Start of an ad/break opportunity (SCTE-35 out-of-network).
29    BreakStart,
30    /// Return to network (SCTE-35 in-to-network).
31    BreakEnd,
32    /// Chapter / program boundary.
33    Chapter,
34    /// Meaning not determined from the source.
35    Unspecified,
36}
37
38impl EventKind {
39    /// Stable label for this variant.
40    pub fn name(&self) -> &'static str {
41        match self {
42            EventKind::BreakStart => "break_start",
43            EventKind::BreakEnd => "break_end",
44            EventKind::Chapter => "chapter",
45            EventKind::Unspecified => "unspecified",
46        }
47    }
48}
49dvb_common::impl_spec_display!(EventKind);
50
51/// The lossless original payload, carried verbatim.
52#[derive(Debug, Clone, PartialEq, Eq)]
53#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
54#[non_exhaustive]
55pub enum SourcePayload {
56    /// A SCTE-35 `splice_info_section`, verbatim.
57    Scte35 { raw: Vec<u8> },
58    /// A DASH `emsg`: its scheme/value plus the verbatim `message_data`.
59    Emsg {
60        scheme_id_uri: String,
61        value: String,
62        raw: Vec<u8>,
63    },
64}
65
66/// The canonical event passed between format adapters.
67#[derive(Debug, Clone, PartialEq, Eq)]
68#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
69pub struct TimedEvent {
70    /// Event id (`splice_event_id` / emsg `id`).
71    pub id: Option<u32>,
72    /// Abstract meaning.
73    pub kind: EventKind,
74    /// Media-timeline instant; `None` = immediate / determined by insertion point.
75    pub at: Option<MediaTime>,
76    /// Event duration, if known.
77    pub duration: Option<MediaDuration>,
78    /// Lossless original.
79    pub source: SourcePayload,
80}
81
82impl TimedEvent {
83    /// Build from a parsed SCTE-35 section, retaining `raw` verbatim.
84    pub fn from_scte35(section: &SpliceInfoSection, raw: &[u8]) -> Result<Self> {
85        let mut id = None;
86        let mut kind = EventKind::Unspecified;
87        let mut at = None;
88        let mut duration = None;
89
90        if let Some(clear) = &section.clear {
91            if let AnyCommand::SpliceInsert(si) = &clear.command {
92                id = Some(si.splice_event_id);
93                kind = if si.out_of_network_indicator {
94                    EventKind::BreakStart
95                } else {
96                    EventKind::BreakEnd
97                };
98                if let Some(st) = &si.splice_time {
99                    at = st.pts_time.map(MediaTime);
100                }
101                if let Some(bd) = &si.break_duration {
102                    duration = Some(MediaDuration(bd.duration));
103                }
104            }
105        }
106
107        Ok(TimedEvent {
108            id,
109            kind,
110            at,
111            duration,
112            source: SourcePayload::Scte35 { raw: raw.to_vec() },
113        })
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use alloc::vec::Vec;
121    use dvb_common::traits::Parse;
122    use scte35_splice::SpliceInfoSection;
123
124    // Real Unified Streaming splice (ID 2002): out-of-network, break_duration 2160000 (24s).
125    fn splice_2002() -> Vec<u8> {
126        let hex = "FC302100000000000000FFF01005000007D27FEF7F7E0020F580C0000000000088B9661D";
127        (0..hex.len())
128            .step_by(2)
129            .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
130            .collect()
131    }
132
133    #[test]
134    fn from_scte35_extracts_break_start_and_duration() {
135        let raw = splice_2002();
136        let section = SpliceInfoSection::parse(&raw).unwrap();
137        let ev = TimedEvent::from_scte35(&section, &raw).unwrap();
138        assert_eq!(ev.id, Some(2002));
139        assert_eq!(ev.kind, EventKind::BreakStart); // out_of_network = true
140        assert_eq!(ev.at, None); // pts_time None (program splice)
141        assert_eq!(ev.duration, Some(MediaDuration(2_160_000)));
142        assert!((ev.duration.unwrap().as_seconds_f64() - 24.0).abs() < 1e-9);
143        match &ev.source {
144            SourcePayload::Scte35 { raw: r } => assert_eq!(r, &raw), // verbatim, lossless
145            _ => panic!("expected Scte35 payload"),
146        }
147    }
148
149    #[test]
150    fn event_kind_labels() {
151        assert_eq!(EventKind::BreakStart.name(), "break_start");
152        assert_eq!(alloc::format!("{}", EventKind::BreakEnd), "break_end");
153    }
154}