"""WebSocket: широковещательные live-обновления статуса камер.""" from __future__ import annotations import asyncio import json import logging from fastapi import APIRouter, WebSocket, WebSocketDisconnect from ..runtime import runtime from ..auth import COOKIE_NAME, auth_manager log = logging.getLogger("nvr.ws") router = APIRouter() class ConnectionManager: def __init__(self) -> None: self._clients: set[WebSocket] = set() self._loop: asyncio.AbstractEventLoop | None = None def bind_loop(self, loop: asyncio.AbstractEventLoop) -> None: self._loop = loop async def connect(self, ws: WebSocket) -> None: await ws.accept() self._clients.add(ws) def disconnect(self, ws: WebSocket) -> None: self._clients.discard(ws) async def broadcast(self, message: dict) -> None: data = json.dumps(message, ensure_ascii=False) dead: list[WebSocket] = [] for ws in list(self._clients): try: await ws.send_text(data) except Exception: # noqa: BLE001 dead.append(ws) for ws in dead: self._clients.discard(ws) def broadcast_threadsafe(self, message: dict) -> None: """Вызов из синхронного callback рекордера.""" if self._loop and self._loop.is_running(): asyncio.run_coroutine_threadsafe(self.broadcast(message), self._loop) @router.websocket("/api/ws") async def ws_endpoint(ws: WebSocket) -> None: if not auth_manager.validate(ws.cookies.get(COOKIE_NAME)): await ws.close(code=1008) # policy violation return await runtime.ws.connect(ws) # начальный снапшот статусов await ws.send_text(json.dumps({ "type": "status", "cameras": [s.to_dict() for s in runtime.supervisor.statuses()], }, ensure_ascii=False)) try: while True: await ws.receive_text() # клиент ничего не шлёт; держим соединение except WebSocketDisconnect: runtime.ws.disconnect(ws)