Skip to main content

snapcast_client/
time_provider.rs

1//! Server time synchronization.
2//!
3//! Port of the C++ `TimeProvider`. Receives client-to-server and server-to-client
4//! latency pairs, computes the clock difference via a median buffer, and provides
5//! `server_now()` — the estimated current server time.
6
7use std::sync::atomic::{AtomicI64, Ordering};
8use std::time::{Duration, Instant};
9
10use snapcast_proto::Timeval;
11
12use crate::double_buffer::DoubleBuffer;
13
14/// Provides the estimated server time based on time sync messages.
15pub struct TimeProvider {
16    diff_buffer: DoubleBuffer,
17    diff_to_server_usec: AtomicI64,
18    last_sync: Option<Instant>,
19}
20
21impl Default for TimeProvider {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl TimeProvider {
28    /// Create a new time provider with default settings.
29    pub fn new() -> Self {
30        Self {
31            diff_buffer: DoubleBuffer::new(200),
32            diff_to_server_usec: AtomicI64::new(0),
33            last_sync: None,
34        }
35    }
36
37    /// Set the time diff from a c2s/s2c latency pair (as received in Time messages).
38    ///
39    /// The diff is computed as `(c2s - s2c) / 2` which cancels out the symmetric
40    /// network latency, leaving only the clock difference.
41    pub fn set_diff(&mut self, c2s: &Timeval, s2c: &Timeval) {
42        let diff_ms = (f64::from(c2s.sec) / 2.0 - f64::from(s2c.sec) / 2.0) * 1000.0
43            + (f64::from(c2s.usec) / 2.0 - f64::from(s2c.usec) / 2.0) / 1000.0;
44        tracing::trace!(diff_ms, "set_diff");
45        self.set_diff_ms(diff_ms);
46    }
47
48    /// Set the time diff directly in milliseconds.
49    pub fn set_diff_ms(&mut self, ms: f64) {
50        let now = Instant::now();
51
52        // Clear buffer if last sync was more than 60 seconds ago
53        if let Some(last) = self.last_sync
54            && now.duration_since(last) > Duration::from_secs(60)
55            && !self.diff_buffer.is_empty()
56        {
57            self.diff_to_server_usec
58                .store((ms * 1000.0) as i64, Ordering::Relaxed);
59            self.diff_buffer.clear();
60        }
61        self.last_sync = Some(now);
62
63        self.diff_buffer.add((ms * 1000.0) as i64);
64        let median = self.diff_buffer.median_simple();
65        self.diff_to_server_usec.store(median, Ordering::Relaxed);
66    }
67
68    /// Get the current diff to server in microseconds.
69    pub fn diff_to_server_usec(&self) -> i64 {
70        self.diff_to_server_usec.load(Ordering::Relaxed)
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn initial_diff_is_zero() {
80        let tp = TimeProvider::new();
81        assert_eq!(tp.diff_to_server_usec(), 0);
82    }
83
84    #[test]
85    fn set_diff_from_latency_pair() {
86        let mut tp = TimeProvider::new();
87
88        // c2s = 10ms, s2c = 8ms → diff = (10 - 8) / 2 = 1ms = 1000 usec
89        let c2s = Timeval {
90            sec: 0,
91            usec: 10_000,
92        };
93        let s2c = Timeval {
94            sec: 0,
95            usec: 8_000,
96        };
97        tp.set_diff(&c2s, &s2c);
98        assert_eq!(tp.diff_to_server_usec(), 1000);
99    }
100
101    #[test]
102    fn median_stabilizes() {
103        let mut tp = TimeProvider::new();
104
105        // Feed several values, one outlier
106        for _ in 0..10 {
107            tp.set_diff_ms(5.0); // 5ms = 5000 usec
108        }
109        tp.set_diff_ms(100.0); // outlier
110
111        // Median should still be close to 5000 usec
112        assert_eq!(tp.diff_to_server_usec(), 5000);
113    }
114
115    #[test]
116    fn negative_diff() {
117        let mut tp = TimeProvider::new();
118        tp.set_diff_ms(-3.5);
119        assert_eq!(tp.diff_to_server_usec(), -3500);
120    }
121
122    #[test]
123    fn set_diff_symmetric_latency_cancels() {
124        let mut tp = TimeProvider::new();
125
126        // If c2s == s2c, the diff should be 0 (symmetric network)
127        let c2s = Timeval { sec: 0, usec: 5000 };
128        let s2c = Timeval { sec: 0, usec: 5000 };
129        tp.set_diff(&c2s, &s2c);
130        assert_eq!(tp.diff_to_server_usec(), 0);
131    }
132}