From cd3137b0ec326a6c752f6ec5115aaa8263b9f634 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Mon, 23 Mar 2026 02:44:46 +0300 Subject: [PATCH] refactor: make OpenCV an optional dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Camera engine and video stream support now gracefully degrade when opencv-python-headless is not installed. The app starts fine without it — camera engine simply doesn't register and video streams raise a clear ImportError with install instructions. Saves ~45MB for users who don't need camera/video capture. --- .../core/capture_engines/__init__.py | 16 +++++++++++---- .../core/processing/live_stream_manager.py | 13 ++++++++++-- .../core/processing/video_stream.py | 20 +++++++++++++++++-- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/server/src/wled_controller/core/capture_engines/__init__.py b/server/src/wled_controller/core/capture_engines/__init__.py index ea2fdc5..2bd901f 100644 --- a/server/src/wled_controller/core/capture_engines/__init__.py +++ b/server/src/wled_controller/core/capture_engines/__init__.py @@ -12,16 +12,23 @@ from wled_controller.core.capture_engines.dxcam_engine import DXcamEngine, DXcam from wled_controller.core.capture_engines.bettercam_engine import BetterCamEngine, BetterCamCaptureStream from wled_controller.core.capture_engines.wgc_engine import WGCEngine, WGCCaptureStream from wled_controller.core.capture_engines.scrcpy_engine import ScrcpyEngine, ScrcpyCaptureStream -from wled_controller.core.capture_engines.camera_engine import CameraEngine, CameraCaptureStream from wled_controller.core.capture_engines.demo_engine import DemoCaptureEngine, DemoCaptureStream +# Camera engine requires OpenCV — optional dependency +try: + from wled_controller.core.capture_engines.camera_engine import CameraEngine, CameraCaptureStream + _has_camera = True +except ImportError: + _has_camera = False + # Auto-register available engines EngineRegistry.register(MSSEngine) EngineRegistry.register(DXcamEngine) EngineRegistry.register(BetterCamEngine) EngineRegistry.register(WGCEngine) EngineRegistry.register(ScrcpyEngine) -EngineRegistry.register(CameraEngine) +if _has_camera: + EngineRegistry.register(CameraEngine) EngineRegistry.register(DemoCaptureEngine) __all__ = [ @@ -40,8 +47,9 @@ __all__ = [ "WGCCaptureStream", "ScrcpyEngine", "ScrcpyCaptureStream", - "CameraEngine", - "CameraCaptureStream", "DemoCaptureEngine", "DemoCaptureStream", ] + +if _has_camera: + __all__ += ["CameraEngine", "CameraCaptureStream"] diff --git a/server/src/wled_controller/core/processing/live_stream_manager.py b/server/src/wled_controller/core/processing/live_stream_manager.py index c974a04..0f3cc3c 100644 --- a/server/src/wled_controller/core/processing/live_stream_manager.py +++ b/server/src/wled_controller/core/processing/live_stream_manager.py @@ -22,7 +22,11 @@ from wled_controller.core.processing.live_stream import ( ScreenCaptureLiveStream, StaticImageLiveStream, ) -from wled_controller.core.processing.video_stream import VideoCaptureLiveStream +try: + from wled_controller.core.processing.video_stream import VideoCaptureLiveStream + _has_video = True +except ImportError: + _has_video = False from wled_controller.utils import get_logger logger = get_logger(__name__) @@ -264,8 +268,13 @@ class LiveStreamManager: logger.warning(f"Skipping unknown filter '{fi.filter_id}': {e}") return resolved - def _create_video_live_stream(self, config) -> VideoCaptureLiveStream: + def _create_video_live_stream(self, config): """Create a VideoCaptureLiveStream from a VideoCaptureSource config.""" + if not _has_video: + raise ImportError( + "OpenCV is required for video stream support. " + "Install it with: pip install opencv-python-headless" + ) stream = VideoCaptureLiveStream( url=config.url, loop=config.loop, diff --git a/server/src/wled_controller/core/processing/video_stream.py b/server/src/wled_controller/core/processing/video_stream.py index d73b6c7..3999b48 100644 --- a/server/src/wled_controller/core/processing/video_stream.py +++ b/server/src/wled_controller/core/processing/video_stream.py @@ -9,9 +9,14 @@ import threading import time from typing import Optional -import cv2 import numpy as np +try: + import cv2 + _has_cv2 = True +except ImportError: + _has_cv2 = False + from wled_controller.core.capture_engines.base import ScreenCapture from wled_controller.core.processing.live_stream import LiveStream from wled_controller.utils import get_logger @@ -67,12 +72,22 @@ def resolve_youtube_url(url: str, resolution_limit: Optional[int] = None) -> str return stream_url +def _require_cv2(): + """Raise a clear error if OpenCV is not installed.""" + if not _has_cv2: + raise ImportError( + "OpenCV is required for camera and video support. " + "Install it with: pip install opencv-python-headless" + ) + + def extract_thumbnail(url: str, resolution_limit: Optional[int] = None) -> Optional[np.ndarray]: """Extract the first frame of a video as a thumbnail (RGB numpy array). For YouTube URLs, resolves via yt-dlp first. Returns None on failure. """ + _require_cv2() try: actual_url = url if is_youtube_url(url): @@ -127,6 +142,7 @@ class VideoCaptureLiveStream(LiveStream): resolution_limit: Optional[int] = None, target_fps: int = 30, ): + _require_cv2() self._original_url = url self._resolved_url: Optional[str] = None self._loop = loop @@ -136,7 +152,7 @@ class VideoCaptureLiveStream(LiveStream): self._resolution_limit = resolution_limit self._target_fps = target_fps - self._cap: Optional[cv2.VideoCapture] = None + self._cap = None # Optional[cv2.VideoCapture] self._video_fps: float = 30.0 self._total_frames: int = 0 self._video_duration: float = 0.0