- Add FastAPI routes for surgery start/end, results, pending confirmation (WAV upload), and health checks. - Implement RTSP/Hikvision capture, consumable classification, session manager, MinIO/Baidu voice resolution, and DB persistence. - Add documentation (client API, video backends, staging checklist) and sample camera/RTSP config. - Add pytest suite (API contract, session manager, voice, repositories, pipeline persistence) and httpx dev dependency. - Replace deprecated HTTP_422_UNPROCESSABLE_ENTITY with HTTP_422_UNPROCESSABLE_CONTENT. - Fix SurgeryPipeline DB reads to use an explicit transaction with autobegin disabled. Made-with: Cursor
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
|