//! AZ-658 — frame timestamping helpers. //! //! `description.md §4` requires every emitted [`Frame`] to carry a //! monotonic capture timestamp stamped at the earliest practical //! point in the pipeline (the moment the lifecycle loop receives an //! RTSP packet from the transport). The decoder runs *after* that //! point, so the [`Frame::decode_ts_monotonic_ns`] field records when //! `FrameDecoder::decode` returned — the difference is the per-frame //! decode latency that feeds the `decode_ms_p50` / `decode_ms_p99` / //! `decode_ms_first_frame` health metrics. //! //! This module owns: //! - [`SeqCounter`] — a strictly-monotonic `u64` sequence number used //! as the frame's identity downstream of the decoder. Saturates at //! `u64::MAX` so a session that never restarts cannot wrap and //! produce duplicate IDs (saturating is preferred over wrapping //! here because `movement_detector` keys per-frame state by `seq` //! and a wrap would corrupt that map). //! - [`FrameStamper`] — pairs a `MonoClock` and a `SeqCounter` so the //! lifecycle loop has one place to read both timestamps for a //! single packet → frame transition. use shared::clock::MonoClock; /// Strictly-monotonic frame sequence counter. Saturates at /// `u64::MAX`; in practice a 30 fps stream takes ~19.5 billion years /// to overflow `u64`, so saturation behaviour is observable only as a /// post-condition for tests with `u64::MAX - 1` priming. #[derive(Debug, Default)] pub struct SeqCounter { next: u64, } impl SeqCounter { pub fn new() -> Self { Self { next: 0 } } /// Returns the next sequence number and advances internal state. /// Saturates at `u64::MAX` (subsequent calls keep returning /// `u64::MAX`). Named `advance` rather than `next` so that the /// type does not collide with `Iterator::next` semantics in /// caller code (and to satisfy `clippy::should_implement_trait` /// — `SeqCounter` is intentionally NOT an Iterator: an unbounded /// monotonic counter has no natural `None` terminator). pub fn advance(&mut self) -> u64 { let s = self.next; self.next = self.next.saturating_add(1); s } } /// Holds a clock + sequence counter so the lifecycle loop only has /// to call [`FrameStamper::capture`] (immediately on packet receipt) /// and [`FrameStamper::decoded`] (immediately after decode returns) /// to produce both monotonic timestamps for the next frame. #[derive(Debug)] pub struct FrameStamper { clock: MonoClock, seq: SeqCounter, } impl FrameStamper { pub fn new(clock: MonoClock) -> Self { Self { clock, seq: SeqCounter::new(), } } /// Snapshot the capture-side timestamp + sequence number. Call /// this the moment the transport hands us the packet, BEFORE /// invoking the decoder. The capture timestamp is the head of /// the per-frame latency budget (`description.md §8`: ≤30 ms p99 /// from RTSP rx → publish on Jetson Orin Nano). pub fn capture(&mut self) -> CaptureMark { CaptureMark { seq: self.seq.advance(), ts_ns: self.clock.elapsed_ns(), } } /// Read the decode-side timestamp at the moment /// `FrameDecoder::decode` returned. Used both for the emitted /// `Frame::decode_ts_monotonic_ns` field and to compute /// `decode_duration = decode_ts - capture_ts` for the histogram. pub fn decoded(&self) -> u64 { self.clock.elapsed_ns() } } /// One capture-side mark per packet. Carried through the decode call /// so the emitted `Frame` keeps the timestamp from packet receipt, /// not from after-decode. #[derive(Debug, Clone, Copy)] pub struct CaptureMark { pub seq: u64, pub ts_ns: u64, } #[cfg(test)] mod tests { use super::*; #[test] fn seq_counter_is_strictly_monotonic() { // Arrange let mut c = SeqCounter::new(); // Act let a = c.advance(); let b = c.advance(); let d = c.advance(); // Assert assert_eq!(a, 0); assert_eq!(b, 1); assert_eq!(d, 2); } #[test] fn seq_counter_saturates_at_max_instead_of_wrapping() { // Arrange — prime to u64::MAX - 1 by direct field assignment // so the test runs in O(1). let mut c = SeqCounter { next: u64::MAX - 1 }; // Act let a = c.advance(); let b = c.advance(); let d = c.advance(); // Assert — once we hit MAX, every subsequent call must keep // returning MAX (no wrap to 0). assert_eq!(a, u64::MAX - 1); assert_eq!(b, u64::MAX); assert_eq!(d, u64::MAX); } #[test] fn frame_stamper_capture_advances_seq_and_ts() { // Arrange let mut s = FrameStamper::new(MonoClock::new()); // Act let m1 = s.capture(); let m2 = s.capture(); // Assert assert_eq!(m1.seq, 0); assert_eq!(m2.seq, 1); assert!(m2.ts_ns >= m1.ts_ns, "monotonic clock went backwards"); } }