diff --git a/apps/desktop/src-tauri/src/camera.rs b/apps/desktop/src-tauri/src/camera.rs index b1766d58e2..9532807114 100644 --- a/apps/desktop/src-tauri/src/camera.rs +++ b/apps/desktop/src-tauri/src/camera.rs @@ -17,8 +17,9 @@ use tokio::{ runtime::Runtime, sync::{broadcast, oneshot}, task::LocalSet, + time::{Duration, Instant}, }; -use tracing::{error, info, trace}; +use tracing::{error, info, trace, warn}; use wgpu::{CompositeAlphaMode, SurfaceTexture}; static TOOLBAR_HEIGHT: f32 = 56.0; // also defined in Typescript @@ -496,8 +497,23 @@ impl Renderer { return; }; + let start_time = Instant::now(); + let startup_timeout = Duration::from_secs(5); + let mut received_first_frame = false; + let mut state = default_state; while let Some(event) = loop { + let timeout_remaining = if received_first_frame { + Duration::MAX + } else { + startup_timeout.saturating_sub(start_time.elapsed()) + }; + + if timeout_remaining.is_zero() { + warn!("Camera preview timed out waiting for first frame, closing window"); + break None; + } + tokio::select! { frame = camera_rx.recv_async() => break frame.ok().map(Ok), result = reconfigure.recv() => { @@ -507,10 +523,15 @@ impl Renderer { continue; } }, + _ = tokio::time::sleep(timeout_remaining) => { + warn!("Camera preview timed out waiting for first frame, closing window"); + break None; + } } } { match event { Ok(frame) => { + received_first_frame = true; let aspect_ratio = frame.inner.width() as f32 / frame.inner.height() as f32; self.sync_ratio_uniform_and_resize_window_to_it(&window, &state, aspect_ratio); diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 12c876e2fe..197b37aaba 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -456,12 +456,17 @@ async fn set_camera_input( .map_err(|e| e.to_string())?; } Some(id) => { + { + let app = &mut *state.write().await; + app.selected_camera_id = Some(id.clone()); + app.camera_in_use = true; + app.camera_cleanup_done = false; + } + let mut attempts = 0; - loop { + let init_result: Result<(), String> = loop { attempts += 1; - // We first ask the actor to set the input - // This returns a future that resolves when the camera is actually ready let request = camera_feed .ask(feeds::camera::SetInput { id: id.clone() }) .await @@ -473,10 +478,10 @@ async fn set_camera_input( }; match result { - Ok(_) => break, + Ok(_) => break Ok(()), Err(e) => { if attempts >= 3 { - return Err(format!( + break Err(format!( "Failed to initialize camera after {} attempts: {}", attempts, e )); @@ -488,6 +493,13 @@ async fn set_camera_input( tokio::time::sleep(Duration::from_millis(500)).await; } } + }; + + if let Err(e) = init_result { + let app = &mut *state.write().await; + app.selected_camera_id = None; + app.camera_in_use = false; + return Err(e); } ShowCapWindow::Camera @@ -2758,7 +2770,13 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { let state = app.state::>(); let app_state = &mut *state.write().await; - if !app_state.is_recording_active_or_pending() { + let camera_window_open = + CapWindowId::Camera.get(&app).is_some(); + + if !app_state.is_recording_active_or_pending() + && !camera_window_open + && !app_state.camera_in_use + { let _ = app_state.mic_feed.ask(microphone::RemoveInput).await; let _ = app_state @@ -2768,7 +2786,6 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { app_state.selected_mic_label = None; app_state.selected_camera_id = None; - app_state.camera_in_use = false; } }); }