121 lines
4.6 KiB
Python
121 lines
4.6 KiB
Python
"""摄像头管理模块:加载设备列表、切换当前设备并解析播放地址。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
import time
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from app.capacity_api import CapacityApiClient
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Device:
|
|
name: str
|
|
device_num: str
|
|
|
|
|
|
class DeviceManager:
|
|
def __init__(self, path: str, api_client: CapacityApiClient, fallback_url: str = ""):
|
|
self.devices = self._load_devices(path)
|
|
self.api_client = api_client
|
|
self.fallback_url = fallback_url
|
|
self.lock = threading.Lock()
|
|
self.current_device_num = self.devices[0].device_num if self.devices else ""
|
|
self.current_url = fallback_url
|
|
self.timings: dict[str, float] = {}
|
|
self.updated_at = 0.0
|
|
self.version = 0
|
|
|
|
def set_current_device(self, device_num: str) -> int:
|
|
if device_num not in {device.device_num for device in self.devices}:
|
|
raise ValueError("设备不在 devicelist.env 中")
|
|
with self.lock:
|
|
old_device_num = self.current_device_num
|
|
self.current_device_num = device_num
|
|
self.current_url = ""
|
|
self.timings = {}
|
|
self.updated_at = time.time()
|
|
self.version += 1
|
|
print(
|
|
f"[device-switch] manager set old={old_device_num} new={device_num} version={self.version}",
|
|
flush=True,
|
|
)
|
|
return self.version
|
|
|
|
def resolve_stream_url(self) -> str:
|
|
with self.lock:
|
|
device_num = self.current_device_num
|
|
version = self.version
|
|
if not device_num:
|
|
if self.fallback_url:
|
|
return self.fallback_url
|
|
raise RuntimeError("devicelist.env 中没有可用设备号")
|
|
|
|
print(f"[device-switch] resolve start device={device_num} version={version}", flush=True)
|
|
try:
|
|
result = self.api_client.get_stream_url_details(device_num)
|
|
except Exception as exc:
|
|
print(
|
|
f"[device-switch] resolve failed device={device_num} version={version} error={exc}",
|
|
flush=True,
|
|
)
|
|
raise
|
|
with self.lock:
|
|
# 避免旧摄像头的慢接口响应覆盖用户刚切换的新选择。
|
|
if version != self.version or device_num != self.current_device_num:
|
|
print(
|
|
f"[device-switch] resolve stale device={device_num} version={version} current={self.current_device_num} current_version={self.version}",
|
|
flush=True,
|
|
)
|
|
return self.current_url
|
|
self.current_url = result.url
|
|
self.timings = dict(result.timings)
|
|
self.updated_at = time.time()
|
|
print(f"[device-switch] resolve success device={device_num} version={version}", flush=True)
|
|
return result.url
|
|
|
|
def resolve_stream_url_for(self, device_num: str) -> str:
|
|
if device_num not in {device.device_num for device in self.devices}:
|
|
raise ValueError("设备不在 devicelist.env 中")
|
|
result = self.api_client.get_stream_url_details(device_num)
|
|
return result.url
|
|
|
|
def get_video_grid_devices(self, limit: int = 4) -> list[Device]:
|
|
return self.devices[:limit]
|
|
|
|
def get_snapshot(self) -> dict[str, Any]:
|
|
with self.lock:
|
|
return {
|
|
"devices": [device.__dict__ for device in self.devices],
|
|
"current_device_num": self.current_device_num,
|
|
"current_url": self.current_url,
|
|
"source_timings": dict(self.timings),
|
|
"source_updated_at": self.updated_at,
|
|
}
|
|
|
|
@staticmethod
|
|
def _load_devices(path: str) -> list[Device]:
|
|
devices: list[Device] = []
|
|
file_path = Path(path)
|
|
if not file_path.exists():
|
|
return devices
|
|
|
|
for line in file_path.read_text(encoding="utf-8").splitlines():
|
|
stripped = line.strip()
|
|
if not stripped or stripped.startswith("#"):
|
|
continue
|
|
if "=" in stripped:
|
|
name, value = stripped.split("=", 1)
|
|
values = [item.strip() for item in value.split(",") if item.strip()]
|
|
display_name = name.strip()
|
|
else:
|
|
values = [stripped]
|
|
display_name = "摄像头"
|
|
for device_num in values:
|
|
name = display_name if len(values) == 1 else f"摄像头 {len(devices) + 1}"
|
|
devices.append(Device(name, device_num))
|
|
return devices
|