Skip to main content

timed_metadata/
timeline.rs

1//! Stateful conversion session: holds the wall-clock anchor and unrolls 33-bit
2//! PTS wrap across a stream of events.
3use crate::anchor::TimeAnchor;
4use crate::convert::{scte35_to_daterange, scte35_to_emsg, EmsgConfig};
5use crate::daterange::DateRange;
6use crate::error::{Error, Result};
7use crate::event::{MediaTime, TimedEvent};
8use alloc::vec::Vec;
9use dvb_common::traits::Parse;
10use scte35_splice::SpliceInfoSection;
11
12/// The 33-bit PTS modulus.
13pub const PTS_WRAP: u64 = 1 << 33;
14
15/// A stateful conversion session.
16#[derive(Debug, Default)]
17pub struct Timeline {
18    anchor: Option<TimeAnchor>,
19    last_pts: Option<u64>,
20    epoch: u64,
21}
22
23impl Timeline {
24    /// New session with no anchor.
25    pub fn new() -> Self {
26        Self::default()
27    }
28    /// New session with a wall-clock anchor.
29    pub fn with_anchor(anchor: TimeAnchor) -> Self {
30        Timeline {
31            anchor: Some(anchor),
32            last_pts: None,
33            epoch: 0,
34        }
35    }
36    /// Set / replace the anchor.
37    pub fn set_anchor(&mut self, anchor: TimeAnchor) {
38        self.anchor = Some(anchor);
39    }
40
41    /// Parse a SCTE-35 section; unroll its PTS into an absolute [`MediaTime`].
42    pub fn push_scte35(&mut self, bytes: &[u8]) -> Result<TimedEvent> {
43        let section = SpliceInfoSection::parse(bytes)?;
44        let mut ev = TimedEvent::from_scte35(&section, bytes)?;
45        if let Some(MediaTime(pts33)) = ev.at {
46            let abs = unroll_pts(&mut self.last_pts, &mut self.epoch, pts33);
47            ev.at = Some(MediaTime(abs));
48        }
49        Ok(ev)
50    }
51
52    /// Convert to a DATERANGE (requires an anchor).
53    pub fn to_daterange(&self, ev: &TimedEvent) -> Result<DateRange> {
54        let anchor = self.anchor.as_ref().ok_or(Error::MissingAnchor)?;
55        scte35_to_daterange(ev, anchor)
56    }
57
58    /// Convert to a serialized SCTE-35 `emsg` box.
59    pub fn to_emsg(&self, ev: &TimedEvent, cfg: &EmsgConfig) -> Result<Vec<u8>> {
60        match &ev.source {
61            crate::event::SourcePayload::Scte35 { raw } => scte35_to_emsg(raw, cfg),
62            crate::event::SourcePayload::Emsg { .. } => Err(Error::AttrParse(
63                alloc::string::String::from("event is not SCTE-35-sourced"),
64            )),
65        }
66    }
67}
68
69/// Unroll a 33-bit PTS to an absolute monotonic value. On a backward jump of
70/// more than half the range, advance one epoch.
71pub(crate) fn unroll_pts(last_pts: &mut Option<u64>, epoch: &mut u64, pts33: u64) -> u64 {
72    if let Some(prev) = *last_pts {
73        if pts33 + (PTS_WRAP / 2) < prev {
74            *epoch += 1;
75        }
76    }
77    *last_pts = Some(pts33);
78    *epoch * PTS_WRAP + pts33
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    fn splice_2002() -> alloc::vec::Vec<u8> {
86        let hex = "FC302100000000000000FFF01005000007D27FEF7F7E0020F580C0000000000088B9661D";
87        (0..hex.len())
88            .step_by(2)
89            .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
90            .collect()
91    }
92
93    #[test]
94    fn push_scte35_returns_event() {
95        let mut tl = Timeline::new();
96        let ev = tl.push_scte35(&splice_2002()).unwrap();
97        assert_eq!(ev.id, Some(2002));
98    }
99
100    #[test]
101    fn to_daterange_without_anchor_errors() {
102        let tl = Timeline::new();
103        let ev = Timeline::new().push_scte35(&splice_2002()).unwrap();
104        assert!(matches!(
105            tl.to_daterange(&ev),
106            Err(crate::Error::MissingAnchor)
107        ));
108    }
109
110    #[test]
111    fn wrap_unroll_adds_one_epoch() {
112        // unroll(prev, cur) — a near-max prev then a small cur crosses one wrap.
113        assert_eq!(
114            unroll_pts(&mut Some((1u64 << 33) - 10), &mut 0u64, 5),
115            5 + (1u64 << 33)
116        );
117    }
118
119    #[test]
120    fn wrap_unroll_forward_delta_keeps_epoch() {
121        // A normal forward delta within range must NOT bump the epoch.
122        let (mut last, mut epoch) = (Some(1_000u64), 0u64);
123        assert_eq!(unroll_pts(&mut last, &mut epoch, 2_000), 2_000);
124        assert_eq!(epoch, 0);
125        // First call (no prior pts) returns the raw value, epoch unchanged.
126        let (mut last2, mut epoch2) = (None, 0u64);
127        assert_eq!(unroll_pts(&mut last2, &mut epoch2, 42), 42);
128        assert_eq!(epoch2, 0);
129    }
130}