//! Event mapper for mapping game events to video timestamps. use chrono::{DateTime, Duration, Utc}; use tracing::debug; use crate::lqp::GameEvent; /// Event mapper that tracks recording start time and maps events to video timestamps. pub struct EventMapper { /// Recording start time. start_time: Option>, /// Game start time (from game event). game_start_time: Option>, } impl EventMapper { /// Create a new event mapper. pub fn new() -> Self { Self { start_time: None, game_start_time: None, } } /// Start the mapper (recording started). pub fn start(&mut self) { self.start_time = Some(Utc::now()); debug!("Event mapper started at {:?}", self.start_time); } /// Stop the mapper (recording stopped). pub fn stop(&mut self) { self.start_time = None; self.game_start_time = None; debug!("Event mapper stopped"); } /// Check if the mapper is active. pub fn is_active(&self) -> bool { self.start_time.is_some() } /// Map a game event to video and game timestamps. pub fn map_event(&self, event: &GameEvent) -> Option<(Duration, Option)> { let start_time = self.start_time?; let now = Utc::now(); // Calculate video timestamp (time since recording started) let video_timestamp = now - start_time; // Calculate game timestamp if we have game start time let game_timestamp = self.game_start_time.map(|game_start| now - game_start); // Update game start time if this is a game start event // (handled separately in handle_event) Some((video_timestamp, game_timestamp)) } /// Handle a game event and return mapped timestamps. pub fn handle_event(&mut self, event: &GameEvent) -> Option<(Duration, Option)> { if !self.is_active() { return None; } // Track game start time if let GameEvent::GameStart(_) = event { self.game_start_time = Some(Utc::now()); debug!("Game start time recorded: {:?}", self.game_start_time); } self.map_event(event) } /// Get the current video timestamp. pub fn current_video_timestamp(&self) -> Option { self.start_time.map(|start| Utc::now() - start) } /// Get the current game timestamp. pub fn current_game_timestamp(&self) -> Option { self.game_start_time.map(|start| Utc::now() - start) } /// Get the recording duration so far. pub fn recording_duration(&self) -> Option { self.current_video_timestamp() } /// Reset the mapper. pub fn reset(&mut self) { self.start_time = None; self.game_start_time = None; debug!("Event mapper reset"); } } impl Default for EventMapper { fn default() -> Self { Self::new() } } /// Event synchronizer for keeping video and game time in sync. /// /// This handles cases where the game time might drift from real time, /// such as when the game pauses or lags. pub struct EventSynchronizer { /// Known sync points (video timestamp, game timestamp). sync_points: Vec<(Duration, Duration)>, /// Maximum allowed drift in seconds. max_drift_secs: f64, } impl EventSynchronizer { /// Create a new event synchronizer. pub fn new() -> Self { Self { sync_points: Vec::new(), max_drift_secs: 5.0, } } /// Add a sync point. pub fn add_sync_point(&mut self, video_ts: Duration, game_ts: Duration) { self.sync_points.push((video_ts, game_ts)); // Keep only recent sync points if self.sync_points.len() > 100 { self.sync_points.remove(0); } } /// Calculate the drift between video and game time. pub fn calculate_drift(&self) -> Option { if self.sync_points.len() < 2 { return None; } let first = self.sync_points.first()?; let last = self.sync_points.last()?; let video_diff = last.0 - first.0; let game_diff = last.1 - first.1; Some(video_diff - game_diff) } /// Check if the drift is within acceptable bounds. pub fn is_drift_acceptable(&self) -> bool { self.calculate_drift() .map(|drift| (drift.num_seconds().abs() as f64) < self.max_drift_secs) .unwrap_or(true) } /// Adjust a game timestamp based on known drift. pub fn adjust_game_timestamp(&self, game_ts: Duration) -> Duration { if let Some(drift) = self.calculate_drift() { game_ts + drift } else { game_ts } } /// Reset the synchronizer. pub fn reset(&mut self) { self.sync_points.clear(); } } impl Default for EventSynchronizer { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; use std::thread::sleep; use std::time::Duration as StdDuration; #[test] fn test_event_mapper_start_stop() { let mut mapper = EventMapper::new(); assert!(!mapper.is_active()); mapper.start(); assert!(mapper.is_active()); mapper.stop(); assert!(!mapper.is_active()); } #[test] fn test_event_mapper_timestamps() { let mut mapper = EventMapper::new(); mapper.start(); sleep(StdDuration::from_millis(100)); let ts = mapper.current_video_timestamp().unwrap(); assert!(ts.num_milliseconds() >= 100); } #[test] fn test_event_synchronizer() { let mut sync = EventSynchronizer::new(); sync.add_sync_point(Duration::seconds(0), Duration::seconds(0)); sync.add_sync_point(Duration::seconds(10), Duration::seconds(10)); assert!(sync.is_drift_acceptable()); // Add a drift sync.add_sync_point(Duration::seconds(20), Duration::seconds(18)); let drift = sync.calculate_drift().unwrap(); assert_eq!(drift.num_seconds(), 2); } }