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

318 lines
8.9 KiB
TypeScript

/* WebRTC signaling helpers extracted from games.ts
* Exports:
* - audio: map of gameId -> peers
* - join(peers, session, config, safeSend)
* - part(peers, session, safeSend)
* - handleRelayICECandidate(gameId, cfg, session, safeSend, debug)
* - handleRelaySessionDescription(gameId, cfg, session, safeSend, debug)
* - broadcastPeerStateUpdate(gameId, cfg, session, safeSend)
*/
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,
{ hasVideo, hasAudio, has_media }: { hasVideo?: boolean; hasAudio?: boolean; has_media?: boolean },
safeSend?: (targetOrSession: any, message: any) => boolean
): void => {
const send = safeSend ? safeSend : defaultSend;
const ws = session.ws;
if (!session.name) {
console.error(`${session.id}: <- 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.id}: <- join - ${session.name}`);
// Determine media capability - prefer has_media if provided
const peerHasMedia = has_media !== undefined ? has_media : hasVideo || hasAudio;
if (session.name in peers) {
console.log(`${session.id}:${session.name} - Already joined to Audio, updating WebSocket reference.`);
try {
const prev = peers[session.name] && peers[session.name].ws;
if (prev && prev._pingInterval) {
clearInterval(prev._pingInterval);
}
} catch (e) {
/* ignore */
}
peers[session.name].ws = ws;
peers[session.name].has_media = peerHasMedia;
peers[session.name].hasAudio = hasAudio;
peers[session.name].hasVideo = hasVideo;
send(ws, {
type: "join_status",
status: "Joined",
message: "Reconnected",
});
for (const peer in peers) {
if (peer === session.name) continue;
send(ws, {
type: "addPeer",
data: {
peer_id: peer,
peer_name: peer,
has_media: peers[peer].has_media,
should_create_offer: true,
hasAudio: peers[peer].hasAudio,
hasVideo: peers[peer].hasVideo,
},
});
}
for (const peer in peers) {
if (peer === session.name) continue;
send(peers[peer].ws, {
type: "addPeer",
data: {
peer_id: session.name,
peer_name: session.name,
has_media: peerHasMedia,
should_create_offer: false,
hasAudio,
hasVideo,
},
});
}
return;
}
for (let peer in peers) {
send(peers[peer].ws, {
type: "addPeer",
data: {
peer_id: session.name,
peer_name: session.name,
has_media: peers[session.name]?.has_media ?? peerHasMedia,
should_create_offer: false,
hasAudio,
hasVideo,
},
});
send(ws, {
type: "addPeer",
data: {
peer_id: peer,
peer_name: peer,
has_media: peers[peer].has_media,
should_create_offer: true,
hasAudio: peers[peer].hasAudio,
hasVideo: peers[peer].hasVideo,
},
});
}
peers[session.name] = {
ws,
hasAudio,
hasVideo,
has_media: peerHasMedia,
};
send(ws, {
type: "join_status",
status: "Joined",
message: "Successfully joined",
});
};
export const part = (peers: any, session: any, safeSend?: (targetOrSession: any, message: any) => boolean): void => {
const ws = session.ws;
const send = safeSend
? safeSend
: defaultSend;
if (!session.name) {
console.error(`${session.id}: <- part - No name set yet. Audio not available.`);
return;
}
if (!(session.name in peers)) {
console.log(`${session.id}: <- ${session.name} - Does not exist in game audio.`);
return;
}
console.log(`${session.id}: <- ${session.name} - Audio part.`);
console.log(`-> removePeer - ${session.name}`);
delete peers[session.name];
for (let peer in peers) {
send(peers[peer].ws, {
type: "removePeer",
data: {
peer_id: session.name,
peer_name: session.name,
},
});
send(ws, {
type: "removePeer",
data: {
peer_id: peer,
peer_name: peer,
},
});
}
};
export const handleRelayICECandidate = (
gameId: string,
cfg: any,
session: any,
safeSend?: (targetOrSession: any, message: any) => boolean,
debug?: any
) => {
const send = safeSend ? safeSend : 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.name,
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,
safeSend?: (targetOrSession: any, message: any) => boolean,
debug?: any
) => {
const send = safeSend ? safeSend : 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.id}:${gameId} - relaySessionDescription ${session.name} to ${peer_id}`, session_description);
const message = JSON.stringify({
type: "sessionDescription",
data: {
peer_id: session.name,
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, safeSend?: (targetOrSession: any, message: any) => boolean) => {
const send = safeSend
? safeSend
: (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.name,
peer_name: session.name,
muted,
video_on,
},
});
for (const other in audio[gameId]) {
if (other === session.name) continue;
try {
const tgt = audio[gameId][other] as any;
if (!tgt || !tgt.ws) {
console.warn(`${session.id}:${gameId} peer_state_update - target ${other} has no ws`);
} else if (!send(tgt.ws, messagePayload)) {
console.warn(`${session.id}:${gameId} peer_state_update - send failed to ${other}`);
}
} catch (e) {
console.warn(`Failed sending peer_state_update to ${other}:`, e);
}
}
};