record-daemon: initial commit
This commit is contained in:
222
record-daemon/src/timeline/mapper.rs
Normal file
222
record-daemon/src/timeline/mapper.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user