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}