refactor: make OpenCV an optional dependency
All checks were successful
Lint & Test / test (push) Successful in 25s
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:
@@ -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.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)
|
||||
if _has_camera:
|
||||
EngineRegistry.register(CameraEngine)
|
||||
EngineRegistry.register(DemoCaptureEngine)
|
||||
|
||||
@@ -40,8 +47,9 @@ __all__ = [
|
||||
"WGCCaptureStream",
|
||||
"ScrcpyEngine",
|
||||
"ScrcpyCaptureStream",
|
||||
"CameraEngine",
|
||||
"CameraCaptureStream",
|
||||
"DemoCaptureEngine",
|
||||
"DemoCaptureStream",
|
||||
]
|
||||
|
||||
if _has_camera:
|
||||
__all__ += ["CameraEngine", "CameraCaptureStream"]
|
||||
|
||||
@@ -22,7 +22,11 @@ from wled_controller.core.processing.live_stream import (
|
||||
ScreenCaptureLiveStream,
|
||||
StaticImageLiveStream,
|
||||
)
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user