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,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.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)
|
||||||
EngineRegistry.register(CameraEngine)
|
if _has_camera:
|
||||||
|
EngineRegistry.register(CameraEngine)
|
||||||
EngineRegistry.register(DemoCaptureEngine)
|
EngineRegistry.register(DemoCaptureEngine)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -40,8 +47,9 @@ __all__ = [
|
|||||||
"WGCCaptureStream",
|
"WGCCaptureStream",
|
||||||
"ScrcpyEngine",
|
"ScrcpyEngine",
|
||||||
"ScrcpyCaptureStream",
|
"ScrcpyCaptureStream",
|
||||||
"CameraEngine",
|
|
||||||
"CameraCaptureStream",
|
|
||||||
"DemoCaptureEngine",
|
"DemoCaptureEngine",
|
||||||
"DemoCaptureStream",
|
"DemoCaptureStream",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if _has_camera:
|
||||||
|
__all__ += ["CameraEngine", "CameraCaptureStream"]
|
||||||
|
|||||||
@@ -22,7 +22,11 @@ from wled_controller.core.processing.live_stream import (
|
|||||||
ScreenCaptureLiveStream,
|
ScreenCaptureLiveStream,
|
||||||
StaticImageLiveStream,
|
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
|
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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user