refactor: make OpenCV an optional dependency
All checks were successful
Lint & Test / test (push) Successful in 25s

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.
This commit is contained in:
2026-03-23 02:44:46 +03:00
parent e391346b4b
commit cd3137b0ec
3 changed files with 41 additions and 8 deletions

View File

@@ -12,15 +12,22 @@ 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.bettercam_engine import BetterCamEngine, BetterCamCaptureStream
from wled_controller.core.capture_engines.wgc_engine import WGCEngine, WGCCaptureStream 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.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 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 # Auto-register available engines
EngineRegistry.register(MSSEngine) EngineRegistry.register(MSSEngine)
EngineRegistry.register(DXcamEngine) EngineRegistry.register(DXcamEngine)
EngineRegistry.register(BetterCamEngine) EngineRegistry.register(BetterCamEngine)
EngineRegistry.register(WGCEngine) EngineRegistry.register(WGCEngine)
EngineRegistry.register(ScrcpyEngine) EngineRegistry.register(ScrcpyEngine)
if _has_camera:
EngineRegistry.register(CameraEngine) EngineRegistry.register(CameraEngine)
EngineRegistry.register(DemoCaptureEngine) EngineRegistry.register(DemoCaptureEngine)
@@ -40,8 +47,9 @@ __all__ = [
"WGCCaptureStream", "WGCCaptureStream",
"ScrcpyEngine", "ScrcpyEngine",
"ScrcpyCaptureStream", "ScrcpyCaptureStream",
"CameraEngine",
"CameraCaptureStream",
"DemoCaptureEngine", "DemoCaptureEngine",
"DemoCaptureStream", "DemoCaptureStream",
] ]
if _has_camera:
__all__ += ["CameraEngine", "CameraCaptureStream"]

View File

@@ -22,7 +22,11 @@ from wled_controller.core.processing.live_stream import (
ScreenCaptureLiveStream, ScreenCaptureLiveStream,
StaticImageLiveStream, StaticImageLiveStream,
) )
try:
from wled_controller.core.processing.video_stream import VideoCaptureLiveStream from wled_controller.core.processing.video_stream import VideoCaptureLiveStream
_has_video = True
except ImportError:
_has_video = False
from wled_controller.utils import get_logger from wled_controller.utils import get_logger
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -264,8 +268,13 @@ class LiveStreamManager:
logger.warning(f"Skipping unknown filter '{fi.filter_id}': {e}") logger.warning(f"Skipping unknown filter '{fi.filter_id}': {e}")
return resolved return resolved
def _create_video_live_stream(self, config) -> VideoCaptureLiveStream: def _create_video_live_stream(self, config):
"""Create a VideoCaptureLiveStream from a VideoCaptureSource 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( stream = VideoCaptureLiveStream(
url=config.url, url=config.url,
loop=config.loop, loop=config.loop,

View File

@@ -9,9 +9,14 @@ import threading
import time import time
from typing import Optional from typing import Optional
import cv2
import numpy as np 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.capture_engines.base import ScreenCapture
from wled_controller.core.processing.live_stream import LiveStream from wled_controller.core.processing.live_stream import LiveStream
from wled_controller.utils import get_logger 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 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]: 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). """Extract the first frame of a video as a thumbnail (RGB numpy array).
For YouTube URLs, resolves via yt-dlp first. For YouTube URLs, resolves via yt-dlp first.
Returns None on failure. Returns None on failure.
""" """
_require_cv2()
try: try:
actual_url = url actual_url = url
if is_youtube_url(url): if is_youtube_url(url):
@@ -127,6 +142,7 @@ class VideoCaptureLiveStream(LiveStream):
resolution_limit: Optional[int] = None, resolution_limit: Optional[int] = None,
target_fps: int = 30, target_fps: int = 30,
): ):
_require_cv2()
self._original_url = url self._original_url = url
self._resolved_url: Optional[str] = None self._resolved_url: Optional[str] = None
self._loop = loop self._loop = loop
@@ -136,7 +152,7 @@ class VideoCaptureLiveStream(LiveStream):
self._resolution_limit = resolution_limit self._resolution_limit = resolution_limit
self._target_fps = target_fps self._target_fps = target_fps
self._cap: Optional[cv2.VideoCapture] = None self._cap = None # Optional[cv2.VideoCapture]
self._video_fps: float = 30.0 self._video_fps: float = 30.0
self._total_frames: int = 0 self._total_frames: int = 0
self._video_duration: float = 0.0 self._video_duration: float = 0.0