- 语音:序数解析(第一个/第二个等)、解析失败计数与 API detail.retry_remaining; 百度 ASR 固定 dev_pid 为普通话;SurgeryPipelineError 支持 extra 并入 HTTP detail。 - Demo:demo 路由与假 RTSP、客户端 index 与 README;BackendResolver 与配置调整。 - 可观测:消耗 TSV 日志、语音文件日志、终端 Markdown 辅助;相关测试与依赖更新。 - 注意:.env 仍被 gitignore,本地密钥不会进入本提交。 Made-with: Cursor
105 lines
3.9 KiB
Python
105 lines
3.9 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
from loguru import logger
|
|
|
|
from app.config import Settings
|
|
from app.services.video.hikvision_runtime import HikvisionRuntime
|
|
from app.services.video.types import VideoBackendKind
|
|
|
|
|
|
class BackendResolver:
|
|
"""Resolve per-camera backend (RTSP vs Hikvision SDK) and RTSP URL."""
|
|
|
|
def __init__(
|
|
self,
|
|
settings: Settings,
|
|
*,
|
|
hikvision_runtime: HikvisionRuntime | None,
|
|
) -> None:
|
|
self._s = settings
|
|
self._hik = hikvision_runtime
|
|
|
|
def _parse_json_object(self, raw: str) -> dict[str, Any]:
|
|
raw = (raw or "").strip()
|
|
if not raw:
|
|
return {}
|
|
try:
|
|
data = json.loads(raw)
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError(f"Invalid JSON mapping: {exc}") from exc
|
|
if not isinstance(data, dict):
|
|
raise ValueError("JSON mapping must be an object")
|
|
return {str(k): v for k, v in data.items()}
|
|
|
|
def backend_for_camera(self, camera_id: str) -> VideoBackendKind:
|
|
overrides = self._parse_json_object(self._s.video_camera_backend_overrides_json)
|
|
if camera_id in overrides:
|
|
v = str(overrides[camera_id]).lower()
|
|
if v in ("rtsp", "hikvision_sdk", "sdk"):
|
|
return (
|
|
VideoBackendKind.HIKVISION_SDK
|
|
if v in ("hikvision_sdk", "sdk")
|
|
else VideoBackendKind.RTSP
|
|
)
|
|
default = self._s.video_default_backend.strip().lower()
|
|
if default in ("auto", ""):
|
|
if self._hik is not None and self._s.hikvision_sdk_enabled:
|
|
return VideoBackendKind.HIKVISION_SDK
|
|
return VideoBackendKind.RTSP
|
|
if default in ("hikvision_sdk", "sdk"):
|
|
return VideoBackendKind.HIKVISION_SDK
|
|
return VideoBackendKind.RTSP
|
|
|
|
def rtsp_url_for_camera(self, camera_id: str) -> str:
|
|
# Re-read on each use so VIDEO_RTSP_URLS_JSON_FILE can be hot-updated (e.g. dev orchestrator).
|
|
m = self._s.video_rtsp_url_map()
|
|
if camera_id in m:
|
|
return m[camera_id]
|
|
tpl = (self._s.video_rtsp_url_template or "").strip()
|
|
if tpl:
|
|
try:
|
|
return tpl.format(camera_id=camera_id)
|
|
except KeyError as exc:
|
|
raise ValueError(
|
|
f"video_rtsp_url_template missing placeholder: {exc}"
|
|
) from exc
|
|
raise ValueError(
|
|
f"No RTSP URL for camera_id={camera_id!r}: set VIDEO_RTSP_URLS_JSON_FILE, "
|
|
f"VIDEO_RTSP_URLS_JSON, or VIDEO_RTSP_URL_TEMPLATE"
|
|
)
|
|
|
|
def rtsp_url_after_hikvision_login(self, camera_id: str) -> str:
|
|
"""RTSP URL used after SDK login (often same as device preview URL)."""
|
|
urls = self._parse_json_object(self._s.hikvision_camera_rtsp_urls_json)
|
|
if camera_id in urls:
|
|
return str(urls[camera_id])
|
|
tpl = (self._s.hikvision_preview_rtsp_template or "").strip()
|
|
if not tpl:
|
|
logger.warning(
|
|
"Hikvision backend without HIKVISION_PREVIEW_RTSP_TEMPLATE / "
|
|
"HIKVISION_CAMERA_RTSP_URLS_JSON — falling back to generic RTSP map"
|
|
)
|
|
return self.rtsp_url_for_camera(camera_id)
|
|
return self._format_hikvision_rtsp(tpl, camera_id)
|
|
|
|
def _format_hikvision_rtsp(self, template: str, camera_id: str) -> str:
|
|
ip = self._s.hikvision_device_ip.strip()
|
|
user = self._s.hikvision_user.strip()
|
|
password = self._s.hikvision_password.strip()
|
|
channel = self._s.hikvision_channel
|
|
try:
|
|
return template.format(
|
|
camera_id=camera_id,
|
|
ip=ip,
|
|
user=user,
|
|
password=password,
|
|
channel=channel,
|
|
)
|
|
except KeyError as exc:
|
|
raise ValueError(
|
|
f"hikvision_preview_rtsp_template missing key: {exc}"
|
|
) from exc
|