57 lines
1.6 KiB
Python
57 lines
1.6 KiB
Python
|
|
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
|