use crate::clock::SharedClock;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::task::JoinHandle;
pub struct StubPlayer {
clock: SharedClock,
playing: Arc<AtomicBool>,
base_ns: Arc<AtomicU64>,
speed: Arc<AtomicU32>,
task: Option<JoinHandle<()>>,
}
impl StubPlayer {
pub fn new() -> Self {
StubPlayer {
clock: SharedClock::new(),
playing: Arc::new(AtomicBool::new(false)),
base_ns: Arc::new(AtomicU64::new(0)),
speed: Arc::new(AtomicU32::new(1000)),
task: None,
}
}
pub fn clock(&self) -> SharedClock {
self.clock.clone()
}
pub fn spawn_ticker(&mut self) {
if self.task.is_some() {
return;
}
let clock = self.clock.clone();
let playing = self.playing.clone();
let base_ns = self.base_ns.clone();
let speed = self.speed.clone();
self.task = Some(tokio::spawn(async move {
let mut last = Instant::now();
loop {
tokio::time::sleep(Duration::from_millis(20)).await;
let now = Instant::now();
let spd = speed.load(Ordering::Relaxed) as f64 / 1000.0;
let delta_ns = ((now.duration_since(last).as_nanos() as f64) * spd) as u64;
last = now;
if playing.load(Ordering::Relaxed) {
let pos = base_ns.fetch_add(delta_ns, Ordering::Relaxed) + delta_ns;
clock.set_ns(pos);
}
}
}));
}
pub fn play(&mut self) {
self.playing.store(true, Ordering::Relaxed);
}
pub fn pause(&self) {
self.playing.store(false, Ordering::Relaxed);
}
pub fn stop(&mut self) {
self.playing.store(false, Ordering::Relaxed);
self.base_ns.store(0, Ordering::Relaxed);
self.clock.set_ns(0);
}
pub fn seek_by(&mut self, delta_s: f64) {
let cur_ns = self.base_ns.load(Ordering::Relaxed) as f64;
let new_ns = (cur_ns + delta_s * 1e9).max(0.0) as u64;
self.base_ns.store(new_ns, Ordering::Relaxed);
self.clock.set_ns(new_ns);
}
pub fn seek_to(&mut self, target_s: f64) {
let new_ns = (target_s.max(0.0) * 1e9) as u64;
self.base_ns.store(new_ns, Ordering::Relaxed);
self.clock.set_ns(new_ns);
}
pub fn set_speed(&self, speed: f64) {
self.speed
.store((speed * 1000.0).round() as u32, Ordering::Relaxed);
}
pub fn speed(&self) -> f64 {
self.speed.load(Ordering::Relaxed) as f64 / 1000.0
}
pub fn position_s(&self) -> f64 {
self.clock.position_s()
}
pub fn is_playing(&self) -> bool {
self.playing.load(Ordering::Relaxed)
}
}
impl Default for StubPlayer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::highlight_index;
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn control_clock_highlight_loop() {
let mut p = StubPlayer::new();
let words = [0.0_f64, 0.3, 0.6, 0.9, 1.2];
p.spawn_ticker();
p.play();
let mut max_highlight = 0usize;
for _ in 0..25 {
tokio::time::sleep(Duration::from_millis(40)).await;
let pos = p.position_s();
if let Some(i) = highlight_index(&words, pos) {
max_highlight = max_highlight.max(i);
}
}
assert!(
max_highlight >= 2,
"highlight should reach word 2+; got {max_highlight}"
);
p.pause();
let frozen = p.position_s();
tokio::time::sleep(Duration::from_millis(150)).await;
let drift = (p.position_s() - frozen).abs();
assert!(drift < 0.02, "pause should freeze the clock; drift={drift}");
let ui_clock = p.clock();
assert!((ui_clock.position_s() - p.position_s()).abs() < 1e-6);
p.stop();
assert!(p.position_s() < 1e-6, "stop should reset the clock");
}
}