import asyncio import json from fastapi import APIRouter, WebSocket, WebSocketDisconnect router = APIRouter() class ConnectionManager: def __init__(self): self.active_connections: list[WebSocket] = [] async def connect(self, ws: WebSocket) -> None: await ws.accept() self.active_connections.append(ws) def disconnect(self, ws: WebSocket) -> None: if ws in self.active_connections: self.active_connections.remove(ws) async def broadcast(self, message: dict) -> None: dead = [] for connection in self.active_connections: try: await connection.send_text(json.dumps(message)) except Exception: dead.append(connection) for d in dead: if d in self.active_connections: self.active_connections.remove(d) manager = ConnectionManager() async def broadcast_loop(get_engine) -> None: """Broadcast playback state to all WebSocket clients at 10Hz (AUD-04). Args: get_engine: callable returning the MPVEngine instance (avoids import cycle) """ while True: await asyncio.sleep(0.1) engine = get_engine() if engine and manager.active_connections: state = engine.get_state() await manager.broadcast({"type": "tick", **state}) @router.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): await manager.connect(websocket) try: while True: data = await websocket.receive_text() msg = json.loads(data) msg_type = msg.get("type") # Import engine reference from main (module-level var) from lightsync.main import engine if engine is None: await websocket.send_text(json.dumps({"type": "error", "message": "engine not ready"})) continue if msg_type == "play": engine.play() await websocket.send_text(json.dumps({"type": "ack", "command": "play"})) elif msg_type == "pause": engine.pause() await websocket.send_text(json.dumps({"type": "ack", "command": "pause"})) elif msg_type == "seek": position = float(msg.get("position", 0)) engine.seek(position) await websocket.send_text(json.dumps({"type": "ack", "command": "seek", "position": position})) elif msg_type == "load": path = msg.get("path", "") if path: engine.load(path) await websocket.send_text(json.dumps({"type": "ack", "command": "load", "path": path})) else: await websocket.send_text(json.dumps({"type": "error", "message": "missing path"})) else: await websocket.send_text(json.dumps({"type": "ack", "echo": msg})) except WebSocketDisconnect: manager.disconnect(websocket)