/* WebRTC signaling helpers extracted from games.ts * Exports: * - audio: map of gameId -> peers * - join(peers, session, config) * - part(peers, session) * - handleRelayICECandidate(gameId, cfg, session, debug) * - handleRelaySessionDescription(gameId, cfg, session, debug) * - broadcastPeerStateUpdate(gameId, cfg, session) */ import { Session } from "./games/types"; interface Peer { ws: any; name: string; } /* Map of session => peer_id => peer */ export const audio: Record> = {}; // Default send helper used when caller doesn't provide a safeSend implementation. const defaultSend = (targetOrSession: any, message: any): boolean => { try { const target = targetOrSession && typeof targetOrSession.send === "function" ? targetOrSession : targetOrSession && targetOrSession.ws ? targetOrSession.ws : null; if (!target) return false; target.send(typeof message === "string" ? message : JSON.stringify(message)); return true; } catch (e) { return false; } }; export const join = (peers: Record, session: Session): void => { const send = defaultSend; const ws = session.ws; if (!session.name) { console.error(`${session.short}: <- join - No name set yet. Audio not available.`); send(ws, { type: "join_status", status: "Error", message: "No name set yet. Audio not available.", }); return; } console.log(`${session.short}: <- join - ${session.name}`); const peer = peers[session.id]; // Use session.id as the canonical peer key if (peer) { console.log(`${session.short}:${session.id} - Already joined to Audio, updating WebSocket reference.`); try { const prev = peer.ws; if (prev && prev._pingInterval) { clearInterval(prev._pingInterval); } } catch (e) { /* ignore */ } peer.ws = ws; send(ws, { type: "join_status", status: "Joined", message: "Reconnected", }); // Tell the reconnecting client about existing peers for (const peerId in peers) { if (peerId === session.id) continue; send(ws, { type: "addPeer", data: { peer_id: peerId, peer_name: peers[peerId]!.name, should_create_offer: true, }, }); } // Tell existing peers about the reconnecting client for (const peerId in peers) { if (peerId === session.id) continue; send(peers[peerId]!.ws, { type: "addPeer", data: { peer_id: session.id, peer_name: session.name, should_create_offer: false, }, }); } return; } for (let peerId in peers) { // notify existing peers about the new client send(peers[peerId]!.ws, { type: "addPeer", data: { peer_id: session.id, peer_name: session.name, should_create_offer: false, }, }); // tell the new client about existing peers send(ws, { type: "addPeer", data: { peer_id: peerId, peer_name: peers[peerId]!.name || peerId, should_create_offer: true, }, }); } // Store peer keyed by session.id and keep the display name peers[session.id] = { ws, name: session.name, }; send(ws, { type: "join_status", status: "Joined", message: "Successfully joined", }); }; export const part = (peers: Record, session: Session): void => { const ws = session.ws; const send = defaultSend; if (!session.name) { console.error(`${session.id}: <- part - No name set yet. Audio not available.`); return; } if (!(session.id in peers)) { console.log(`${session.short}: <- ${session.name} - Does not exist in game audio.`); return; } console.log(`${session.short}: <- ${session.name} - Audio part.`); console.log(`${session.short}: -> removePeer - ${session.name}`); // Remove this peer delete peers[session.id]; for (let peerId in peers) { send(peers[peerId]!.ws, { type: "removePeer", data: { peer_id: session.id, peer_name: session.name, }, }); send(ws, { type: "removePeer", data: { peer_id: peerId, peer_name: peers[peerId]!.name || peerId, }, }); } }; export const handleRelayICECandidate = (gameId: string, cfg: any, session: Session, debug?: any) => { const send = defaultSend; const ws = session && session.ws; if (!cfg) { // Reply with an error to the sender to aid debugging (mirror Python behaviour) send(ws, { type: "error", data: { error: "relayICECandidate missing data" } }); return; } if (!(gameId in audio)) { console.error(`${session.id}:${gameId} <- relayICECandidate - Does not have Audio`); return; } const { peer_id, candidate } = cfg; if (debug && debug.audio) console.log(`${session.id}:${gameId} <- relayICECandidate ${session.name} to ${peer_id}`, candidate); const message = JSON.stringify({ type: "iceCandidate", data: { peer_id: session.id, peer_name: session.name, candidate, }, }); if (peer_id in audio[gameId]!) { const target = audio[gameId]![peer_id] as any; if (!target || !target.ws) { console.warn(`${session.id}:${gameId} relayICECandidate - target ${peer_id} has no ws`); } else if (!send(target.ws, message)) { console.warn(`${session.id}:${gameId} relayICECandidate - send failed to ${peer_id}`); } } }; export const handleRelaySessionDescription = (gameId: string, cfg: any, session: any, debug?: any) => { const send = defaultSend; const ws = session && session.ws; if (!cfg) { send(ws, { type: "error", data: { error: "relaySessionDescription missing data" } }); return; } if (!(gameId in audio)) { console.error(`${gameId} - relaySessionDescription - Does not have Audio`); return; } const { peer_id, session_description } = cfg; if (!peer_id) { send(ws, { type: "error", data: { error: "relaySessionDescription missing peer_id" } }); return; } if (debug && debug.audio) console.log( `${session.short}:${gameId} - relaySessionDescription ${session.name} to ${peer_id}`, session_description ); const message = JSON.stringify({ type: "sessionDescription", data: { peer_id: session.id, peer_name: session.name, session_description, }, }); if (peer_id in audio[gameId]!) { const target = audio[gameId]![peer_id] as any; if (!target || !target.ws) { console.warn(`${session.id}:${gameId} relaySessionDescription - target ${peer_id} has no ws`); } else if (!send(target.ws, message)) { console.warn(`${session.id}:${gameId} relaySessionDescription - send failed to ${peer_id}`); } } }; export const broadcastPeerStateUpdate = (gameId: string, cfg: any, session: any) => { const send = (targetOrSession: any, message: any) => { try { const target = targetOrSession && typeof targetOrSession.send === "function" ? targetOrSession : targetOrSession && targetOrSession.ws ? targetOrSession.ws : null; if (!target) return false; target.send(typeof message === "string" ? message : JSON.stringify(message)); return true; } catch (e) { return false; } }; if (!(gameId in audio)) { console.error(`${session.id}:${gameId} <- peer_state_update - Does not have Audio`); return; } const { muted, video_on } = cfg; if (!session.name) { console.error(`${session.id}: peer_state_update - unnamed session`); return; } const messagePayload = JSON.stringify({ type: "peer_state_update", data: { peer_id: session.id, peer_name: session.name, muted, video_on, }, }); for (const otherId in audio[gameId]) { if (otherId === session.id) continue; try { const tgt = audio[gameId][otherId] as any; if (!tgt || !tgt.ws) { console.warn(`${session.id}:${gameId} peer_state_update - target ${otherId} has no ws`); } else if (!send(tgt.ws, messagePayload)) { console.warn(`${session.id}:${gameId} peer_state_update - send failed to ${otherId}`); } } catch (e) { console.warn(`Failed sending peer_state_update to ${otherId}:`, e); } } };