318 lines
8.9 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
};
|