chuck.js/poc-webrtc/public/client.js
Jeena 47faae81e5 Add standalone POC comparing WebRTC DataChannel vs Socket.IO
Self-contained test under poc-webrtc/ that does not touch the game.
Spins up an Express + WebSocket signaling + node-datachannel server
alongside a Socket.IO server, serves a simple browser client that
runs the same game-like traffic pattern (14Hz worldUpdates, input
events, ping/pong) over either transport based on a URL flag.

Captures per-session stats to a JSONL file and ships an analyze.js
that prints a per-(transport, phase) summary of RTT percentiles,
receive rate, and seq-gap counts so the TCP-vs-UDP-style comparison
becomes quantitative rather than eyeball.

Confirms node-datachannel installs and works on this platform and
that the dual-channel (reliable + unreliable) pattern is feasible
to maintain — both prerequisites for the real integration.
2026-05-11 00:38:01 +00:00

80 lines
2.9 KiB
JavaScript

// WebRTC DataChannel client. Signaling over WebSocket, then two channels:
// - reliable: default settings, used for welcome/control
// - unreliable: ordered:false, maxRetransmits:0 — used for game state + pings
(function () {
const POC = window.POC;
POC.setStatus("signaling…", "wait");
const wsProto = location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(wsProto + "://" + location.host + "/signal");
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
});
// Create both channels client-side so the server's onDataChannel fires for both.
const reliable = pc.createDataChannel("reliable");
const unreliable = pc.createDataChannel("unreliable", {
ordered: false,
maxRetransmits: 0
});
function wireChannel(ch, label) {
ch.onopen = () => {
POC.log(`channel '${label}' open`);
if (label === "unreliable") {
POC.attach((obj) => {
if (ch.readyState === "open") ch.send(JSON.stringify(obj));
});
POC.setStatus("connected (webrtc unreliable)", "ok");
}
};
ch.onclose = () => {
POC.log(`channel '${label}' closed`);
POC.setStatus("disconnected", "bad");
};
ch.onerror = (e) => POC.log(`channel '${label}' error: ${e.message || e}`);
ch.onmessage = (e) => {
try { POC.handleMessage(JSON.parse(e.data)); }
catch (err) { POC.log("parse error: " + err.message); }
};
}
wireChannel(reliable, "reliable");
wireChannel(unreliable, "unreliable");
pc.onicecandidate = (e) => {
if (e.candidate) {
ws.send(JSON.stringify({
type: "ice",
candidate: e.candidate.candidate,
mid: e.candidate.sdpMid
}));
}
};
pc.onconnectionstatechange = () => {
POC.log("pc state: " + pc.connectionState);
if (pc.connectionState === "failed") POC.setStatus("failed", "bad");
};
ws.onopen = async () => {
POC.log("signaling open, creating offer");
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
ws.send(JSON.stringify({ type: "sdp", sdp: offer.sdp, sdpType: offer.type }));
};
ws.onmessage = async (e) => {
const m = JSON.parse(e.data);
if (m.type === "sdp") {
await pc.setRemoteDescription({ type: m.sdpType, sdp: m.sdp });
POC.log("got remote sdp (" + m.sdpType + ")");
} else if (m.type === "ice") {
try {
await pc.addIceCandidate({ candidate: m.candidate, sdpMid: m.mid });
} catch (err) {
POC.log("addIceCandidate error: " + err.message);
}
}
};
ws.onclose = () => POC.log("signaling closed");
})();