259 lines
7.6 KiB
Rust
259 lines
7.6 KiB
Rust
//! 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<ObsContext>,
|
|
/// Current settings.
|
|
settings: Arc<RwLock<Settings>>,
|
|
/// Current recording output.
|
|
current_output: Option<RecordingOutput>,
|
|
/// Recording start time.
|
|
start_time: Option<DateTime<Utc>>,
|
|
/// 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<u64> {
|
|
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<u64>, 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<RecordingResult> {
|
|
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<chrono::Duration> {
|
|
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<u64>, 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"));
|
|
}
|
|
}
|