record-daemon: fix obs recording

This commit is contained in:
2026-03-20 20:34:44 +01:00
parent dbb224e118
commit 1166424c29
12 changed files with 3717 additions and 515 deletions
+206 -28
View File
@@ -3,6 +3,7 @@
use std::sync::Arc;
use clap::Parser;
use libobs_bootstrapper::{ObsBootstrapper, ObsBootstrapperOptions, ObsBootstrapperResult};
use parking_lot::RwLock;
use tokio::sync::broadcast;
use tracing::{debug, error, info, warn};
@@ -81,11 +82,31 @@ impl Daemon {
async fn init(&mut self) -> Result<()> {
info!("Initializing record daemon v{}", record_daemon::VERSION);
// Initialize recording engine
// Initialize recording engine (blocking operation)
let settings = self.settings.read().clone();
let mut engine = RecordingEngine::new(settings);
engine.initialize()?;
*self.recording_engine.write() = Some(engine);
let recording_engine = self.recording_engine.clone();
let result = tokio::task::spawn_blocking(move || {
let mut engine = RecordingEngine::new(settings);
if let Err(e) = engine.initialize() {
return Err(format!("Failed to initialize recording engine: {:?}", e));
}
*recording_engine.write() = Some(engine);
Ok::<_, String>(())
})
.await;
match result {
Ok(Ok(())) => {}
Ok(Err(e)) => return Err(record_daemon::error::RecordingError::ObsInitError(e).into()),
Err(e) => {
return Err(record_daemon::error::RecordingError::ObsInitError(format!(
"spawn_blocking error during init: {:?}",
e
))
.into())
}
}
// Load existing recordings from disk
self.timeline_store.read().load_from_disk()?;
@@ -201,26 +222,79 @@ impl Daemon {
/// Handle a game event.
async fn handle_game_event(&self, event: GameEvent) -> Result<()> {
debug!("Game event: {:?}", event);
use std::io::Write;
info!("[EVENT_HANDLER] Game event received: {:?}", event);
std::io::stderr().flush().ok();
// Process state transitions
if let Some(transition) = self.state_machine.process_event(&event) {
info!("[EVENT_HANDLER] State transition: {:?}", transition);
std::io::stderr().flush().ok();
self.state_machine.transition(transition.clone());
// Handle recording start/stop
match transition {
StateTransition::GameStarted { game_id, champion } => {
info!(
"[EVENT_HANDLER] GameStarted transition - game_id: {}, champion: {:?}",
game_id, champion
);
std::io::stderr().flush().ok();
// If already recording, stop the current recording first
if self.state_machine.is_recording() {
info!("Stopping previous recording before starting new one");
info!(
"[EVENT_HANDLER] Stopping previous recording before starting new one"
);
std::io::stderr().flush().ok();
if let Err(e) = self.stop_recording().await {
warn!("Failed to stop previous recording: {}", e);
warn!("[EVENT_HANDLER] Failed to stop previous recording: {}", e);
std::io::stderr().flush().ok();
}
}
self.start_recording(game_id, champion.as_deref()).await?;
info!("[EVENT_HANDLER] Calling start_recording...");
std::io::stderr().flush().ok();
// Wrap the start_recording call to catch any panics
let start_result =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
// We need to use a blocking approach here since we're in catch_unwind
// The actual async call happens outside
}));
if let Err(panic_info) = start_result {
error!(
"[EVENT_HANDLER] PANIC before start_recording: {:?}",
panic_info
);
std::io::stderr().flush().ok();
eprintln!(
"[EVENT_HANDLER] PANIC before start_recording: {:?}",
panic_info
);
}
if let Err(e) = self.start_recording(game_id, champion.as_deref()).await {
error!("[EVENT_HANDLER] Failed to start recording: {}", e);
std::io::stderr().flush().ok();
// Don't propagate error - keep daemon running
} else {
info!("[EVENT_HANDLER] start_recording completed successfully");
std::io::stderr().flush().ok();
}
}
StateTransition::GameEnded => {
self.stop_recording().await?;
info!("[EVENT_HANDLER] GameEnded transition");
std::io::stderr().flush().ok();
if let Err(e) = self.stop_recording().await {
error!("[EVENT_HANDLER] Failed to stop recording: {}", e);
std::io::stderr().flush().ok();
// Don't propagate error - keep daemon running
}
}
_ => {}
}
@@ -237,36 +311,80 @@ impl Daemon {
}
}
info!("[EVENT_HANDLER] Event handling complete");
std::io::stderr().flush().ok();
Ok(())
}
/// Start recording.
async fn start_recording(&self, game_id: u64, champion: Option<&str>) -> Result<()> {
info!("Starting recording for game {} ({:?})", game_id, champion);
info!(
"Daemon::start_recording called - game {} ({:?})",
game_id, champion
);
let mut engine_guard = self.recording_engine.write();
if let Some(ref mut engine) = *engine_guard {
engine.start_recording(Some(game_id), champion)?;
self.event_mapper.write().start();
}
// Clone Arc references for use in spawn_blocking
let recording_engine = self.recording_engine.clone();
let event_mapper = self.event_mapper.clone();
let champion_owned = champion.map(|s| s.to_string());
Ok(())
// Use spawn_blocking to avoid blocking the async runtime
tokio::task::spawn_blocking(move || {
info!("Acquiring recording engine write lock...");
let mut engine_guard = recording_engine.write();
info!("Recording engine lock acquired");
if let Some(ref mut engine) = *engine_guard {
info!("Calling engine.start_recording...");
engine.start_recording(Some(game_id), champion_owned.as_deref())?;
info!("engine.start_recording returned successfully");
event_mapper.write().start();
info!("Event mapper started");
} else {
warn!("Recording engine is None!");
}
info!("Daemon::start_recording completed successfully");
Ok(())
})
.await
.map_err(|e| {
record_daemon::error::RecordingError::ObsInitError(format!(
"spawn_blocking error: {:?}",
e
))
})?
}
/// Stop recording.
async fn stop_recording(&self) -> Result<()> {
info!("Stopping recording");
let mut engine_guard = self.recording_engine.write();
if let Some(ref mut engine) = *engine_guard {
let result = engine.stop_recording()?;
self.event_mapper.write().stop();
// Clone Arc references for use in spawn_blocking
let recording_engine = self.recording_engine.clone();
let event_mapper = self.event_mapper.clone();
let timeline_store = self.timeline_store.clone();
// Save to timeline
self.timeline_store.write().add_recording(result)?;
}
// Use spawn_blocking to avoid blocking the async runtime
tokio::task::spawn_blocking(move || {
let mut engine_guard = recording_engine.write();
if let Some(ref mut engine) = *engine_guard {
let result = engine.stop_recording()?;
event_mapper.write().stop();
Ok(())
// Save to timeline
timeline_store.write().add_recording(result)?;
}
Ok(())
})
.await
.map_err(|e| {
record_daemon::error::RecordingError::ObsInitError(format!(
"spawn_blocking error: {:?}",
e
))
})?
}
/// Shutdown the daemon.
@@ -283,9 +401,28 @@ impl Daemon {
ipc_server.stop().await?;
}
// Shutdown recording engine
if let Some(ref mut engine) = *self.recording_engine.write() {
engine.shutdown()?;
// Shutdown recording engine (blocking operation)
let recording_engine = self.recording_engine.clone();
let result = tokio::task::spawn_blocking(move || {
if let Some(ref mut engine) = *recording_engine.write() {
if let Err(e) = engine.shutdown() {
return Err(format!("Failed to shutdown recording engine: {:?}", e));
}
}
Ok::<_, String>(())
})
.await;
match result {
Ok(Ok(())) => {}
Ok(Err(e)) => return Err(record_daemon::error::RecordingError::ObsInitError(e).into()),
Err(e) => {
return Err(record_daemon::error::RecordingError::ObsInitError(format!(
"spawn_blocking error during shutdown: {:?}",
e
))
.into())
}
}
info!("Daemon shutdown complete");
@@ -302,6 +439,25 @@ fn init_logging(level: &str) {
.with(filter)
.with(tracing_subscriber::fmt::layer())
.init();
// Set up panic hook to log panics
std::panic::set_hook(Box::new(|panic_info| {
let location = panic_info
.location()
.map(|loc| format!("{}:{}:{}", loc.file(), loc.line(), loc.column()))
.unwrap_or_else(|| "unknown location".to_string());
let message = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
s.clone()
} else {
"Unknown panic".to_string()
};
error!("PANIC at {}: {}", location, message);
eprintln!("PANIC at {}: {}", location, message);
}));
}
#[tokio::main]
@@ -313,6 +469,28 @@ async fn main() -> Result<()> {
info!("Record Daemon v{} starting", record_daemon::VERSION);
// Bootstrap OBS - download and extract if needed
info!("Bootstrapping OBS...");
let bootstrap_options = ObsBootstrapperOptions::default().set_update(false);
let bootstrap_result = ObsBootstrapper::bootstrap(&bootstrap_options).await;
match bootstrap_result {
Ok(ObsBootstrapperResult::Restart) => {
info!("OBS has been downloaded and extracted.");
return Ok(());
}
Ok(ObsBootstrapperResult::None) => {
info!("OBS bootstrap complete, continuing...");
}
Err(e) => {
error!("Failed to bootstrap OBS: {:?}", e);
return Err(record_daemon::error::RecordingError::ObsInitError(format!(
"OBS bootstrap failed: {:?}",
e
))
.into());
}
}
// Load configuration
let settings = if let Some(config_path) = args.config {
config::ConfigPersistence::new(config_path).load()?
@@ -324,7 +502,7 @@ async fn main() -> Result<()> {
let mut daemon = Daemon::new(settings);
// Handle shutdown signals
let shutdown_tx = daemon.shutdown_tx.clone();
let _shutdown_tx = daemon.shutdown_tx.clone();
#[cfg(unix)]
{