65 lines
2.1 KiB
Python
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)
|