record-daemon: refactor and cleanup
This commit is contained in:
@@ -90,6 +90,13 @@ pub struct OutputSettings {
|
|||||||
pub split_time_minutes: u32,
|
pub split_time_minutes: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl OutputSettings {
|
||||||
|
/// Get the output format enum for this container.
|
||||||
|
pub fn output_format(&self) -> crate::recording::OutputFormat {
|
||||||
|
crate::recording::OutputFormat::from(self.container.as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for OutputSettings {
|
impl Default for OutputSettings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ pub enum DaemonError {
|
|||||||
Serialization(#[from] serde_json::Error),
|
Serialization(#[from] serde_json::Error),
|
||||||
|
|
||||||
#[error("WebSocket error: {0}")]
|
#[error("WebSocket error: {0}")]
|
||||||
WebSocket(#[from] tokio_tungstenite::tungstenite::Error),
|
WebSocket(#[from] Box<tokio_tungstenite::tungstenite::Error>),
|
||||||
|
|
||||||
#[error("HTTP request error: {0}")]
|
#[error("HTTP request error: {0}")]
|
||||||
Http(#[from] reqwest::Error),
|
Http(#[from] reqwest::Error),
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ use async_trait::async_trait;
|
|||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
use super::protocol::{IpcCommand, IpcMessage, IpcResponse};
|
use super::protocol::{
|
||||||
|
EncoderInfo, GetEncodersResponse, GetRecordingsResponse, GetSettingsResponse,
|
||||||
|
GetStatusResponse, IpcCommand, IpcMessage, IpcResponse,
|
||||||
|
};
|
||||||
use crate::config::Settings;
|
use crate::config::Settings;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::recording::encoder::{detect_hardware_encoders, EncoderCapability};
|
use crate::recording::encoder::{detect_hardware_encoders, EncoderCapability};
|
||||||
@@ -94,7 +97,8 @@ impl IpcHandlers {
|
|||||||
|
|
||||||
async fn handle_get_settings(&self) -> Result<serde_json::Value> {
|
async fn handle_get_settings(&self) -> Result<serde_json::Value> {
|
||||||
let settings = self.settings.read().clone();
|
let settings = self.settings.read().clone();
|
||||||
Ok(serde_json::to_value(settings)?)
|
let response = GetSettingsResponse { settings };
|
||||||
|
Ok(serde_json::to_value(response)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_update_settings(
|
async fn handle_update_settings(
|
||||||
@@ -142,12 +146,15 @@ impl IpcHandlers {
|
|||||||
.map(|r| r.is_recording())
|
.map(|r| r.is_recording())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let client_connected = *self.client_connected.read();
|
let client_connected = *self.client_connected.read();
|
||||||
|
let current_game_id = recording.as_ref().and_then(|r| r.current_game_id());
|
||||||
|
|
||||||
Ok(serde_json::json!({
|
let response = GetStatusResponse {
|
||||||
"status": status,
|
status,
|
||||||
"isRecording": is_recording,
|
is_recording,
|
||||||
"clientConnected": client_connected,
|
current_game_id,
|
||||||
}))
|
client_connected,
|
||||||
|
};
|
||||||
|
Ok(serde_json::to_value(response)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_get_encoders(&self) -> Result<serde_json::Value> {
|
async fn handle_get_encoders(&self) -> Result<serde_json::Value> {
|
||||||
@@ -160,40 +167,38 @@ impl IpcHandlers {
|
|||||||
.encoder_name()
|
.encoder_name()
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let encoders: Vec<_> = capabilities
|
let available: Vec<EncoderInfo> = capabilities
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cap| match cap {
|
.map(|cap| match cap {
|
||||||
EncoderCapability::Nvenc => serde_json::json!({
|
EncoderCapability::Nvenc => EncoderInfo {
|
||||||
"id": "jim_nvenc",
|
id: "jim_nvenc".to_string(),
|
||||||
"name": "NVIDIA NVENC",
|
name: "NVIDIA NVENC".to_string(),
|
||||||
"isHardware": true,
|
is_hardware: true,
|
||||||
"available": true,
|
available: true,
|
||||||
}),
|
},
|
||||||
EncoderCapability::Amf => serde_json::json!({
|
EncoderCapability::Amf => EncoderInfo {
|
||||||
"id": "amd_amf_h264",
|
id: "amd_amf_h264".to_string(),
|
||||||
"name": "AMD AMF",
|
name: "AMD AMF".to_string(),
|
||||||
"isHardware": true,
|
is_hardware: true,
|
||||||
"available": true,
|
available: true,
|
||||||
}),
|
},
|
||||||
EncoderCapability::QuickSync => serde_json::json!({
|
EncoderCapability::QuickSync => EncoderInfo {
|
||||||
"id": "qsv",
|
id: "qsv".to_string(),
|
||||||
"name": "Intel QuickSync",
|
name: "Intel QuickSync".to_string(),
|
||||||
"isHardware": true,
|
is_hardware: true,
|
||||||
"available": true,
|
available: true,
|
||||||
}),
|
},
|
||||||
EncoderCapability::Software => serde_json::json!({
|
EncoderCapability::Software => EncoderInfo {
|
||||||
"id": "x264",
|
id: "x264".to_string(),
|
||||||
"name": "x264 (Software)",
|
name: "x264 (Software)".to_string(),
|
||||||
"isHardware": false,
|
is_hardware: false,
|
||||||
"available": true,
|
available: true,
|
||||||
}),
|
},
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(serde_json::json!({
|
let response = GetEncodersResponse { available, current };
|
||||||
"available": encoders,
|
Ok(serde_json::to_value(response)?)
|
||||||
"current": current,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_start_recording(&self) -> Result<serde_json::Value> {
|
async fn handle_start_recording(&self) -> Result<serde_json::Value> {
|
||||||
@@ -232,7 +237,8 @@ impl IpcHandlers {
|
|||||||
|
|
||||||
async fn handle_get_recordings(&self) -> Result<serde_json::Value> {
|
async fn handle_get_recordings(&self) -> Result<serde_json::Value> {
|
||||||
let recordings = self.timeline.read().get_all_recordings()?;
|
let recordings = self.timeline.read().get_all_recordings()?;
|
||||||
Ok(serde_json::to_value(recordings)?)
|
let response = GetRecordingsResponse { recordings };
|
||||||
|
Ok(serde_json::to_value(response)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_get_recording(&self, id: uuid::Uuid) -> Result<serde_json::Value> {
|
async fn handle_get_recording(&self, id: uuid::Uuid) -> Result<serde_json::Value> {
|
||||||
|
|||||||
@@ -13,18 +13,10 @@ use crate::error::{LqpError, Result};
|
|||||||
/// League Client lockfile location (relative to League Client install).
|
/// League Client lockfile location (relative to League Client install).
|
||||||
pub const LOCKFILE_NAME: &str = "lockfile";
|
pub const LOCKFILE_NAME: &str = "lockfile";
|
||||||
|
|
||||||
/// Default League Client paths on Linux (via Lutris/Wine).
|
|
||||||
pub const LINUX_CLIENT_PATHS: &[&str] = &[
|
|
||||||
// Lutris default path
|
|
||||||
"$HOME/Games/League of Legends/LeagueClient",
|
|
||||||
// Wine prefix
|
|
||||||
"$HOME/.wine/drive_c/Riot Games/League of Legends/LeagueClient",
|
|
||||||
];
|
|
||||||
|
|
||||||
/// Default League Client paths on Windows.
|
/// Default League Client paths on Windows.
|
||||||
pub const WINDOWS_CLIENT_PATHS: &[&str] = &[
|
pub const WINDOWS_CLIENT_PATHS: &[&str] = &[
|
||||||
"C:\\Riot Games\\League of Legends\\lockfile",
|
"C:\\Riot Games\\League of Legends",
|
||||||
"D:\\Riot Games\\League of Legends\\lockfile",
|
"D:\\Riot Games\\League of Legends",
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Credentials extracted from the League Client lockfile.
|
/// Credentials extracted from the League Client lockfile.
|
||||||
@@ -126,63 +118,16 @@ impl LockfileWatcher {
|
|||||||
|
|
||||||
/// Discover possible lockfile paths based on OS.
|
/// Discover possible lockfile paths based on OS.
|
||||||
fn discover_lockfile_paths() -> Vec<PathBuf> {
|
fn discover_lockfile_paths() -> Vec<PathBuf> {
|
||||||
let mut paths = Vec::new();
|
// For now, League of Legends is not available on Linux.
|
||||||
|
// There is no point in trying to locate the client.
|
||||||
#[cfg(target_os = "linux")]
|
if cfg!(target_os = "windows") {
|
||||||
{
|
WINDOWS_CLIENT_PATHS
|
||||||
// Check for Wine/Lutris installations
|
.iter()
|
||||||
if let Some(home) = std::env::var_os("HOME") {
|
.map(|s| PathBuf::from(s).join(LOCKFILE_NAME))
|
||||||
let home_path = PathBuf::from(&home);
|
.collect()
|
||||||
|
} else {
|
||||||
// Lutris default
|
Vec::new()
|
||||||
let lutris_path = home_path
|
|
||||||
.join("Games")
|
|
||||||
.join("League of Legends")
|
|
||||||
.join("lockfile");
|
|
||||||
paths.push(lutris_path);
|
|
||||||
|
|
||||||
// Wine prefix
|
|
||||||
let wine_path = home_path
|
|
||||||
.join(".wine")
|
|
||||||
.join("drive_c")
|
|
||||||
.join("Riot Games")
|
|
||||||
.join("League of Legends")
|
|
||||||
.join("lockfile");
|
|
||||||
paths.push(wine_path);
|
|
||||||
|
|
||||||
// Proton (Steam)
|
|
||||||
let proton_path = home_path
|
|
||||||
.join(".local")
|
|
||||||
.join("share")
|
|
||||||
.join("Steam")
|
|
||||||
.join("steamapps")
|
|
||||||
.join("compatdata")
|
|
||||||
.join("League of Legends")
|
|
||||||
.join("pfx")
|
|
||||||
.join("drive_c")
|
|
||||||
.join("Riot Games")
|
|
||||||
.join("League of Legends")
|
|
||||||
.join("lockfile");
|
|
||||||
paths.push(proton_path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
// Default Windows paths
|
|
||||||
paths.push(PathBuf::from("C:\\Riot Games\\League of Legends\\lockfile"));
|
|
||||||
paths.push(PathBuf::from("D:\\Riot Games\\League of Legends\\lockfile"));
|
|
||||||
|
|
||||||
// Check Program Files
|
|
||||||
paths.push(PathBuf::from(
|
|
||||||
"C:\\Program Files\\Riot Games\\League of Legends\\lockfile",
|
|
||||||
));
|
|
||||||
paths.push(PathBuf::from(
|
|
||||||
"C:\\Program Files (x86)\\Riot Games\\League of Legends\\lockfile",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
paths
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the League Client is currently running.
|
/// Check if the League Client is currently running.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use tokio_tungstenite::{connect_async_tls_with_config, tungstenite::protocol::Me
|
|||||||
use tracing::{debug, error, info, trace, warn};
|
use tracing::{debug, error, info, trace, warn};
|
||||||
|
|
||||||
use super::auth::LockfileCredentials;
|
use super::auth::LockfileCredentials;
|
||||||
use super::events::GameEvent;
|
use super::events::{GameEvent, RawEvent};
|
||||||
use crate::error::{LqpError, Result};
|
use crate::error::{LqpError, Result};
|
||||||
|
|
||||||
/// Custom certificate verifier that accepts any certificate.
|
/// Custom certificate verifier that accepts any certificate.
|
||||||
@@ -385,7 +385,22 @@ impl LqpClient {
|
|||||||
let event_data = arr.get(2)?;
|
let event_data = arr.get(2)?;
|
||||||
|
|
||||||
if callback == "OnJsonApiEvent" {
|
if callback == "OnJsonApiEvent" {
|
||||||
// Extract the actual URI and data from the event
|
// Try to parse as RawEvent for type-safe access
|
||||||
|
if let Ok(raw_event) =
|
||||||
|
serde_json::from_value::<RawEvent>(event_data.clone())
|
||||||
|
{
|
||||||
|
let event_type = event_data
|
||||||
|
.get("eventType")
|
||||||
|
.and_then(|t| t.as_str())
|
||||||
|
.unwrap_or("Update");
|
||||||
|
return Self::parse_event_from_uri(
|
||||||
|
&raw_event.uri,
|
||||||
|
event_type,
|
||||||
|
&serde_json::to_value(raw_event.data).unwrap_or_default(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to manual extraction
|
||||||
let uri = event_data.get("uri")?.as_str()?;
|
let uri = event_data.get("uri")?.as_str()?;
|
||||||
let data = event_data.get("data")?;
|
let data = event_data.get("data")?;
|
||||||
let event_type = event_data
|
let event_type = event_data
|
||||||
@@ -582,6 +597,16 @@ impl LqpClient {
|
|||||||
pub async fn get_summoner(&self) -> Result<serde_json::Value> {
|
pub async fn get_summoner(&self) -> Result<serde_json::Value> {
|
||||||
self.request("GET", endpoints::SUMMONER).await
|
self.request("GET", endpoints::SUMMONER).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get champion select session info.
|
||||||
|
pub async fn get_champion_select(&self) -> Result<serde_json::Value> {
|
||||||
|
self.request("GET", endpoints::CHAMPION_SELECT).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get end-of-game stats.
|
||||||
|
pub async fn get_game_stats(&self) -> Result<serde_json::Value> {
|
||||||
|
self.request("GET", endpoints::GAME_STATS).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LqpClient {
|
impl Default for LqpClient {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use tokio::sync::broadcast;
|
|||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
use futures::StreamExt;
|
|
||||||
use record_daemon::{
|
use record_daemon::{
|
||||||
config::{self, Settings},
|
config::{self, Settings},
|
||||||
error::Result,
|
error::Result,
|
||||||
|
|||||||
@@ -193,33 +193,6 @@ pub enum SourceType {
|
|||||||
MonitorCapture,
|
MonitorCapture,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the League of Legends game window.
|
|
||||||
///
|
|
||||||
/// Returns the window title if found, or a default if not found.
|
|
||||||
pub fn find_league_window() -> Option<String> {
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
// On Windows, we can use the Win32 API to find the window
|
|
||||||
// For now, return the expected window title
|
|
||||||
Some("League of Legends (TM) Client".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
// On Linux, we would use X11 or Wayland APIs
|
|
||||||
// For now, return the expected window title
|
|
||||||
Some("League of Legends (TM) Client".to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the default capture configuration for League of Legends.
|
|
||||||
pub fn league_capture_config() -> GameCapture {
|
|
||||||
GameCapture::for_league_of_legends()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -244,7 +217,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_league_capture_config() {
|
fn test_league_capture_config() {
|
||||||
let capture = league_capture_config();
|
let capture = GameCapture::for_league_of_legends();
|
||||||
assert_eq!(capture.name, "League of Legends Capture");
|
assert_eq!(capture.name, "League of Legends Capture");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
capture.process_name,
|
capture.process_name,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ mod output;
|
|||||||
pub use capture::{CaptureMode, GameCapture, MonitorCapture, SourceType, WindowCapture};
|
pub use capture::{CaptureMode, GameCapture, MonitorCapture, SourceType, WindowCapture};
|
||||||
pub use encoder::{AudioEncoderConfig, EncoderCapability, EncoderConfig, EncoderSettings};
|
pub use encoder::{AudioEncoderConfig, EncoderCapability, EncoderConfig, EncoderSettings};
|
||||||
pub use obs_context::{ObsContext, ObsContextBuilder};
|
pub use obs_context::{ObsContext, ObsContextBuilder};
|
||||||
pub use output::{OutputConfig, RecordingOutput, RecordingResult};
|
pub use output::{OutputConfig, OutputFormat, RecordingOutput, RecordingResult, RecordingStats};
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -86,6 +86,11 @@ impl RecordingEngine {
|
|||||||
self.is_recording
|
self.is_recording
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the current game ID if recording.
|
||||||
|
pub fn current_game_id(&self) -> Option<u64> {
|
||||||
|
self.current_output.as_ref().and_then(|o| o.game_id)
|
||||||
|
}
|
||||||
|
|
||||||
/// Start recording.
|
/// Start recording.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@@ -170,6 +175,7 @@ impl RecordingEngine {
|
|||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
duration,
|
duration,
|
||||||
|
stats: None, // Stats would be populated from OBS context if available
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("Recording stopped: {:?}", result.path);
|
info!("Recording stopped: {:?}", result.path);
|
||||||
|
|||||||
@@ -129,6 +129,11 @@ impl ObsContext {
|
|||||||
&self.video_settings
|
&self.video_settings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the output directory for recordings.
|
||||||
|
pub fn output_dir(&self) -> &std::path::Path {
|
||||||
|
&self.output_dir
|
||||||
|
}
|
||||||
|
|
||||||
/// Initialize OBS.
|
/// Initialize OBS.
|
||||||
fn initialize(&mut self) -> Result<()> {
|
fn initialize(&mut self) -> Result<()> {
|
||||||
if self.context.is_some() {
|
if self.context.is_some() {
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ pub struct RecordingResult {
|
|||||||
/// Recording duration.
|
/// Recording duration.
|
||||||
// #[serde(with = "chrono::serde::seconds")]
|
// #[serde(with = "chrono::serde::seconds")]
|
||||||
pub duration: Duration,
|
pub duration: Duration,
|
||||||
|
/// Recording statistics (if available).
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub stats: Option<RecordingStats>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecordingResult {
|
impl RecordingResult {
|
||||||
@@ -209,6 +212,7 @@ mod tests {
|
|||||||
start_time: Utc::now(),
|
start_time: Utc::now(),
|
||||||
end_time: Utc::now() + Duration::seconds(125),
|
end_time: Utc::now() + Duration::seconds(125),
|
||||||
duration: Duration::seconds(125),
|
duration: Duration::seconds(125),
|
||||||
|
stats: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(result.duration_human(), "2m 5s");
|
assert_eq!(result.duration_human(), "2m 5s");
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ pub struct EventMapper {
|
|||||||
start_time: Option<DateTime<Utc>>,
|
start_time: Option<DateTime<Utc>>,
|
||||||
/// Game start time (from game event).
|
/// Game start time (from game event).
|
||||||
game_start_time: Option<DateTime<Utc>>,
|
game_start_time: Option<DateTime<Utc>>,
|
||||||
|
/// Synchronizer for handling time drift.
|
||||||
|
synchronizer: EventSynchronizer,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventMapper {
|
impl EventMapper {
|
||||||
@@ -19,12 +21,14 @@ impl EventMapper {
|
|||||||
Self {
|
Self {
|
||||||
start_time: None,
|
start_time: None,
|
||||||
game_start_time: None,
|
game_start_time: None,
|
||||||
|
synchronizer: EventSynchronizer::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start the mapper (recording started).
|
/// Start the mapper (recording started).
|
||||||
pub fn start(&mut self) {
|
pub fn start(&mut self) {
|
||||||
self.start_time = Some(Utc::now());
|
self.start_time = Some(Utc::now());
|
||||||
|
self.synchronizer.reset();
|
||||||
debug!("Event mapper started at {:?}", self.start_time);
|
debug!("Event mapper started at {:?}", self.start_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +36,7 @@ impl EventMapper {
|
|||||||
pub fn stop(&mut self) {
|
pub fn stop(&mut self) {
|
||||||
self.start_time = None;
|
self.start_time = None;
|
||||||
self.game_start_time = None;
|
self.game_start_time = None;
|
||||||
|
self.synchronizer.reset();
|
||||||
debug!("Event mapper stopped");
|
debug!("Event mapper stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +54,11 @@ impl EventMapper {
|
|||||||
let video_timestamp = now - start_time;
|
let video_timestamp = now - start_time;
|
||||||
|
|
||||||
// Calculate game timestamp if we have game start time
|
// Calculate game timestamp if we have game start time
|
||||||
let game_timestamp = self.game_start_time.map(|game_start| now - game_start);
|
let game_timestamp = self.game_start_time.map(|game_start| {
|
||||||
|
let raw_ts = now - game_start;
|
||||||
|
// Apply drift correction if synchronizer has data
|
||||||
|
self.synchronizer.adjust_game_timestamp(raw_ts)
|
||||||
|
});
|
||||||
|
|
||||||
// Update game start time if this is a game start event
|
// Update game start time if this is a game start event
|
||||||
// (handled separately in handle_event)
|
// (handled separately in handle_event)
|
||||||
@@ -69,7 +78,14 @@ impl EventMapper {
|
|||||||
debug!("Game start time recorded: {:?}", self.game_start_time);
|
debug!("Game start time recorded: {:?}", self.game_start_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.map_event(event)
|
let result = self.map_event(event);
|
||||||
|
|
||||||
|
// Add sync point if we have both timestamps
|
||||||
|
if let Some((video_ts, Some(game_ts))) = result {
|
||||||
|
self.synchronizer.add_sync_point(video_ts, game_ts);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current video timestamp.
|
/// Get the current video timestamp.
|
||||||
@@ -79,7 +95,10 @@ impl EventMapper {
|
|||||||
|
|
||||||
/// Get the current game timestamp.
|
/// Get the current game timestamp.
|
||||||
pub fn current_game_timestamp(&self) -> Option<Duration> {
|
pub fn current_game_timestamp(&self) -> Option<Duration> {
|
||||||
self.game_start_time.map(|start| Utc::now() - start)
|
self.game_start_time.map(|start| {
|
||||||
|
let raw_ts = Utc::now() - start;
|
||||||
|
self.synchronizer.adjust_game_timestamp(raw_ts)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the recording duration so far.
|
/// Get the recording duration so far.
|
||||||
@@ -87,10 +106,21 @@ impl EventMapper {
|
|||||||
self.current_video_timestamp()
|
self.current_video_timestamp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if time drift is within acceptable bounds.
|
||||||
|
pub fn is_time_sync_healthy(&self) -> bool {
|
||||||
|
self.synchronizer.is_drift_acceptable()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current time drift, if any.
|
||||||
|
pub fn time_drift(&self) -> Option<Duration> {
|
||||||
|
self.synchronizer.calculate_drift()
|
||||||
|
}
|
||||||
|
|
||||||
/// Reset the mapper.
|
/// Reset the mapper.
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
self.start_time = None;
|
self.start_time = None;
|
||||||
self.game_start_time = None;
|
self.game_start_time = None;
|
||||||
|
self.synchronizer.reset();
|
||||||
debug!("Event mapper reset");
|
debug!("Event mapper reset");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,6 +299,7 @@ mod tests {
|
|||||||
start_time: Utc::now(),
|
start_time: Utc::now(),
|
||||||
end_time: Utc::now() + Duration::seconds(60),
|
end_time: Utc::now() + Duration::seconds(60),
|
||||||
duration: Duration::seconds(60),
|
duration: Duration::seconds(60),
|
||||||
|
stats: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let id = store.add_recording(result).unwrap();
|
let id = store.add_recording(result).unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user