"""Bonvoice media — raw frame relay only (no PCM processing on the API).""" import asyncio import json import logging from collections.abc import Awaitable, Callable from typing import Any from uuid import UUID from fastapi import WebSocket, WebSocketDisconnect from .presence import console_presence logger = logging.getLogger(__name__) OnEndCallback = Callable[[], Awaitable[None]] def parse_event(raw: str) -> dict[str, Any] | None: try: payload = json.loads(raw) except json.JSONDecodeError: return None return payload if isinstance(payload, dict) else None def extract_start_metadata(message: dict[str, Any]) -> dict[str, Any]: data = message.get("data") or message.get("start") or {} stream_id = data.get("stream_id") if data.get("stream_id") is not None else message.get("stream_id") return { "stream_id": stream_id, "stream_id_str": str(stream_id) if stream_id is not None else "", "call_id": str(data.get("call_id") or message.get("call_id") or ""), "from": str(data.get("from") or message.get("from") or ""), "to": str(data.get("to") or message.get("to") or ""), } async def run_console_raw_relay( provider: WebSocket, user_id: UUID, console_ws: WebSocket, *, initial_frames: list[str] | None = None, on_end: OnEndCallback | None = None, ) -> None: """Console test bridge — forward raw JSON frames both ways (no server audio processing).""" stop = asyncio.Event() relay_queue = await console_presence.start_relay(user_id) for raw in initial_frames or []: await console_ws.send_text(raw) async def console_to_provider() -> None: try: while not stop.is_set(): raw = await relay_queue.get() await provider.send_text(raw) except (WebSocketDisconnect, RuntimeError): stop.set() async def provider_to_console() -> None: try: while not stop.is_set(): raw = await provider.receive_text() await console_ws.send_text(raw) except (WebSocketDisconnect, RuntimeError): stop.set() try: await asyncio.gather(console_to_provider(), provider_to_console()) finally: stop.set() await console_presence.stop_relay(user_id) if on_end: await on_end()