use std::sync::atomic::{AtomicI64, Ordering};
use std::time::{Duration, Instant};
use snapcast_proto::Timeval;
use crate::double_buffer::DoubleBuffer;
pub struct TimeProvider {
diff_buffer: DoubleBuffer,
diff_to_server_usec: AtomicI64,
last_sync: Option<Instant>,
}
impl Default for TimeProvider {
fn default() -> Self {
Self::new()
}
}
impl TimeProvider {
pub fn new() -> Self {
Self {
diff_buffer: DoubleBuffer::new(200),
diff_to_server_usec: AtomicI64::new(0),
last_sync: None,
}
}
pub fn set_diff(&mut self, c2s: &Timeval, s2c: &Timeval) {
let diff_ms = (f64::from(c2s.sec) / 2.0 - f64::from(s2c.sec) / 2.0) * 1000.0
+ (f64::from(c2s.usec) / 2.0 - f64::from(s2c.usec) / 2.0) / 1000.0;
tracing::trace!(diff_ms, "set_diff");
self.set_diff_ms(diff_ms);
}
pub fn set_diff_ms(&mut self, ms: f64) {
let now = Instant::now();
if let Some(last) = self.last_sync
&& now.duration_since(last) > Duration::from_secs(60)
&& !self.diff_buffer.is_empty()
{
self.diff_to_server_usec
.store((ms * 1000.0) as i64, Ordering::Relaxed);
self.diff_buffer.clear();
}
self.last_sync = Some(now);
self.diff_buffer.add((ms * 1000.0) as i64);
let median = self.diff_buffer.median_simple();
self.diff_to_server_usec.store(median, Ordering::Relaxed);
}
pub fn diff_to_server_usec(&self) -> i64 {
self.diff_to_server_usec.load(Ordering::Relaxed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn initial_diff_is_zero() {
let tp = TimeProvider::new();
assert_eq!(tp.diff_to_server_usec(), 0);
}
#[test]
fn set_diff_from_latency_pair() {
let mut tp = TimeProvider::new();
let c2s = Timeval {
sec: 0,
usec: 10_000,
};
let s2c = Timeval {
sec: 0,
usec: 8_000,
};
tp.set_diff(&c2s, &s2c);
assert_eq!(tp.diff_to_server_usec(), 1000);
}
#[test]
fn median_stabilizes() {
let mut tp = TimeProvider::new();
for _ in 0..10 {
tp.set_diff_ms(5.0); }
tp.set_diff_ms(100.0);
assert_eq!(tp.diff_to_server_usec(), 5000);
}
#[test]
fn negative_diff() {
let mut tp = TimeProvider::new();
tp.set_diff_ms(-3.5);
assert_eq!(tp.diff_to_server_usec(), -3500);
}
#[test]
fn set_diff_symmetric_latency_cancels() {
let mut tp = TimeProvider::new();
let c2s = Timeval { sec: 0, usec: 5000 };
let s2c = Timeval { sec: 0, usec: 5000 };
tp.set_diff(&c2s, &s2c);
assert_eq!(tp.diff_to_server_usec(), 0);
}
}