"""WebSocket broadcast manager for streaming horror to connected clients.""" import json import random from fastapi import WebSocket class StreamManager: """Manages WebSocket clients and broadcasts horror events.""" TRANSITIONS = ["crossfade", "dissolve", "glitch_cut", "melt_morph"] def __init__(self): self._clients: set[WebSocket] = set() @property def client_count(self) -> int: return len(self._clients) def add_client(self, ws: WebSocket) -> None: self._clients.add(ws) def remove_client(self, ws: WebSocket) -> None: self._clients.discard(ws) async def _broadcast(self, message: str) -> None: """Send to all clients, remove dead ones.""" dead: list[WebSocket] = [] for ws in self._clients: try: await ws.send_text(message) except Exception: dead.append(ws) for ws in dead: self._clients.discard(ws) async def broadcast_phase(self, intensity: float, params: dict) -> None: """Push a phase update with current intensity and rendering params.""" msg = json.dumps({ "type": "phase", "intensity": round(intensity, 2), "params": params, }) await self._broadcast(msg) async def broadcast_asset( self, url: str, severity: float, transition: str | None = None, ) -> None: """Push a new image asset reference.""" if transition is None: transition = random.choice(self.TRANSITIONS) msg = json.dumps({ "type": "asset", "url": url, "severity": round(severity, 2), "transition": transition, }) await self._broadcast(msg) async def broadcast_whisper( self, url: str, pan: float, volume: float, reverb: float, ) -> None: """Push a whisper audio clip reference.""" msg = json.dumps({ "type": "whisper", "url": url, "pan": round(pan, 2), "volume": round(volume, 2), "reverb": round(reverb, 2), }) await self._broadcast(msg) async def broadcast_address(self, audio_b64: str, text: str) -> None: """Push a direct address audio clip (base64-encoded WAV).""" msg = json.dumps({ "type": "address", "audio": audio_b64, "text": text, }) await self._broadcast(msg) async def broadcast_scare(self, effect: str, duration_ms: int) -> None: """Push a scare event (face flash, white-out, inversion, etc.).""" msg = json.dumps({ "type": "scare", "effect": effect, "duration_ms": duration_ms, }) await self._broadcast(msg)