Files
CoRE.Vision/backend/app/api/ws.py
T
2026-05-30 00:08:59 +05:00

65 lines
2.1 KiB
Python

"""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)