Files
leaguerecorder/record-daemon/src/timeline/mapper.rs

223 lines
6.0 KiB
Rust

//! 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<DateTime<Utc>>,
/// Game start time (from game event).
game_start_time: Option<DateTime<Utc>>,
}
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<Duration>)> {
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<Duration>)> {
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<Duration> {
self.start_time.map(|start| Utc::now() - start)
}
/// Get the current game timestamp.
pub fn current_game_timestamp(&self) -> Option<Duration> {
self.game_start_time.map(|start| Utc::now() - start)
}
/// Get the recording duration so far.
pub fn recording_duration(&self) -> Option<Duration> {
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<Duration> {
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);
}
}