diff --git a/server/src/wled_controller/core/capture_engines/wgc_engine.py b/server/src/wled_controller/core/capture_engines/wgc_engine.py index 4c1895d..91b97ac 100644 --- a/server/src/wled_controller/core/capture_engines/wgc_engine.py +++ b/server/src/wled_controller/core/capture_engines/wgc_engine.py @@ -92,10 +92,6 @@ class WGCEngine(CaptureEngine): try: logger.debug("WGC frame callback triggered") - # Store capture_control reference for cleanup - if self._capture_control is None: - self._capture_control = capture_control - # Get frame buffer as numpy array frame_buffer = frame.frame_buffer width = frame.width @@ -126,8 +122,9 @@ class WGCEngine(CaptureEngine): self._capture_instance.closed_handler = on_closed # Start capture using free-threaded mode (non-blocking) + # IMPORTANT: start_free_threaded() returns a CaptureControl object for cleanup logger.debug("Starting WGC capture (free-threaded mode)...") - self._capture_instance.start_free_threaded() + self._capture_control = self._capture_instance.start_free_threaded() # Wait for first frame to arrive (with timeout) logger.debug("Waiting for first WGC frame...") @@ -152,48 +149,43 @@ class WGCEngine(CaptureEngine): def cleanup(self) -> None: """Cleanup WGC resources.""" - # For free-threaded captures, cleanup is tricky: - # 1. Stop capture via capture_control if available - # 2. Explicitly delete the capture instance (releases COM objects) - # 3. Wait for resources to be freed (give Windows time to cleanup) - # 4. Force garbage collection multiple times to ensure COM cleanup + # Proper cleanup for free-threaded captures: + # 1. Stop capture via CaptureControl.stop() (signals thread to stop) + # 2. Wait for thread to finish using CaptureControl.wait() (blocks until done) + # 3. Delete capture instance (releases COM objects) + # 4. Force garbage collection (ensures COM cleanup) if self._capture_control: try: - logger.debug("Stopping WGC capture session via capture_control...") + logger.debug("Stopping WGC capture thread...") self._capture_control.stop() + + logger.debug("Waiting for WGC capture thread to finish...") + # This will block until the capture thread actually finishes + # This is the CORRECT way to wait for cleanup (not a timeout!) + self._capture_control.wait() + logger.debug("WGC capture thread finished successfully") except Exception as e: - logger.error(f"Error stopping WGC capture_control: {e}") + logger.error(f"Error during WGC capture control cleanup: {e}", exc_info=True) finally: self._capture_control = None - # Explicitly delete the capture instance BEFORE waiting - # This is critical for releasing COM objects and GPU resources + # Now that the thread has stopped, delete the capture instance if self._capture_instance: try: - logger.debug("Explicitly deleting WGC capture instance...") + logger.debug("Deleting WGC capture instance...") instance = self._capture_instance self._capture_instance = None del instance logger.debug("WGC capture instance deleted") except Exception as e: - logger.error(f"Error releasing WGC capture instance: {e}") + logger.error(f"Error deleting WGC capture instance: {e}", exc_info=True) self._capture_instance = None - # Force garbage collection multiple times to ensure COM cleanup - # COM objects may require multiple GC passes to fully release - logger.debug("Forcing garbage collection for COM cleanup...") - for i in range(3): - gc.collect() - time.sleep(0.05) # Small delay between GC passes - - logger.debug("Waiting for WGC resources to be fully released...") - # Give Windows time to clean up the capture session and remove border - time.sleep(0.3) - - # Final GC pass + # Force garbage collection to release COM objects + logger.debug("Running garbage collection for COM cleanup...") gc.collect() - logger.debug("Final garbage collection completed") + logger.debug("Garbage collection completed") with self._frame_lock: self._latest_frame = None