record-daemon: initial commit

This commit is contained in:
2026-03-19 17:48:07 +01:00
commit d6c0334369
30 changed files with 9486 additions and 0 deletions

View File

@@ -0,0 +1,193 @@
//! Timeline module for storing game events and mapping them to video timestamps.
mod mapper;
mod store;
pub use mapper::EventMapper;
pub use store::{RecordingMetadata, TimelineStore, TimestampedEvent};
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::lqp::GameEvent;
/// A timeline of events for a recording.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Timeline {
/// Recording ID.
pub recording_id: Uuid,
/// Recording start time.
pub start_time: DateTime<Utc>,
/// Recording end time.
pub end_time: Option<DateTime<Utc>>,
/// Total duration in seconds.
pub duration_secs: i64,
/// Events in the timeline.
pub events: Vec<TimestampedEvent>,
}
impl Timeline {
/// Get the duration as chrono Duration.
pub fn duration(&self) -> Duration {
Duration::seconds(self.duration_secs)
}
}
impl Timeline {
/// Create a new timeline for a recording.
pub fn new(recording_id: Uuid) -> Self {
Self {
recording_id,
start_time: Utc::now(),
end_time: None,
duration_secs: 0,
events: Vec::new(),
}
}
/// Add an event to the timeline.
pub fn add_event(
&mut self,
event: GameEvent,
video_timestamp: Duration,
game_timestamp: Option<Duration>,
) {
let timestamped = TimestampedEvent {
video_timestamp,
game_timestamp,
timestamp: Utc::now(),
event_type: event_type_name(&event),
description: event.description(),
event,
};
self.events.push(timestamped);
}
/// Finalize the timeline.
pub fn finalize(&mut self) {
self.end_time = Some(Utc::now());
self.duration_secs = (self.end_time.unwrap() - self.start_time).num_seconds();
}
/// Get events within a time range.
pub fn events_in_range(&self, start: Duration, end: Duration) -> Vec<&TimestampedEvent> {
self.events
.iter()
.filter(|e| e.video_timestamp >= start && e.video_timestamp <= end)
.collect()
}
/// Get events of a specific type.
pub fn events_of_type(&self, event_type: &str) -> Vec<&TimestampedEvent> {
self.events
.iter()
.filter(|e| e.event_type == event_type)
.collect()
}
/// Get the number of events.
pub fn event_count(&self) -> usize {
self.events.len()
}
/// Export timeline to JSON.
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
/// Export timeline to CSV.
pub fn to_csv(&self) -> String {
let mut csv =
String::from("video_timestamp,game_timestamp,event_type,description,timestamp\n");
for event in &self.events {
let video_ts = format_timestamp(event.video_timestamp);
let game_ts = event
.game_timestamp
.map(format_timestamp)
.unwrap_or_default();
csv.push_str(&format!(
"{},{},{},{},{}\n",
video_ts,
game_ts,
event.event_type,
event.description,
event.timestamp.to_rfc3339(),
));
}
csv
}
}
/// Format a duration as HH:MM:SS.mmm
fn format_timestamp(duration: Duration) -> String {
let total_ms = duration.num_milliseconds();
let hours = total_ms / 3_600_000;
let minutes = (total_ms % 3_600_000) / 60_000;
let seconds = (total_ms % 60_000) / 1000;
let millis = total_ms % 1000;
format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis)
}
/// Get the event type name.
fn event_type_name(event: &GameEvent) -> String {
match event {
GameEvent::MatchFound(_) => "match_found",
GameEvent::GameStart(_) => "game_start",
GameEvent::Kill(_) => "kill",
GameEvent::Death(_) => "death",
GameEvent::Objective(_) => "objective",
GameEvent::GameEnd(_) => "game_end",
GameEvent::Unknown => "unknown",
}
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lqp::{KillEvent, ObjectiveEvent, ObjectiveType};
#[test]
fn test_timeline_creation() {
let id = Uuid::new_v4();
let timeline = Timeline::new(id);
assert_eq!(timeline.recording_id, id);
assert!(timeline.events.is_empty());
}
#[test]
fn test_add_event() {
let id = Uuid::new_v4();
let mut timeline = Timeline::new(id);
let event = GameEvent::Kill(KillEvent {
killer: "Player1".to_string(),
killer_champion: Some("Ahri".to_string()),
victim: "Player2".to_string(),
victim_champion: Some("Lux".to_string()),
solo_kill: true,
assists: 0,
position: None,
game_time: Some(120.0),
timestamp: Utc::now(),
});
timeline.add_event(event, Duration::seconds(5), Some(Duration::seconds(120)));
assert_eq!(timeline.event_count(), 1);
}
#[test]
fn test_format_timestamp() {
let duration = Duration::milliseconds(3723456); // 1:02:03.456
let formatted = format_timestamp(duration);
assert_eq!(formatted, "01:02:03.456");
}
}