Skip to main content

g2g_core/
time.rs

1//! Boundary-scoped time newtypes (M618): distinct types at the clock / PTP / RTP
2//! seam, where three different "just an integer" times meet and are easy to mix up.
3//!
4//! ST 2110 and PTP juggle several clocks: the pipeline's monotonic reference, PTP /
5//! TAI absolute time, and the 32-bit wrapping RTP media-clock timestamp on the wire.
6//! They are all integers, so nothing stops a `reference_ns` being passed where a TAI
7//! time is wanted, or an RTP timestamp being treated as nanoseconds, exactly the
8//! confusion the PTP servo work hit (a monotonic reference minus a TAI master is a
9//! meaningless offset). These newtypes make the seam explicit: [`MediaClock`] takes a
10//! [`TaiNs`] and returns an [`RtpTs`], so the compiler rejects handing it the wrong
11//! clock. Deliberately narrow, this is the RTP / TAI boundary, not a `Frame.timing`
12//! retrofit (PTS stays a plain `u64` ns in the pipeline's own timeline).
13//!
14//! [`MediaClock`]: crate::mediaclock::MediaClock
15
16/// PTP / TAI time in nanoseconds since the PTP epoch (absolute wall time all
17/// grandmaster-locked endpoints agree on). Distinct from the pipeline's monotonic
18/// `pts_ns` (a relative timeline) and from a raw duration.
19#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
20pub struct TaiNs(pub u64);
21
22impl TaiNs {
23    /// The underlying nanosecond count.
24    pub const fn get(self) -> u64 {
25        self.0
26    }
27
28    /// This instant advanced by `ns` nanoseconds (saturating), e.g. a base time plus
29    /// a frame's PTS offset.
30    pub fn saturating_add_ns(self, ns: u64) -> Self {
31        Self(self.0.saturating_add(ns))
32    }
33}
34
35impl From<u64> for TaiNs {
36    fn from(ns: u64) -> Self {
37        Self(ns)
38    }
39}
40
41/// The pipeline's monotonic reference time in nanoseconds: a relative timeline with
42/// an arbitrary epoch (whatever the platform clock started at), *not* absolute wall
43/// time. Distinct from [`TaiNs`]: a PTP servo disciplines a `RefNs` reading to a
44/// `TaiNs` master, and subtracting one from the other directly is the meaningless
45/// offset the servo work hit. The type keeps the two apart at the servo seam
46/// (`observe_master` / `sync_exchange` take a `RefNs` for `t2` / `t3` and a `TaiNs`
47/// for `t1` / `t4`), so the master and reference roles can no longer be swapped.
48#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
49pub struct RefNs(pub u64);
50
51impl RefNs {
52    /// The underlying nanosecond count.
53    pub const fn get(self) -> u64 {
54        self.0
55    }
56}
57
58impl From<u64> for RefNs {
59    fn from(ns: u64) -> Self {
60        Self(ns)
61    }
62}
63
64/// A 32-bit RTP media-clock timestamp as it appears on the wire (wraps at `2^32`).
65/// Distinct from a nanosecond time: it counts media-clock ticks, not ns.
66#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
67pub struct RtpTs(pub u32);
68
69impl RtpTs {
70    /// The underlying 32-bit tick count.
71    pub const fn get(self) -> u32 {
72        self.0
73    }
74
75    /// Big-endian octets, as the timestamp sits in an RTP header.
76    pub fn to_be_bytes(self) -> [u8; 4] {
77        self.0.to_be_bytes()
78    }
79
80    /// This timestamp advanced by `ticks` (wrapping), e.g. per-packet sample-count
81    /// stepping within a frame.
82    pub fn wrapping_add(self, ticks: u32) -> Self {
83        Self(self.0.wrapping_add(ticks))
84    }
85}
86
87impl From<u32> for RtpTs {
88    fn from(ts: u32) -> Self {
89        Self(ts)
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn tai_ns_accessors() {
99        let t = TaiNs::from(1_000);
100        assert_eq!(t.get(), 1_000);
101        assert_eq!(t.saturating_add_ns(500).get(), 1_500);
102        assert_eq!(
103            TaiNs::from(u64::MAX).saturating_add_ns(1),
104            TaiNs(u64::MAX),
105            "saturates"
106        );
107    }
108
109    #[test]
110    fn ref_ns_accessors() {
111        let r = RefNs::from(1_000_000_000);
112        assert_eq!(r.get(), 1_000_000_000);
113        assert_eq!(RefNs::default(), RefNs(0));
114        // A reference and a TAI time are distinct types: they cannot be mixed up.
115        assert_ne!(
116            core::any::type_name::<RefNs>(),
117            core::any::type_name::<TaiNs>()
118        );
119    }
120
121    #[test]
122    fn rtp_ts_accessors() {
123        let r = RtpTs::from(0xFFFF_FFFF);
124        assert_eq!(r.get(), 0xFFFF_FFFF);
125        assert_eq!(r.wrapping_add(2), RtpTs(1), "wraps at 2^32");
126        assert_eq!(r.to_be_bytes(), [0xFF, 0xFF, 0xFF, 0xFF]);
127    }
128}