diff --git a/server/src/wled_controller/core/live_stream.py b/server/src/wled_controller/core/live_stream.py index 7ba5637..f420ad0 100644 --- a/server/src/wled_controller/core/live_stream.py +++ b/server/src/wled_controller/core/live_stream.py @@ -141,8 +141,11 @@ class ScreenCaptureLiveStream(LiveStream): except Exception as e: logger.error(f"Capture error (display={self._capture_stream.display_index}): {e}") - # No FPS throttling - capture as fast as frames are available - # But sleep briefly during idle periods to avoid burning CPU + # Throttle to target FPS + elapsed = time.time() - loop_start + remaining = frame_time - elapsed + if remaining > 0: + time.sleep(remaining) class ProcessedLiveStream(LiveStream): @@ -209,18 +212,17 @@ class ProcessedLiveStream(LiveStream): # processed by a consumer), so the 3rd slot is always safe to reuse. _ring: List[Optional[np.ndarray]] = [None, None, None] _ring_idx = 0 + frame_time = 1.0 / self._source.target_fps if self._source.target_fps > 0 else 1.0 while self._running: - source_frame = self._source.get_latest_frame() - if source_frame is None: - # Small sleep when waiting for frames to avoid CPU spinning - time.sleep(0.001) - continue + loop_start = time.time() - # Identity cache: Skip processing duplicate frames to save CPU - # (Compare object identity to detect when capture engine returns same frame) - if source_frame is cached_source_frame: - time.sleep(0.001) + source_frame = self._source.get_latest_frame() + if source_frame is None or source_frame is cached_source_frame: + # Sleep until next frame is expected + elapsed = time.time() - loop_start + remaining = frame_time - elapsed + time.sleep(max(remaining, 0.001)) continue cached_source_frame = source_frame diff --git a/server/src/wled_controller/core/processor_manager.py b/server/src/wled_controller/core/processor_manager.py index 7ebe11e..8f35d17 100644 --- a/server/src/wled_controller/core/processor_manager.py +++ b/server/src/wled_controller/core/processor_manager.py @@ -859,8 +859,11 @@ class ProcessorManager: state.metrics.last_error = str(e) logger.error(f"Processing error for target {target_id}: {e}", exc_info=True) - # No FPS control - process frames as fast as they arrive (match test behavior) - pass + # Throttle to target FPS + elapsed = time.time() - loop_start + remaining = frame_time - elapsed + if remaining > 0: + await asyncio.sleep(remaining) except asyncio.CancelledError: logger.info(f"Processing loop cancelled for target {target_id}") @@ -1503,8 +1506,11 @@ class ProcessorManager: state.metrics.last_error = str(e) logger.error(f"KC processing error for {target_id}: {e}", exc_info=True) - # No FPS control - process frames as fast as they arrive (match test behavior) - pass + # Throttle to target FPS + elapsed = time.time() - loop_start + remaining = frame_time - elapsed + if remaining > 0: + await asyncio.sleep(remaining) except asyncio.CancelledError: logger.info(f"KC processing loop cancelled for target {target_id}")