1
0
peddlers-of-ketran/server/routes/webrtc-signaling.ts

301 lines
8.1 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";
export const audio: Record<string, any> = {};
// 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: any, session: any): 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}`);
// Use session.id as the canonical peer key
if (session.id in peers) {
console.log(`${session.short}:${session.id} - Already joined to Audio, updating WebSocket reference.`);
try {
const prev = peers[session.id] && peers[session.id].ws;
if (prev && prev._pingInterval) {
clearInterval(prev._pingInterval);
}
} catch (e) {
/* ignore */
}
peers[session.id].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 || peerId,
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: any, session: any): 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);
}
}
};