from __future__ import annotations import time from dataclasses import dataclass from typing import Any import cv2 import numpy as np from loguru import logger @dataclass class RtspCapture: """Thin OpenCV RTSP wrapper (blocking). Use from asyncio via to_thread.""" url: str open_timeout_sec: float def __post_init__(self) -> None: self._cap: cv2.VideoCapture | None = None def open(self) -> None: self._cap = cv2.VideoCapture(self.url, cv2.CAP_FFMPEG) if not self._cap.isOpened(): raise RuntimeError(f"RTSP open failed (isOpened=False): {self.url!r}") # Reduce internal buffering where supported try: self._cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) except Exception: pass deadline = time.monotonic() + self.open_timeout_sec while time.monotonic() < deadline: ok, frame = self._cap.read() if ok and frame is not None: return time.sleep(0.05) raise TimeoutError( f"RTSP first frame timeout after {self.open_timeout_sec}s: {self.url!r}" ) def read(self) -> tuple[bool, np.ndarray | None]: if self._cap is None: return False, None return self._cap.read() def release(self) -> None: if self._cap is not None: try: self._cap.release() except Exception as exc: logger.debug("VideoCapture.release: {}", exc) self._cap = None @property def cap(self) -> Any: return self._cap