record-daemon: fix obs recording
This commit is contained in:
+206
-28
@@ -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)]
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user