308 lines
8.2 KiB
TypeScript
308 lines
8.2 KiB
TypeScript
/* 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<string, Record<string, Peer>> = {};
|
|
|
|
// 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<string, Peer>, 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<string, Peer>, 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);
|
|
}
|
|
}
|
|
};
|