- Add broadcast_loop sending position ticks at 10Hz to all clients - Replace echo stub with play/pause/seek/load command dispatch to MPVEngine - Start broadcast_loop as asyncio task in lifespan, cancel on shutdown - Use get_engine callable to avoid circular import at module level
86 lines
2.9 KiB
Python
86 lines
2.9 KiB
Python
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)
|