//! Recording output configuration and results. use std::path::PathBuf; use chrono::{DateTime, Duration, Utc}; use serde::{Deserialize, Serialize}; /// Output configuration for recordings. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OutputConfig { /// Output container format. pub container: String, /// Output file path pattern. pub path_pattern: String, /// Maximum file size before splitting (MB). 0 = no limit. pub max_size_mb: u32, /// Maximum duration before splitting (seconds). 0 = no limit. pub max_duration_secs: u32, /// Whether to use fragmented MP4 for better crash recovery. pub fragmented: bool, } impl Default for OutputConfig { fn default() -> Self { Self { container: "mp4".to_string(), path_pattern: "{date}_{time}".to_string(), max_size_mb: 0, max_duration_secs: 0, fragmented: false, } } } /// Recording output handle. #[derive(Debug, Clone)] pub struct RecordingOutput { /// Output file path. pub path: PathBuf, /// Game ID if available. pub game_id: Option, /// Champion name if available. pub champion: Option, } /// Result of a completed recording. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RecordingResult { /// Output file path. pub path: PathBuf, /// Game ID if available. pub game_id: Option, /// Champion name if available. pub champion: Option, /// Recording start time. pub start_time: DateTime, /// Recording end time. pub end_time: DateTime, /// Recording duration. // #[serde(with = "chrono::serde::seconds")] pub duration: Duration, /// Recording statistics (if available). #[serde(skip_serializing_if = "Option::is_none")] pub stats: Option, } impl RecordingResult { /// Get the file size in bytes. pub fn file_size(&self) -> Option { std::fs::metadata(&self.path).ok().map(|m| m.len()) } /// Get the file size in a human-readable format. pub fn file_size_human(&self) -> String { let bytes = self.file_size().unwrap_or(0); let mb = bytes as f64 / (1024.0 * 1024.0); format!("{:.2} MB", mb) } /// Get the duration in a human-readable format. pub fn duration_human(&self) -> String { let total_secs = self.duration.num_seconds(); let hours = total_secs / 3600; let mins = (total_secs % 3600) / 60; let secs = total_secs % 60; if hours > 0 { format!("{}h {}m {}s", hours, mins, secs) } else if mins > 0 { format!("{}m {}s", mins, secs) } else { format!("{}s", secs) } } /// Check if the recording file exists. pub fn exists(&self) -> bool { self.path.exists() } /// Delete the recording file. pub fn delete(&self) -> std::io::Result<()> { std::fs::remove_file(&self.path) } } /// Output format type. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum OutputFormat { /// MP4 container. Mp4, /// MKV container. Mkv, /// MOV container. Mov, /// FLV container. Flv, } impl OutputFormat { /// Get the file extension. pub fn extension(&self) -> &'static str { match self { OutputFormat::Mp4 => "mp4", OutputFormat::Mkv => "mkv", OutputFormat::Mov => "mov", OutputFormat::Flv => "flv", } } /// Get the OBS output ID. pub fn obs_output_id(&self) -> &'static str { match self { OutputFormat::Mp4 => "ffmpeg_muxer", OutputFormat::Mkv => "ffmpeg_muxer", OutputFormat::Mov => "ffmpeg_muxer", OutputFormat::Flv => "ffmpeg_muxer", } } /// Get the FFmpeg format name. pub fn ffmpeg_format(&self) -> &'static str { match self { OutputFormat::Mp4 => "mp4", OutputFormat::Mkv => "matroska", OutputFormat::Mov => "mov", OutputFormat::Flv => "flv", } } } impl From<&str> for OutputFormat { fn from(s: &str) -> Self { match s.to_lowercase().as_str() { "mp4" => OutputFormat::Mp4, "mkv" => OutputFormat::Mkv, "mov" => OutputFormat::Mov, "flv" => OutputFormat::Flv, _ => OutputFormat::Mp4, } } } /// Recording statistics. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct RecordingStats { /// Total frames recorded. pub frames_recorded: u64, /// Frames dropped due to encoding lag. pub frames_dropped: u64, /// Average frame time in milliseconds. pub avg_frame_time_ms: f64, /// Current bitrate in kbps. pub current_bitrate: u32, /// Recording duration in seconds. pub duration_secs: f64, } impl RecordingStats { /// Calculate the drop rate percentage. pub fn drop_rate(&self) -> f64 { if self.frames_recorded == 0 { return 0.0; } (self.frames_dropped as f64 / self.frames_recorded as f64) * 100.0 } /// Check if the recording is healthy (low drop rate). pub fn is_healthy(&self) -> bool { self.drop_rate() < 5.0 // Less than 5% dropped frames } } #[cfg(test)] mod tests { use super::*; #[test] fn test_output_format_from_str() { assert_eq!(OutputFormat::from("mp4"), OutputFormat::Mp4); assert_eq!(OutputFormat::from("MKV"), OutputFormat::Mkv); assert_eq!(OutputFormat::from("unknown"), OutputFormat::Mp4); } #[test] fn test_recording_result_duration_human() { let result = RecordingResult { path: PathBuf::from("/tmp/test.mp4"), game_id: Some(12345), champion: Some("Ahri".to_string()), start_time: Utc::now(), end_time: Utc::now() + Duration::seconds(125), duration: Duration::seconds(125), stats: None, }; assert_eq!(result.duration_human(), "2m 5s"); } #[test] fn test_recording_stats_drop_rate() { let stats = RecordingStats { frames_recorded: 1000, frames_dropped: 40, ..Default::default() }; assert_eq!(stats.drop_rate(), 4.0); assert!(stats.is_healthy()); } }