91 lines
2.8 KiB
Python
91 lines
2.8 KiB
Python
"""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)
|