Restructuring backend to get media to work.
This commit is contained in:
parent
9d2c5f2516
commit
130b0371c5
@ -6,6 +6,7 @@ export type GlobalContextType = {
|
||||
sendJsonMessage?: (message: any) => void;
|
||||
chat?: Array<unknown>;
|
||||
socketUrl?: string;
|
||||
readyState?: any;
|
||||
session?: Session;
|
||||
lastJsonMessage?: any;
|
||||
};
|
||||
|
@ -8,8 +8,9 @@ import VideocamOff from "@mui/icons-material/VideocamOff";
|
||||
import Videocam from "@mui/icons-material/Videocam";
|
||||
import Box from "@mui/material/Box";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import useWebSocket, { ReadyState } from "react-use-websocket";
|
||||
import { Session } from "./GlobalContext";
|
||||
import { ReadyState } from "react-use-websocket";
|
||||
import { Session, GlobalContext } from "./GlobalContext";
|
||||
import { useContext } from "react";
|
||||
import WebRTCStatus from "./WebRTCStatus";
|
||||
import Moveable from "react-moveable";
|
||||
import { flushSync } from "react-dom";
|
||||
@ -308,7 +309,6 @@ const Video: React.FC<VideoProps> = ({ srcObject, local, ...props }) => {
|
||||
/* ---------- MediaAgent (signaling + peer setup) ---------- */
|
||||
|
||||
type MediaAgentProps = {
|
||||
socketUrl: string;
|
||||
session: Session;
|
||||
peers: Record<string, Peer>;
|
||||
setPeers: React.Dispatch<React.SetStateAction<Record<string, Peer>>>;
|
||||
@ -317,7 +317,7 @@ type MediaAgentProps = {
|
||||
type JoinStatus = { status: "Not joined" | "Joining" | "Joined" | "Error"; message?: string };
|
||||
|
||||
const MediaAgent = (props: MediaAgentProps) => {
|
||||
const { peers, setPeers, socketUrl, session } = props;
|
||||
const { peers, setPeers, session } = props;
|
||||
const [joinStatus, setJoinStatus] = useState<JoinStatus>({ status: "Not joined" });
|
||||
const [media, setMedia] = useState<MediaStream | null>(null);
|
||||
const [pendingPeers, setPendingPeers] = useState<AddPeerConfig[]>([]);
|
||||
@ -354,37 +354,8 @@ const MediaAgent = (props: MediaAgentProps) => {
|
||||
[setPeers]
|
||||
);
|
||||
|
||||
const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket(socketUrl, {
|
||||
share: true,
|
||||
shouldReconnect: (closeEvent) => true, // Auto-reconnect on connection loss
|
||||
reconnectInterval: 5000,
|
||||
onError: (err) => {
|
||||
console.error(err);
|
||||
},
|
||||
onClose: (_event: CloseEvent) => {
|
||||
if (!session) return;
|
||||
|
||||
console.log(`media-agent - ${session.name} Disconnected from signaling server`);
|
||||
|
||||
// Clean up all peer connections
|
||||
connectionsRef.current.forEach((connection, peerId) => {
|
||||
connection.close();
|
||||
});
|
||||
connectionsRef.current.clear();
|
||||
|
||||
// Mark all peers as dead
|
||||
const updatedPeers = { ...peers };
|
||||
Object.keys(updatedPeers).forEach((id) => {
|
||||
if (!updatedPeers[id].local) {
|
||||
updatedPeers[id].dead = true;
|
||||
updatedPeers[id].connection = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
if (debug) console.log(`media-agent - close`, updatedPeers);
|
||||
setPeers(updatedPeers);
|
||||
},
|
||||
});
|
||||
// Use the global websocket provided by RoomView to avoid duplicate sockets
|
||||
const { sendJsonMessage, lastJsonMessage, readyState } = useContext(GlobalContext);
|
||||
|
||||
useEffect(() => {
|
||||
for (let peer in peers) {
|
||||
@ -1120,14 +1091,17 @@ const MediaAgent = (props: MediaAgentProps) => {
|
||||
|
||||
// Join lobby when media is ready
|
||||
useEffect(() => {
|
||||
if (media && joinStatus.status === "Not joined" && readyState === ReadyState.OPEN) {
|
||||
// Only attempt to join once we have local media, an open socket, and a known session name.
|
||||
// Joining with a null/empty name can cause the signaling server to treat the peer as anonymous
|
||||
// which results in other peers not receiving expected addPeer/track messages.
|
||||
if (media && joinStatus.status === "Not joined" && readyState === ReadyState.OPEN && session && session.name) {
|
||||
console.log(`media-agent - Initiating media join for ${session.name}`);
|
||||
setJoinStatus({ status: "Joining" });
|
||||
sendJsonMessage({
|
||||
type: "join",
|
||||
data: {
|
||||
has_media: session.has_media !== false, // Default to true for backward compatibility
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [media, joinStatus.status, sendJsonMessage, readyState, session.name, session.has_media]);
|
||||
|
@ -5,8 +5,26 @@ import { styles } from "./Styles";
|
||||
|
||||
type PlayerColorProps = { color?: string };
|
||||
|
||||
const mapColor = (c?: string) => {
|
||||
if (!c) return undefined;
|
||||
const key = c.toLowerCase();
|
||||
switch (key) {
|
||||
case "red":
|
||||
return "R";
|
||||
case "orange":
|
||||
return "O";
|
||||
case "white":
|
||||
return "W";
|
||||
case "blue":
|
||||
return "B";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const PlayerColor: React.FC<PlayerColorProps> = ({ color }) => {
|
||||
return <Avatar sx={color ? styles[color] : {}} className="PlayerColor" />;
|
||||
const k = mapColor(color) as keyof typeof styles | undefined;
|
||||
return <Avatar sx={k ? styles[k] : {}} className="PlayerColor" />;
|
||||
};
|
||||
|
||||
export { PlayerColor };
|
||||
|
@ -3,6 +3,7 @@ import Paper from "@mui/material/Paper";
|
||||
import List from "@mui/material/List";
|
||||
import "./PlayerList.css";
|
||||
import { MediaControl, MediaAgent, Peer } from "./MediaControl";
|
||||
import { PlayerColor } from "./PlayerColor";
|
||||
import Box from "@mui/material/Box";
|
||||
import { GlobalContext } from "./GlobalContext";
|
||||
|
||||
@ -13,6 +14,7 @@ type Player = {
|
||||
local: boolean /* Client side variable */;
|
||||
protected?: boolean;
|
||||
has_media?: boolean; // Whether this Player provides audio/video streams
|
||||
color?: string;
|
||||
bot_run_id?: string;
|
||||
bot_provider_id?: string;
|
||||
bot_instance_id?: string; // For bot instances
|
||||
@ -24,6 +26,27 @@ const PlayerList: React.FC = () => {
|
||||
const { session, socketUrl, lastJsonMessage, sendJsonMessage } = useContext(GlobalContext);
|
||||
const [players, setPlayers] = useState<Player[] | null>(null);
|
||||
const [peers, setPeers] = useState<Record<string, Peer>>({});
|
||||
useEffect(() => {
|
||||
console.log("PlayerList - Mounted - requesting fields");
|
||||
if (sendJsonMessage) {
|
||||
sendJsonMessage({
|
||||
type: "get",
|
||||
fields: ["participants"],
|
||||
});
|
||||
}
|
||||
}, [sendJsonMessage]);
|
||||
|
||||
// Debug logging
|
||||
useEffect(() => {
|
||||
console.log("PlayerList - Debug state:", {
|
||||
session_id: session?.id,
|
||||
session_name: session?.name,
|
||||
players_count: players?.length,
|
||||
players: players,
|
||||
peers_keys: Object.keys(peers),
|
||||
peers: peers,
|
||||
});
|
||||
}, [players, peers, session]);
|
||||
|
||||
const sortPlayers = useCallback(
|
||||
(A: any, B: any) => {
|
||||
@ -124,6 +147,18 @@ const PlayerList: React.FC = () => {
|
||||
});
|
||||
}, [players, sendJsonMessage]);
|
||||
|
||||
// Debug logging
|
||||
useEffect(() => {
|
||||
console.log("PlayerList - Debug state:", {
|
||||
session_id: session?.id,
|
||||
session_name: session?.name,
|
||||
players_count: players?.length,
|
||||
players: players,
|
||||
peers_keys: Object.keys(peers),
|
||||
peers: peers,
|
||||
});
|
||||
}, [players, peers, session]);
|
||||
|
||||
return (
|
||||
<Box sx={{ position: "relative", width: "100%" }}>
|
||||
<Paper
|
||||
@ -134,7 +169,7 @@ const PlayerList: React.FC = () => {
|
||||
m: { xs: 0, sm: 2 },
|
||||
}}
|
||||
>
|
||||
<MediaAgent {...{ session, socketUrl, peers, setPeers }} />
|
||||
<MediaAgent {...{ session, peers, setPeers }} />
|
||||
<List className="PlayerSelector">
|
||||
{players?.map((player) => (
|
||||
<Box
|
||||
@ -164,6 +199,7 @@ const PlayerList: React.FC = () => {
|
||||
{player.name && !player.live && <div className="NoNetwork"></div>}
|
||||
</Box>
|
||||
{player.name && player.live && peers[player.session_id] && (player.local || player.has_media !== false) ? (
|
||||
<>
|
||||
<MediaControl
|
||||
className="Medium"
|
||||
key={player.session_id}
|
||||
@ -173,6 +209,43 @@ const PlayerList: React.FC = () => {
|
||||
remoteAudioMuted={peers[player.session_id].muted}
|
||||
remoteVideoOff={peers[player.session_id].video_on === false}
|
||||
/>
|
||||
|
||||
{/* If this is the local player and they haven't picked a color, show a picker */}
|
||||
{player.local && !player.color && (
|
||||
<div style={{ marginTop: 8, width: "100%" }}>
|
||||
<div style={{ marginBottom: 6, fontSize: "0.9em" }}>Pick your color:</div>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
{[
|
||||
{ label: "Orange", value: "orange" },
|
||||
{ label: "Red", value: "red" },
|
||||
{ label: "White", value: "white" },
|
||||
{ label: "Blue", value: "blue" },
|
||||
].map((c) => (
|
||||
<button
|
||||
key={c.value}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
padding: "6px 8px",
|
||||
borderRadius: 6,
|
||||
border: "1px solid #ccc",
|
||||
background: "#fff",
|
||||
cursor: sendJsonMessage ? "pointer" : "not-allowed",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!sendJsonMessage) return;
|
||||
sendJsonMessage({ type: "set", field: "color", value: c.value });
|
||||
}}
|
||||
>
|
||||
<PlayerColor color={c.value} />
|
||||
<div style={{ fontSize: "0.9em" }}>{c.label}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : player.name && player.live && player.has_media === false ? (
|
||||
<div
|
||||
className="Video fade-in"
|
||||
|
@ -168,6 +168,14 @@ const RoomView = (props: RoomProps) => {
|
||||
const priv = data.update.private;
|
||||
if (priv.name !== name) {
|
||||
setName(priv.name);
|
||||
// Mirror the name into the shared session so consumers that read
|
||||
// `session.name` (eg. MediaAgent) will see the name and can act
|
||||
// (for example, initiate the media join).
|
||||
try {
|
||||
setSession((s) => (s ? { ...s, name: priv.name } : s));
|
||||
} catch (e) {
|
||||
console.warn("Failed to set session name from private payload", e);
|
||||
}
|
||||
}
|
||||
if (priv.color !== color) {
|
||||
setColor(priv.color);
|
||||
@ -178,6 +186,13 @@ const RoomView = (props: RoomProps) => {
|
||||
if ("name" in data.update) {
|
||||
if (data.update.name) {
|
||||
setName(data.update.name);
|
||||
// Also update the session object so components using session.name
|
||||
// immediately observe the change.
|
||||
try {
|
||||
setSession((s) => (s ? { ...s, name: data.update.name } : s));
|
||||
} catch (e) {
|
||||
console.warn("Failed to set session name from name payload", e);
|
||||
}
|
||||
} else {
|
||||
setWarning("");
|
||||
setError("");
|
||||
@ -284,7 +299,7 @@ const RoomView = (props: RoomProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<GlobalContext.Provider value={{ socketUrl, session, name, roomName, sendJsonMessage, lastJsonMessage }}>
|
||||
<GlobalContext.Provider value={{ socketUrl, session, name, roomName, sendJsonMessage, lastJsonMessage, readyState }}>
|
||||
<div className="RoomView">
|
||||
{!name ? (
|
||||
<Paper>
|
||||
|
131
client/src/plugins/vite-console-forward-plugin.js
Normal file
131
client/src/plugins/vite-console-forward-plugin.js
Normal file
@ -0,0 +1,131 @@
|
||||
// Lightweight Vite plugin that injects a small client-side script to
|
||||
// forward selected console messages to the dev server. The dev server
|
||||
// registers an endpoint at /__console_forward and will log the messages
|
||||
// so you can see browser console output from the container logs.
|
||||
|
||||
export function consoleForwardPlugin(opts = {}) {
|
||||
const levels = Array.isArray(opts.levels) && opts.levels.length ? opts.levels : ["log", "warn", "error"];
|
||||
const enabled = opts.enabled !== false; // Default to true
|
||||
|
||||
if (!enabled) {
|
||||
// No-op plugin
|
||||
return {
|
||||
name: "vite-console-forward-plugin",
|
||||
enforce: "pre",
|
||||
transformIndexHtml(html) {
|
||||
return html;
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: "vite-console-forward-plugin",
|
||||
enforce: "pre",
|
||||
|
||||
configureServer(server) {
|
||||
// Register a simple POST handler to receive the forwarded console
|
||||
// messages from the browser. We keep it minimal and robust.
|
||||
server.middlewares.use("/__console_forward", (req, res, next) => {
|
||||
if (req.method !== "POST") return next();
|
||||
|
||||
let body = "";
|
||||
req.on("data", (chunk) => (body += chunk));
|
||||
req.on("end", () => {
|
||||
try {
|
||||
const payload = JSON.parse(body || "{}");
|
||||
const lvl = payload.level || "log";
|
||||
const args = Array.isArray(payload.args) ? payload.args : payload.args ? [payload.args] : [];
|
||||
const stack = payload.stack;
|
||||
|
||||
// Print an informative prefix so these lines are easy to grep in container logs
|
||||
if (stack) {
|
||||
console.error("[frontend][error]", stack);
|
||||
}
|
||||
|
||||
// Use the requested console level where available; fall back to console.log
|
||||
const fn = console[lvl] || console.log;
|
||||
try {
|
||||
fn("[frontend]", ...args);
|
||||
} catch (e) {
|
||||
// Ensure we don't crash the dev server due to malformed payloads
|
||||
console.log("[frontend][fallback]", ...args);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("console-forward: failed to parse payload", e);
|
||||
}
|
||||
res.statusCode = 204;
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
transformIndexHtml(html) {
|
||||
// Only inject the forwarding script in non-production mode. Vite sets
|
||||
// NODE_ENV during the dev server; keep this conservative.
|
||||
if (process.env.NODE_ENV === "production") return html;
|
||||
|
||||
const script = `(function(){
|
||||
if (window.__vite_console_forward_installed__) return;
|
||||
window.__vite_console_forward_installed__ = true;
|
||||
|
||||
function safeSerialize(v){
|
||||
try { return typeof v === 'string' ? v : JSON.stringify(v); }
|
||||
catch(e){ try{ return String(v); }catch(_){ return 'unserializable'; } }
|
||||
}
|
||||
|
||||
var levels = ${JSON.stringify(levels)};
|
||||
levels.forEach(function(l){
|
||||
var orig = console[l];
|
||||
if (!orig) return;
|
||||
|
||||
console[l] = function(){
|
||||
// Call original first
|
||||
try{ orig.apply(console, arguments); }catch(e){/* ignore */}
|
||||
|
||||
// Capture the real caller from the stack
|
||||
try{
|
||||
var stack = new Error().stack;
|
||||
var callerLine = null;
|
||||
|
||||
// Parse stack to find the first line that's NOT this wrapper
|
||||
if (stack) {
|
||||
var lines = stack.split('\\n');
|
||||
// Skip the first 2-3 lines (Error, this wrapper)
|
||||
for (var i = 2; i < lines.length; i++) {
|
||||
if (lines[i] && !lines[i].includes('vite-console-forward')) {
|
||||
callerLine = lines[i].trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var args = Array.prototype.slice.call(arguments).map(safeSerialize);
|
||||
var payload = JSON.stringify({
|
||||
level: l,
|
||||
args: args,
|
||||
caller: callerLine // Include the real caller
|
||||
});
|
||||
|
||||
if (navigator.sendBeacon) {
|
||||
try { navigator.sendBeacon('/__console_forward', payload); return; } catch(e){}
|
||||
}
|
||||
fetch('/__console_forward', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload }).catch(function(){/* ignore */});
|
||||
}catch(e){/* ignore */}
|
||||
};
|
||||
});
|
||||
|
||||
window.addEventListener('error', function(ev){
|
||||
try{
|
||||
var stack = ev && ev.error && ev.error.stack ? ev.error.stack : (ev.message + ' at ' + ev.filename + ':' + ev.lineno + ':' + ev.colno);
|
||||
var payload = JSON.stringify({ level: 'error', stack: stack });
|
||||
if (navigator.sendBeacon) { try { navigator.sendBeacon('/__console_forward', payload); return; } catch(e){} }
|
||||
fetch('/__console_forward', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload }).catch(function(){});
|
||||
}catch(e){}
|
||||
}, true);
|
||||
})();`;
|
||||
|
||||
return html.replace(/<\/head>/i, `<script>${script}</script></head>`);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default consoleForwardPlugin;
|
@ -1,20 +1,34 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires, no-undef, @typescript-eslint/no-explicit-any */
|
||||
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||
const http = require('http');
|
||||
|
||||
module.exports = function(app) {
|
||||
const base = process.env.PUBLIC_URL;
|
||||
const base = process.env.PUBLIC_URL || '';
|
||||
console.log(`http-proxy-middleware ${base}`);
|
||||
|
||||
// Keep-alive agent for websocket target to reduce connection churn
|
||||
const keepAliveAgent = new http.Agent({ keepAlive: true });
|
||||
|
||||
app.use(createProxyMiddleware(
|
||||
`${base}/api/v1/games/ws`, {
|
||||
ws: true,
|
||||
target: 'ws://pok-server:8930',
|
||||
changeOrigin: true,
|
||||
// use a persistent agent so the proxy reuses sockets for upstream
|
||||
agent: keepAliveAgent,
|
||||
// disable proxy timeouts in dev so intermediate proxies don't drop idle WS
|
||||
proxyTimeout: 0,
|
||||
timeout: 0,
|
||||
pathRewrite: { [`^${base}`]: '' },
|
||||
}));
|
||||
|
||||
app.use(createProxyMiddleware(
|
||||
`${base}/api`, {
|
||||
target: 'http://pok-server:8930',
|
||||
changeOrigin: true,
|
||||
// give HTTP API calls a longer timeout in dev
|
||||
proxyTimeout: 120000,
|
||||
timeout: 120000,
|
||||
pathRewrite: { [`^${base}`]: '' },
|
||||
}));
|
||||
};
|
||||
|
@ -5,6 +5,10 @@ import fs from 'fs';
|
||||
const httpsEnv = (process.env.HTTPS || '').toLowerCase();
|
||||
const useHttps = httpsEnv === 'true' || httpsEnv === '1';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
// Forward browser console messages to the dev server logs so container logs
|
||||
// show frontend console output. This is enabled only when running the dev
|
||||
// server (not for production builds).
|
||||
import { consoleForwardPlugin } from './src/plugins/vite-console-forward-plugin.js'
|
||||
|
||||
|
||||
// If custom cert paths are provided via env, use them; otherwise let Vite handle a self-signed cert when true.
|
||||
@ -40,14 +44,19 @@ export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
tsconfigPaths(),
|
||||
// Only enable the console forwarding plugin while running the dev
|
||||
// server (NODE_ENV typically not 'production'). It is safe to include
|
||||
// here because the plugin's transformIndexHtml is a no-op in
|
||||
// production by checking NODE_ENV.
|
||||
consoleForwardPlugin({ enabled: false, levels: ["log", "warn", "error"] }),
|
||||
// Dev-only plugin: when the dev server receives requests that are
|
||||
// already prefixed with the base (e.g. /ketr.ketran/assets/...), strip
|
||||
// the prefix so Vite can serve the underlying files from /assets/...
|
||||
{
|
||||
name: 'strip-basepath-for-dev',
|
||||
name: "strip-basepath-for-dev",
|
||||
configureServer(server) {
|
||||
// Only install the middleware when a non-root base is configured
|
||||
if (!normalizedBase || normalizedBase === '/') return;
|
||||
if (!normalizedBase || normalizedBase === "/") return;
|
||||
server.middlewares.use((req, res, next) => {
|
||||
try {
|
||||
// Log incoming base-prefixed requests for debugging only. Do NOT
|
||||
@ -67,15 +76,15 @@ export default defineConfig({
|
||||
// asset paths here to avoid interfering with module paths
|
||||
// and HMR endpoints which Vite already serves correctly
|
||||
// when the server `base` is configured.
|
||||
const assetsPrefix = normalizedBase.replace(/\/$/, '') + '/assets/';
|
||||
const assetsPrefix = normalizedBase.replace(/\/$/, "") + "/assets/";
|
||||
if (req.url.indexOf(assetsPrefix) === 0) {
|
||||
const original = req.url;
|
||||
// Preserve the base and change '/assets/' to '/gfx/' so the
|
||||
// dev server serves files from public/gfx which are exposed at
|
||||
// '/<base>/gfx/...'. Example: '/ketr.ketran/assets/gfx/x' ->
|
||||
// '/ketr.ketran/gfx/x'.
|
||||
const baseNoTrail = normalizedBase.replace(/\/$/, '');
|
||||
req.url = req.url.replace(new RegExp('^' + baseNoTrail + '/assets/'), baseNoTrail + '/gfx/');
|
||||
const baseNoTrail = normalizedBase.replace(/\/$/, "");
|
||||
req.url = req.url.replace(new RegExp("^" + baseNoTrail + "/assets/"), baseNoTrail + "/gfx/");
|
||||
console.log(`[vite] rewritten asset ${original} -> ${req.url}`);
|
||||
}
|
||||
}
|
||||
@ -85,41 +94,41 @@ export default defineConfig({
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
build: {
|
||||
outDir: 'build',
|
||||
outDir: "build",
|
||||
},
|
||||
server: {
|
||||
host: process.env.HOST || '0.0.0.0',
|
||||
host: process.env.HOST || "0.0.0.0",
|
||||
port: Number(process.env.PORT) || 3001,
|
||||
https: httpsOption,
|
||||
proxy: {
|
||||
// Support requests that already include the basePath (/ketr.ketran/api)
|
||||
// and requests that use the shorter /api path. Both should be forwarded
|
||||
// to the backend server which serves the API under /ketr.ketran/api.
|
||||
'/ketr.ketran/api': {
|
||||
target: 'http://pok-server:8930',
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
secure: false
|
||||
},
|
||||
'/api': {
|
||||
target: 'http://pok-server:8930',
|
||||
"/ketr.ketran/api": {
|
||||
target: "http://pok-server:8930",
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
secure: false,
|
||||
rewrite: (path) => `/ketr.ketran${path}`
|
||||
}
|
||||
},
|
||||
"/api": {
|
||||
target: "http://pok-server:8930",
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
secure: false,
|
||||
rewrite: (path) => `/ketr.ketran${path}`,
|
||||
},
|
||||
},
|
||||
// HMR options: advertise the external hostname and port so browsers
|
||||
// accessing via `battle-linux.ketrenos.com` can connect to the websocket.
|
||||
// The certs mounted into the container must be trusted by the browser.
|
||||
hmr: {
|
||||
host: process.env.VITE_HMR_HOST || 'battle-linux.ketrenos.com',
|
||||
host: process.env.VITE_HMR_HOST || "battle-linux.ketrenos.com",
|
||||
port: Number(process.env.VITE_HMR_PORT) || 3001,
|
||||
protocol: process.env.VITE_HMR_PROTOCOL || 'wss'
|
||||
protocol: process.env.VITE_HMR_PROTOCOL || "wss",
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
|
@ -1,28 +1,73 @@
|
||||
/* monkey-patch console.log to prefix with file/line-number */
|
||||
if (process.env['LOG_LINE']) {
|
||||
let cwd = process.cwd(),
|
||||
cwdRe = new RegExp("^[^/]*" + cwd.replace("/", "\\/") + "\/([^:]*:[0-9]*).*$");
|
||||
[ "log", "warn", "error" ].forEach(function(method: string) {
|
||||
(console as any)[method] = (function () {
|
||||
let orig = (console as any)[method];
|
||||
return function (this: any, ...args: any[]) {
|
||||
function getErrorObject(): Error {
|
||||
/* monkey-patch console methods to prefix messages with file:line for easier logs */
|
||||
(() => {
|
||||
const cwd = process.cwd();
|
||||
const cwdRe = new RegExp("^[^/]*" + cwd.replace("/", "\\/") + "/([^:]*:[0-9]*).*$");
|
||||
const methods = ["log", "warn", "error", "info", "debug"] as const;
|
||||
|
||||
function getCallerFileLine(): string {
|
||||
try {
|
||||
throw Error('');
|
||||
} catch (err) {
|
||||
return err as Error;
|
||||
// Create an Error to capture stack
|
||||
const err = new Error();
|
||||
if (!err.stack) return "unknown:0 -";
|
||||
const lines = err.stack.split("\n").slice(1);
|
||||
// Find the first stack line that is not this file
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (!line) continue;
|
||||
if (line.indexOf("console-line") !== -1) continue; // skip this helper
|
||||
// Try to extract file:line from the line. Use a stricter capture so we
|
||||
// don't accidentally include leading whitespace or the 'at' token.
|
||||
const m = line.match(/\(?(\S+:\d+:\d+)\)?$/);
|
||||
if (m && m[1]) {
|
||||
return m[1].trim() + " -";
|
||||
}
|
||||
}
|
||||
// Fallback: try to extract file:line:col from the third stack line even
|
||||
// if it contains leading whitespace and the 'at' prefix. If that fails,
|
||||
// fall back to the cwd-based replace and trim whitespace.
|
||||
const fallback = err.stack.split("\n")[3] || "";
|
||||
const m2 = fallback.match(/\(?(\S+:\d+:\d+)\)?$/);
|
||||
if (m2 && m2[1]) return m2[1].trim() + " -";
|
||||
const replaced = fallback.replace(cwdRe, "$1 -").trim();
|
||||
return replaced || "unknown:0 -";
|
||||
} catch (e) {
|
||||
return "unknown:0 -";
|
||||
}
|
||||
}
|
||||
|
||||
let err = getErrorObject(),
|
||||
caller_line = err.stack?.split("\n")[3] || '',
|
||||
prefixedArgs = [caller_line.replace(cwdRe, "$1 -")];
|
||||
methods.forEach((method) => {
|
||||
const orig = (console as any)[method] || console.log;
|
||||
(console as any)[method] = function (...args: any[]) {
|
||||
try {
|
||||
const prefix = getCallerFileLine();
|
||||
|
||||
/* arguments.unshift() doesn't exist... */
|
||||
prefixedArgs.push(...args);
|
||||
// Separate Error objects from other args so we can print their stacks
|
||||
// line-by-line with the same prefix. This keeps stack traces intact
|
||||
// while ensuring every printed line shows the caller prefix.
|
||||
const errorArgs = args.filter((a: any) => a instanceof Error) as Error[];
|
||||
const otherArgs = args.filter((a: any) => !(a instanceof Error));
|
||||
|
||||
orig.apply(this, prefixedArgs);
|
||||
};
|
||||
})();
|
||||
// Print non-error args in a single call (preserving original formatting)
|
||||
const processedOther = otherArgs.map((a: any) => (a instanceof Error ? a.stack || a.toString() : a));
|
||||
if (processedOther.length > 0) {
|
||||
orig.apply(this, [prefix, ...processedOther]);
|
||||
}
|
||||
|
||||
// For each Error, print each line of its stack as a separate prefixed log
|
||||
// entry so lines that begin with ' at' are not orphaned.
|
||||
errorArgs.forEach((err) => {
|
||||
const stack = err.stack || err.toString();
|
||||
stack.split("\n").forEach((line) => {
|
||||
orig.apply(this, [prefix, line]);
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
try {
|
||||
orig.apply(this, args);
|
||||
} catch (e2) {
|
||||
/* swallow */
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
})();
|
||||
|
@ -19,6 +19,14 @@ import { getValidRoads, getValidCorners, isRuleEnabled } from "../util/validLoca
|
||||
import { Player, Game, Session, CornerPlacement, RoadPlacement, Offer, Turn } from "./games/types";
|
||||
import { newPlayer } from "./games/playerFactory";
|
||||
import { normalizeIncoming, shuffleArray } from "./games/utils";
|
||||
import {
|
||||
audio as audioMap,
|
||||
join as webrtcJoin,
|
||||
part as webrtcPart,
|
||||
handleRelayICECandidate,
|
||||
handleRelaySessionDescription,
|
||||
broadcastPeerStateUpdate,
|
||||
} from "./webrtc-signaling";
|
||||
// import type { GameState } from './games/state'; // unused import removed during typing pass
|
||||
|
||||
const router = express.Router();
|
||||
@ -51,7 +59,9 @@ initGameDB()
|
||||
// shuffleArray imported from './games/utils.ts'
|
||||
|
||||
const games: Record<string, Game> = {};
|
||||
const audio: Record<string, any> = {};
|
||||
|
||||
// Re-exported audio map from webrtc-signaling for in-file use
|
||||
const audio = audioMap;
|
||||
|
||||
const processTies = (players: Player[]): boolean => {
|
||||
/* Sort the players into buckets based on their
|
||||
@ -1116,12 +1126,12 @@ const setPlayerName = (game: Game, session: Session, name: string): string | und
|
||||
}
|
||||
session.name = name;
|
||||
if (session.ws && game.id in audio && session.name in audio[game.id]) {
|
||||
part(audio[game.id], session);
|
||||
webrtcPart(audio[game.id], session);
|
||||
}
|
||||
} else {
|
||||
message = `${session.name} has changed their name to ${name}.`;
|
||||
if (session.ws && game.id in audio) {
|
||||
part(audio[game.id], session);
|
||||
webrtcPart(audio[game.id], session);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1138,7 +1148,7 @@ const setPlayerName = (game: Game, session: Session, name: string): string | und
|
||||
}
|
||||
|
||||
if (session.ws && session.hasAudio) {
|
||||
join(audio[game.id], session, {
|
||||
webrtcJoin(audio[game.id], session, {
|
||||
hasVideo: session.video ? true : false,
|
||||
hasAudio: session.audio ? true : false,
|
||||
});
|
||||
@ -3412,171 +3422,8 @@ const resetDisconnectCheck = (_game: any, req: any): void => {
|
||||
//req.disconnectCheck = setTimeout(() => { wsInactive(game, req) }, 20000);
|
||||
};
|
||||
|
||||
const join = (peers: any, session: any, { hasVideo, hasAudio, has_media }: { hasVideo?: boolean; hasAudio?: boolean; has_media?: boolean }): void => {
|
||||
const ws = session.ws;
|
||||
|
||||
if (!session.name) {
|
||||
console.error(`${session.id}: <- join - No name set yet. Audio not available.`);
|
||||
ws.send(JSON.stringify({
|
||||
type: "join_status",
|
||||
status: "Error",
|
||||
message: "No name set yet. Audio not available."
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${session.id}: <- join - ${session.name}`);
|
||||
console.log(`${all}: -> addPeer - ${session.name}`);
|
||||
|
||||
// Determine media capability - prefer has_media if provided, otherwise derive from hasVideo/hasAudio
|
||||
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.`);
|
||||
|
||||
// Update the WebSocket reference in case of reconnection
|
||||
peers[session.name].ws = ws;
|
||||
peers[session.name].has_media = peerHasMedia;
|
||||
peers[session.name].hasAudio = hasAudio;
|
||||
peers[session.name].hasVideo = hasVideo;
|
||||
|
||||
// Send join status to reconnected client
|
||||
ws.send(JSON.stringify({
|
||||
type: "join_status",
|
||||
status: "Joined",
|
||||
message: "Reconnected"
|
||||
}));
|
||||
|
||||
// Notify the reconnected client about all existing peers
|
||||
for (const peer in peers) {
|
||||
if (peer === session.name) continue; // Skip self
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
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,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Notify all other peers about the reconnected peer (with updated connection)
|
||||
for (const peer in peers) {
|
||||
if (peer === session.name) continue; // Skip self
|
||||
|
||||
peers[peer].ws.send(
|
||||
JSON.stringify({
|
||||
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) {
|
||||
/* Add this caller to all peers */
|
||||
peers[peer].ws.send(
|
||||
JSON.stringify({
|
||||
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,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
/* Add each other peer to the caller */
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
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,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/* Add this user as a peer connected to this WebSocket */
|
||||
peers[session.name] = {
|
||||
ws,
|
||||
hasAudio,
|
||||
hasVideo,
|
||||
has_media: peerHasMedia,
|
||||
};
|
||||
|
||||
/* Send join success status */
|
||||
ws.send(JSON.stringify({
|
||||
type: "join_status",
|
||||
status: "Joined",
|
||||
message: "Successfully joined"
|
||||
}));
|
||||
};
|
||||
|
||||
const part = (peers: any, session: any): void => {
|
||||
const ws = session.ws;
|
||||
|
||||
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(`${all}: -> removePeer - ${session.name}`);
|
||||
|
||||
delete peers[session.name];
|
||||
|
||||
/* Remove this peer from all other peers, and remove each
|
||||
* peer from this peer */
|
||||
for (let peer in peers) {
|
||||
peers[peer].ws.send(
|
||||
JSON.stringify({
|
||||
type: "removePeer",
|
||||
data: {
|
||||
peer_id: session.name,
|
||||
peer_name: session.name
|
||||
},
|
||||
})
|
||||
);
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "removePeer",
|
||||
data: {
|
||||
peer_id: peer,
|
||||
peer_name: peer
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
// WebRTC join/part handling moved to server/routes/webrtc-signaling.ts
|
||||
// use webrtcJoin(audio[gameId], session, config) and webrtcPart(audio[gameId], session)
|
||||
|
||||
const getName = (session: any): string => {
|
||||
return session ? (session.name ? session.name : session.id) : "Admin";
|
||||
@ -4262,7 +4109,7 @@ router.ws("/ws/:id", async (ws, req) => {
|
||||
/* Cleanup any voice channels */
|
||||
if (gameId in audio) {
|
||||
try {
|
||||
part(audio[gameId], session);
|
||||
webrtcPart(audio[gameId], session);
|
||||
} catch (e) {
|
||||
console.warn(`${short}: Error during part():`, e);
|
||||
}
|
||||
@ -4354,7 +4201,7 @@ router.ws("/ws/:id", async (ws, req) => {
|
||||
// Clean up peer from audio registry before replacing WebSocket
|
||||
if (gameId in audio) {
|
||||
try {
|
||||
part(audio[gameId], session);
|
||||
webrtcPart(audio[gameId], session);
|
||||
console.log(`${short}: Cleaned up peer ${session.name} from audio registry during reconnection`);
|
||||
} catch (e) {
|
||||
console.warn(`${short}: Error cleaning up peer during reconnection:`, e);
|
||||
@ -4396,75 +4243,25 @@ router.ws("/ws/:id", async (ws, req) => {
|
||||
case "join":
|
||||
// Accept either legacy `config` or newer `data` field from clients
|
||||
|
||||
join(audio[gameId], session, data.config || data.data || {});
|
||||
webrtcJoin(audio[gameId], session, data.config || data.data || {});
|
||||
break;
|
||||
|
||||
case "part":
|
||||
part(audio[gameId], session);
|
||||
webrtcPart(audio[gameId], session);
|
||||
break;
|
||||
|
||||
case "relayICECandidate":
|
||||
{
|
||||
if (!(gameId in audio)) {
|
||||
console.error(`${session.id}:${id} <- relayICECandidate - Does not have Audio`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Support both { config: {...} } and { data: {...} } client payloads
|
||||
// Delegate to the webrtc signaling helper (it performs its own checks)
|
||||
const cfg = data.config || data.data || {};
|
||||
const { peer_id, candidate } = cfg;
|
||||
if (debug.audio)
|
||||
console.log(`${short}:${id} <- relayICECandidate ${getName(session)} to ${peer_id}`, candidate);
|
||||
|
||||
message = JSON.stringify({
|
||||
type: "iceCandidate",
|
||||
data: {
|
||||
peer_id: getName(session),
|
||||
peer_name: getName(session),
|
||||
candidate: candidate
|
||||
},
|
||||
}) as any;
|
||||
|
||||
if (peer_id in audio[gameId]) {
|
||||
try {
|
||||
(audio[gameId][peer_id] as any).ws.send(message as any);
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
handleRelayICECandidate(gameId, cfg, session, undefined, debug);
|
||||
}
|
||||
break;
|
||||
|
||||
case "relaySessionDescription":
|
||||
{
|
||||
if (!(gameId in audio)) {
|
||||
console.error(`${gameId} - relaySessionDescription - Does not have Audio`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Support both { config: {...} } and { data: {...} } client payloads
|
||||
const cfg = data.config || data.data || {};
|
||||
const { peer_id, session_description } = cfg;
|
||||
if (debug.audio)
|
||||
console.log(
|
||||
`${short}:${id} - relaySessionDescription ${getName(session)} to ${peer_id}`,
|
||||
session_description
|
||||
);
|
||||
message = JSON.stringify({
|
||||
type: "sessionDescription",
|
||||
data: {
|
||||
peer_id: getName(session),
|
||||
peer_name: getName(session),
|
||||
session_description: session_description
|
||||
},
|
||||
}) as any;
|
||||
if (peer_id in audio[gameId]) {
|
||||
try {
|
||||
(audio[gameId][peer_id] as any).ws.send(message as any);
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
handleRelaySessionDescription(gameId, cfg, session, undefined, debug);
|
||||
}
|
||||
break;
|
||||
|
||||
@ -4479,42 +4276,8 @@ router.ws("/ws/:id", async (ws, req) => {
|
||||
|
||||
case "peer_state_update":
|
||||
{
|
||||
// Broadcast a peer state update (muted/video_on) to other peers in the game audio map
|
||||
if (!(gameId in audio)) {
|
||||
console.error(`${session.id}:${gameId} <- peer_state_update - Does not have Audio`);
|
||||
return;
|
||||
}
|
||||
|
||||
const cfg = data.config || data.data || {};
|
||||
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
|
||||
},
|
||||
});
|
||||
|
||||
// Send to all other peers
|
||||
for (const other in audio[gameId]) {
|
||||
if (other === session.name) continue;
|
||||
try {
|
||||
try {
|
||||
(audio[gameId][other] as any).ws.send(messagePayload as any);
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed sending peer_state_update to ${other}:`, e);
|
||||
}
|
||||
}
|
||||
broadcastPeerStateUpdate(gameId, cfg, session, undefined);
|
||||
}
|
||||
break;
|
||||
|
||||
|
317
server/routes/webrtc-signaling.ts
Normal file
317
server/routes/webrtc-signaling.ts
Normal file
@ -0,0 +1,317 @@
|
||||
/* 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);
|
||||
}
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user