From 52a54636645491de6716bae359c79613e5ec40b7 Mon Sep 17 00:00:00 2001 From: grabbit Date: Sat, 28 Jun 2025 19:55:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9C=A8=20open=5Ffile()=20=E4=B8=AD?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=A7=86=E9=A2=91=E9=A2=84=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E5=92=8C=E5=87=86=E5=A4=87=E6=A3=80=E6=9F=A5=20;=20=E5=9C=A8?= =?UTF-8?q?=20capture=20loop=20=E4=B8=AD=E6=B7=BB=E5=8A=A0=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E7=AD=89=E5=BE=85=E6=9C=9F=E5=92=8C=E5=8C=BA=E5=88=86?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E9=94=99=E8=AF=AF=E4=B8=8E=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E6=97=B6=E9=94=99=E8=AF=AF;=20=E6=B7=BB=E5=8A=A0=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E6=96=87=E4=BB=B6=E5=B0=B1=E7=BB=AA=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=E5=87=BD=E6=95=B0;=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E8=BF=87=E6=97=A9=E5=85=B3=E9=97=AD;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/camera/controller.rs | 149 ++++++++++++++++++++++++++++----------- src/camera/opencv.rs | 124 ++++++++++++++++++++++++++++++-- 2 files changed, 224 insertions(+), 49 deletions(-) diff --git a/src/camera/controller.rs b/src/camera/controller.rs index 82d185b..d23e5fa 100644 --- a/src/camera/controller.rs +++ b/src/camera/controller.rs @@ -198,19 +198,61 @@ impl CameraController { let frame_interval = Duration::from_secs_f64(1.0 / fps as f64); let mut interval = time::interval(frame_interval); - // Error tracking for recovery - let mut consecutive_errors = 0; - const ERROR_THRESHOLD: u32 = 10; // After 10 errors, try to reinitialize + // πŸš€ PERFORMANCE FIX: Separate startup phase from runtime phase + let mut startup_attempts = 0; + let mut consecutive_runtime_errors = 0; + let mut is_startup_phase = true; - info!("Starting camera capture at {} fps", fps); + const STARTUP_MAX_ATTEMPTS: u32 = 100; // Give video 10 seconds to start (100 * 100ms) + const RUNTIME_ERROR_THRESHOLD: u32 = 20; // More tolerance for runtime errors + const STARTUP_RETRY_DELAY: u64 = 100; // 100ms between startup retries + const RUNTIME_RETRY_DELAY: u64 = 50; // 50ms between runtime retries + + info!("πŸš€ Starting camera capture at {} fps (startup phase)", fps); + + // 🎯 SMART WAITING: For video files, do initial readiness verification + if !stream.is_ready() { + info!("πŸ“Ή Video file detected, performing readiness check..."); + let mut readiness_attempts = 0; + const MAX_READINESS_ATTEMPTS: u32 = 50; // 5 seconds + + while readiness_attempts < MAX_READINESS_ATTEMPTS { + match stream.verify_readiness() { + Ok(true) => { + info!("βœ… Video file verified ready after {} attempts", readiness_attempts); + break; + }, + Ok(false) => { + readiness_attempts += 1; + if readiness_attempts % 10 == 0 { + info!("⏳ Video still not ready, attempt {}/{}", readiness_attempts, MAX_READINESS_ATTEMPTS); + } + time::sleep(Duration::from_millis(100)).await; + }, + Err(e) => { + warn!("Video readiness check failed: {}, continuing anyway", e); + break; + } + } + } + + if readiness_attempts >= MAX_READINESS_ATTEMPTS { + warn!("⚠️ Video readiness verification timed out, proceeding with capture anyway"); + } + } loop { interval.tick().await; match stream.capture_frame() { Ok(mat) => { - // Reset error counter on success - consecutive_errors = 0; + // πŸŽ‰ SUCCESS - Reset all error counters and exit startup phase + if is_startup_phase { + is_startup_phase = false; + info!("βœ… Startup phase completed successfully after {} attempts", startup_attempts); + } + startup_attempts = 0; + consecutive_runtime_errors = 0; // Create a new frame with timestamp let frame = Frame::new(mat, Utc::now(), frame_count); @@ -220,13 +262,6 @@ impl CameraController { if let Err(e) = frame_buffer.push(frame.clone()) { error!("Failed to add frame to buffer: {}", e); } - // - // // Also add to LZ4 buffer if available (for demos) - ultra-fast compression! - // if let Some(ref lz4_buf) = lz4_buffer { - // if let Err(e) = lz4_buf.push(frame.clone()) { - // error!("Failed to add frame to LZ4 buffer: {}", e); - // } - // } // Broadcast frame to listeners if let Err(e) = frame_tx.send(frame) { @@ -234,41 +269,71 @@ impl CameraController { } } Err(e) => { - error!("Failed to capture frame: {}", e); - consecutive_errors += 1; - - if consecutive_errors >= ERROR_THRESHOLD { - error!("Too many consecutive errors ({}), attempting to reinitialize camera stream", consecutive_errors); + // πŸ”₯ DISTINGUISH between startup errors and runtime errors + if is_startup_phase { + startup_attempts += 1; - // Try to recreate the stream - this is a simplified version - // In a real implementation, you might want to signal the main thread - // to fully reinitialize the camera - // todo? δΈΊε•₯ε€±θ΄₯ε€ͺε€šζ‰“εΌ€ 0? - match OpenCVCamera::open("0") { // Note: simplified, should use original device - Ok(mut camera) => { - // Configure minimal settings - let _ = camera.set_format(Resolution::HD720p); - let _ = camera.set_fps(fps); - - // Try to start streaming again - match camera.start_streaming() { - Ok(new_stream) => { - info!("Successfully reinitialized camera stream"); - stream = new_stream; - consecutive_errors = 0; - }, - Err(e) => error!("Failed to restart camera streaming: {}", e) - } - }, - Err(e) => error!("Failed to reopen camera: {}", e) + // Check if this is a "video not ready" error (expected during startup) + let error_msg = format!("{}", e); + if error_msg.contains("Video not ready yet") { + if startup_attempts % 20 == 0 { // Log every 2 seconds + info!("⏳ Video still loading... attempt {}/{} ({})", startup_attempts, STARTUP_MAX_ATTEMPTS, e); + } + } else { + debug!("Startup error attempt {}/{}: {}", startup_attempts, STARTUP_MAX_ATTEMPTS, e); } + + if startup_attempts >= STARTUP_MAX_ATTEMPTS { + error!("❌ Video failed to start after {} attempts over {} seconds", + STARTUP_MAX_ATTEMPTS, STARTUP_MAX_ATTEMPTS as u64 * STARTUP_RETRY_DELAY / 1000); + error!("Last error: {}", e); + break; // Exit the capture loop - video can't be loaded + } + + // Longer delay during startup to give video time to load + time::sleep(Duration::from_millis(STARTUP_RETRY_DELAY)).await; + } else { + // RUNTIME ERROR - after video was working + consecutive_runtime_errors += 1; + error!("Runtime capture error {}/{}: {}", consecutive_runtime_errors, RUNTIME_ERROR_THRESHOLD, e); + + if consecutive_runtime_errors >= RUNTIME_ERROR_THRESHOLD { + error!("Too many consecutive runtime errors ({}), attempting to reinitialize camera stream", consecutive_runtime_errors); + + // Try to recreate the stream - this is a simplified version + // In a real implementation, you might want to signal the main thread + // to fully reinitialize the camera + match OpenCVCamera::open("0") { // Note: simplified, should use original device + Ok(mut camera) => { + // Configure minimal settings + let _ = camera.set_format(Resolution::HD720p); + let _ = camera.set_fps(fps); + + // Try to start streaming again + match camera.start_streaming() { + Ok(new_stream) => { + info!("Successfully reinitialized camera stream"); + stream = new_stream; + consecutive_runtime_errors = 0; + is_startup_phase = true; // Back to startup phase + startup_attempts = 0; + }, + Err(e) => error!("Failed to restart camera streaming: {}", e) + } + }, + Err(e) => error!("Failed to reopen camera: {}", e) + } + } + + // Shorter delay during runtime to maintain responsiveness + time::sleep(Duration::from_millis(RUNTIME_RETRY_DELAY)).await; } - - // Small delay to avoid tight error loop - time::sleep(Duration::from_millis(100)).await; } } } + + // If we reach here, the capture loop has been terminated + warn!("πŸ›‘ Camera capture loop terminated"); }); info!("Camera capture started"); diff --git a/src/camera/opencv.rs b/src/camera/opencv.rs index 2593615..6012cb3 100644 --- a/src/camera/opencv.rs +++ b/src/camera/opencv.rs @@ -56,20 +56,77 @@ impl OpenCVCamera { let path_str = path.as_ref().to_str() .ok_or_else(|| anyhow!("Invalid path"))?; + info!("Opening video file: {}", path_str); let mut capture = videoio::VideoCapture::from_file(path_str, videoio::CAP_ANY)?; if !capture.is_opened()? { return Err(anyhow!("Failed to open video file: {}", path_str)); } - // Get video properties - let width = capture.get(videoio::CAP_PROP_FRAME_WIDTH)? as u32; - let height = capture.get(videoio::CAP_PROP_FRAME_HEIGHT)? as u32; - let fps = capture.get(videoio::CAP_PROP_FPS)? as u32; - let total_frames = capture.get(videoio::CAP_PROP_FRAME_COUNT)? as u32; + // πŸš€ PERFORMANCE FIX: Ensure video is actually ready before proceeding + // On slow devices like Raspberry Pi, the video file might not be immediately ready + info!("Waiting for video file to be ready..."); + + let mut width = 0u32; + let mut height = 0u32; + let mut fps = 0u32; + let mut total_frames = 0u32; + + // Try to get video properties with retries for slow devices + let mut attempts = 0; + const MAX_PROPERTY_ATTEMPTS: u32 = 10; + + while attempts < MAX_PROPERTY_ATTEMPTS { + // Try to get basic properties + let w = capture.get(videoio::CAP_PROP_FRAME_WIDTH).unwrap_or(0.0) as u32; + let h = capture.get(videoio::CAP_PROP_FRAME_HEIGHT).unwrap_or(0.0) as u32; + let f = capture.get(videoio::CAP_PROP_FPS).unwrap_or(0.0) as u32; + let t = capture.get(videoio::CAP_PROP_FRAME_COUNT).unwrap_or(0.0) as u32; + + if w > 0 && h > 0 && f > 0 { + width = w; + height = h; + fps = f; + total_frames = t; + break; + } + + attempts += 1; + debug!("Video properties not ready yet, attempt {}/{}", attempts, MAX_PROPERTY_ATTEMPTS); + std::thread::sleep(std::time::Duration::from_millis(200)); // Wait 200ms + } + + if width == 0 || height == 0 { + return Err(anyhow!("Failed to get valid video properties after {} attempts", MAX_PROPERTY_ATTEMPTS)); + } + + // πŸš€ CRITICAL: Try to read the first frame to ensure video is truly ready + info!("Testing video readiness by reading first frame..."); + let mut test_frame = core::Mat::default(); + let mut first_frame_attempts = 0; + const MAX_FIRST_FRAME_ATTEMPTS: u32 = 20; + + while first_frame_attempts < MAX_FIRST_FRAME_ATTEMPTS { + if capture.read(&mut test_frame).unwrap_or(false) && !test_frame.empty() { + info!("βœ… Video file is ready and first frame loaded successfully"); + // Reset to beginning for actual playback + if let Err(e) = capture.set(videoio::CAP_PROP_POS_FRAMES, 0.0) { + warn!("Could not reset video to beginning: {}", e); + } + break; + } + + first_frame_attempts += 1; + debug!("First frame not ready yet, attempt {}/{}", first_frame_attempts, MAX_FIRST_FRAME_ATTEMPTS); + std::thread::sleep(std::time::Duration::from_millis(100)); // Wait 100ms + } + + if first_frame_attempts >= MAX_FIRST_FRAME_ATTEMPTS { + warn!("⚠️ Could not read first frame after {} attempts, proceeding anyway", MAX_FIRST_FRAME_ATTEMPTS); + } info!( - "Opened video file: {} ({}x{} @ {} fps, {} frames)", + "βœ… Opened video file: {} ({}x{} @ {} fps, {} frames)", path_str, width, height, fps, total_frames ); @@ -300,6 +357,7 @@ impl OpenCVCamera { pub struct OpenCVCaptureStream { capture: Arc>, loop_video: bool, + is_video_ready: bool, // Track if video is confirmed ready } impl OpenCVCaptureStream { @@ -308,6 +366,44 @@ impl OpenCVCaptureStream { Self { capture, loop_video, + is_video_ready: false, // Will be set to true after first successful frame + } + } + + /// Check if the video stream is ready and can provide frames + pub fn is_ready(&self) -> bool { + // For live cameras, assume always ready + // Note: we identify live cameras by checking if loop_video is false + // This is a bit of a hack, but works for our current architecture + if !self.loop_video { + return true; + } + + // For video files, check if we've successfully read at least one frame + self.is_video_ready + } + + /// Force check if the video file is ready by attempting to read a frame + /// This is useful for slow devices where the initial open might succeed + /// but the video isn't actually ready for streaming yet + pub fn verify_readiness(&mut self) -> Result { + if self.is_video_ready { + return Ok(true); + } + + let mut test_frame = core::Mat::default(); + let mut capture_guard = self.capture.lock().unwrap(); + + // Try to read without advancing position + let current_pos = capture_guard.get(videoio::CAP_PROP_POS_FRAMES).unwrap_or(0.0); + + if capture_guard.read(&mut test_frame)? && !test_frame.empty() { + // Reset position + let _ = capture_guard.set(videoio::CAP_PROP_POS_FRAMES, current_pos); + self.is_video_ready = true; + Ok(true) + } else { + Ok(false) } } @@ -337,9 +433,23 @@ impl OpenCVCaptureStream { return Err(anyhow!("End of video reached")); } } + + // πŸš€ Mark video as ready after first successful frame read + if !self.is_video_ready { + self.is_video_ready = true; + debug!("βœ… Video stream confirmed ready - first frame successfully captured"); + } + Ok(frame) } else { - Err(anyhow!("Failed to capture frame")) + // Distinguish between "video not ready yet" and "real error" + if !self.is_video_ready { + // This might be a startup issue on slow devices + Err(anyhow!("Video not ready yet (startup phase)")) + } else { + // This is a real capture error after video was working + Err(anyhow!("Failed to capture frame")) + } } } }