//! Recording module for video capture using libobs. //! //! This module provides the recording engine that captures game footage //! and encodes it using hardware or software encoders. mod capture; pub mod encoder; mod obs_context; mod output; pub use capture::{CaptureMode, GameCapture, MonitorCapture, SourceType, WindowCapture}; pub use encoder::{AudioEncoderConfig, EncoderCapability, EncoderConfig, EncoderSettings}; pub use obs_context::{ObsContext, ObsContextBuilder}; pub use output::{OutputConfig, OutputFormat, RecordingOutput, RecordingResult, RecordingStats}; use std::sync::Arc; use chrono::{DateTime, Utc}; use parking_lot::RwLock; use tracing::{error, info, warn}; use crate::config::Settings; use crate::error::{RecordingError, Result}; /// Recording engine that manages the entire recording pipeline. pub struct RecordingEngine { /// OBS context. context: Option, /// Current settings. settings: Arc>, /// Current recording output. current_output: Option, /// Recording start time. start_time: Option>, /// Whether recording is active. is_recording: bool, } impl RecordingEngine { /// Create a new recording engine. pub fn new(settings: Settings) -> Self { Self { context: None, settings: Arc::new(RwLock::new(settings)), current_output: None, start_time: None, is_recording: false, } } /// Initialize the recording engine. /// /// This sets up OBS and prepares for recording. pub fn initialize(&mut self) -> Result<()> { info!("Initializing recording engine"); let settings = self.settings.read().clone(); // Build OBS context let context = ObsContextBuilder::new() .with_video_settings(settings.video.clone()) .with_audio_settings(settings.audio.clone()) .with_output_dir(&settings.output.path) .build()?; self.context = Some(context); info!("Recording engine initialized successfully"); Ok(()) } /// Update settings. pub fn update_settings(&self, new_settings: Settings) -> Result<()> { let mut settings = self.settings.write(); *settings = new_settings; Ok(()) } /// Get current settings. pub fn settings(&self) -> Settings { self.settings.read().clone() } /// Check if currently recording. pub fn is_recording(&self) -> bool { self.is_recording } /// Get the current game ID if recording. pub fn current_game_id(&self) -> Option { self.current_output.as_ref().and_then(|o| o.game_id) } /// Start recording. /// /// # Arguments /// * `game_id` - Optional game ID for the recording. /// * `champion` - Optional champion name for the filename. pub fn start_recording(&mut self, game_id: Option, champion: Option<&str>) -> Result<()> { info!( "RecordingEngine::start_recording called - game_id: {:?}, champion: {:?}", game_id, champion ); if self.is_recording { warn!("Already recording, returning error"); return Err(RecordingError::AlreadyRecording.into()); } info!("Reading settings..."); let settings = self.settings.read().clone(); // Generate output filename let filename = self.generate_filename(game_id, champion); let output_path = settings.output.path.join(&filename); info!("Starting recording to: {:?}", output_path); // Start the recording let context = self.context.as_mut().ok_or_else(|| { error!("OBS not initialized when start_recording was called"); RecordingError::ObsInitError("OBS not initialized".to_string()) })?; info!("Calling OBS context start_recording..."); context.start_recording(&output_path)?; info!("OBS context start_recording returned successfully"); self.current_output = Some(RecordingOutput { path: output_path, game_id, champion: champion.map(String::from), }); self.start_time = Some(Utc::now()); self.is_recording = true; info!("Recording started successfully"); Ok(()) } /// Stop recording. pub fn stop_recording(&mut self) -> Result { if !self.is_recording { return Err(RecordingError::NotRecording.into()); } let context = self.context.as_mut().ok_or(RecordingError::ObsInitError( "OBS not initialized".to_string(), ))?; info!("Stopping recording"); // Stop the recording context.stop_recording()?; let output = self .current_output .take() .ok_or(RecordingError::StopError("No active output".to_string()))?; let start_time = self .start_time .take() .ok_or(RecordingError::StopError("No start time".to_string()))?; let end_time = Utc::now(); let duration = end_time - start_time; self.is_recording = false; let result = RecordingResult { path: output.path, game_id: output.game_id, champion: output.champion, start_time, end_time, duration, stats: None, // Stats would be populated from OBS context if available }; info!("Recording stopped: {:?}", result.path); Ok(result) } /// Get the current recording timestamp (time since recording started). pub fn current_timestamp(&self) -> Option { if !self.is_recording { return None; } self.start_time.map(|start| Utc::now() - start) } /// Generate a filename for the recording. fn generate_filename(&self, game_id: Option, champion: Option<&str>) -> String { let settings = self.settings.read(); let now = Utc::now(); let date = now.format("%Y-%m-%d").to_string(); let time = now.format("%H-%M-%S").to_string(); let game_id_str = game_id.map(|id| id.to_string()).unwrap_or_default(); let champion_str = champion.unwrap_or("unknown"); let filename = settings .output .naming_pattern .replace("{date}", &date) .replace("{time}", &time) .replace("{game_id}", &game_id_str) .replace("{champion}", champion_str); format!("{}.{}", filename, settings.output.container) } /// Shutdown the recording engine. pub fn shutdown(&mut self) -> Result<()> { if self.is_recording { self.stop_recording()?; } if let Some(mut context) = self.context.take() { context.shutdown()?; } info!("Recording engine shut down"); Ok(()) } } impl Drop for RecordingEngine { fn drop(&mut self) { if let Err(e) = self.shutdown() { warn!("Error shutting down recording engine: {}", e); } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_recording_engine_creation() { let settings = Settings::default(); let engine = RecordingEngine::new(settings); assert!(!engine.is_recording()); } #[test] fn test_filename_generation() { let settings = Settings::default(); let engine = RecordingEngine::new(settings); let filename = engine.generate_filename(Some(12345), Some("Ahri")); assert!(filename.ends_with(".mp4")); assert!(filename.contains("Ahri")); } }