mirror of
https://github.com/logsol/chuck.js.git
synced 2026-05-11 10:37:34 +00:00
Socket.IO (TCP) holds back later packets while it retransmits a lost one, which stalls worldUpdate delivery on lossy long-distance links — exactly the pattern game state suffers worst from. WebRTC DataChannels in unreliable mode (ordered:false, maxRetransmits:0) drop late packets instead of queueing them, which is what we want for high-frequency state sync. Adds a per-user WebRTCTransport on top of the existing Socket.IO connection. Socket.IO stays in charge of bootstrap, signaling (SDP/ICE exchange), and control messages — only gameCommand payloads get routed onto the unreliable channel once it's open. If WebRTC fails to negotiate, gameCommand transparently falls back to Socket.IO, so the game keeps working unchanged. A new StatsLogger writes per-session JSONL events (session_start, webrtc_ready with negotiation time, per-second stats with transport, RTT samples, recv/send rates, seq gaps) so we can compare real-world runs (e.g. Germany server <-> Korea client) instead of guessing. URL flag ?webrtc=0 forces fallback for A/B testing. scripts/webrtc-browser-test.js spins up a headless Chromium against a freshly-started server and asserts the unreliable channel opens and gameCommand traffic actually rides it.
112 lines
3.8 KiB
JavaScript
112 lines
3.8 KiB
JavaScript
define([
|
|
"node-datachannel"
|
|
],
|
|
|
|
function (nodeDataChannel) {
|
|
|
|
"use strict";
|
|
|
|
// Per-user WebRTC peer + unreliable data channel.
|
|
// The User class relays SDP/ICE between this transport and the client
|
|
// (over Socket.IO). This class only deals with the WebRTC peer state.
|
|
function WebRTCTransport(userId, callbacks) {
|
|
this.userId = userId;
|
|
this.callbacks = callbacks || {};
|
|
this.unreliable = null;
|
|
this.ready = false;
|
|
this.createdAt = Date.now();
|
|
|
|
var iceServers = ["stun:stun.l.google.com:19302"];
|
|
this.pc = new nodeDataChannel.PeerConnection("peer-" + userId, {
|
|
iceServers: iceServers
|
|
});
|
|
|
|
var self = this;
|
|
|
|
this.pc.onLocalDescription(function (sdp, type) {
|
|
if (self.callbacks.onLocalDescription) {
|
|
self.callbacks.onLocalDescription({ sdp: sdp, type: type });
|
|
}
|
|
});
|
|
|
|
this.pc.onLocalCandidate(function (candidate, mid) {
|
|
if (self.callbacks.onLocalCandidate) {
|
|
self.callbacks.onLocalCandidate({ candidate: candidate, mid: mid });
|
|
}
|
|
});
|
|
|
|
this.pc.onDataChannel(function (dc) {
|
|
var label = dc.getLabel();
|
|
if (label !== "unreliable") return;
|
|
self.unreliable = dc;
|
|
dc.onOpen(function () {
|
|
self.ready = true;
|
|
if (self.callbacks.onReady) {
|
|
self.callbacks.onReady(Date.now() - self.createdAt);
|
|
}
|
|
});
|
|
dc.onMessage(function (msg) {
|
|
if (self.callbacks.onMessage) self.callbacks.onMessage(msg);
|
|
});
|
|
dc.onClosed(function () {
|
|
self.ready = false;
|
|
if (self.callbacks.onClosed) self.callbacks.onClosed();
|
|
});
|
|
});
|
|
}
|
|
|
|
WebRTCTransport.prototype.handleRemoteDescription = function (sdp, type) {
|
|
try {
|
|
console.log("[webrtc] " + this.userId + " setRemoteDescription " + type);
|
|
this.pc.setRemoteDescription(sdp, type);
|
|
this.haveRemoteDesc = true;
|
|
// Flush any candidates that arrived before the offer.
|
|
if (this.pendingCandidates && this.pendingCandidates.length) {
|
|
var pending = this.pendingCandidates;
|
|
this.pendingCandidates = [];
|
|
for (var i = 0; i < pending.length; i++) {
|
|
this.handleRemoteCandidate(pending[i].candidate, pending[i].mid);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error("[webrtc] setRemoteDescription failed:", e && (e.message || e));
|
|
}
|
|
};
|
|
|
|
WebRTCTransport.prototype.handleRemoteCandidate = function (candidate, mid) {
|
|
// Buffer until we have the remote description — adding candidates before
|
|
// setRemoteDescription throws.
|
|
if (!this.haveRemoteDesc) {
|
|
this.pendingCandidates = this.pendingCandidates || [];
|
|
this.pendingCandidates.push({ candidate: candidate, mid: mid });
|
|
return;
|
|
}
|
|
try {
|
|
this.pc.addRemoteCandidate(candidate, mid || "0");
|
|
} catch (e) {
|
|
console.error("[webrtc] addRemoteCandidate failed:", e && (e.message || e));
|
|
}
|
|
};
|
|
|
|
WebRTCTransport.prototype.isReady = function () {
|
|
return this.ready && this.unreliable && this.unreliable.isOpen();
|
|
};
|
|
|
|
WebRTCTransport.prototype.send = function (message) {
|
|
if (!this.isReady()) return false;
|
|
try {
|
|
this.unreliable.sendMessage(message);
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
WebRTCTransport.prototype.destroy = function () {
|
|
try { if (this.unreliable) this.unreliable.close(); } catch (_) {}
|
|
try { this.pc.close(); } catch (_) {}
|
|
this.ready = false;
|
|
};
|
|
|
|
return WebRTCTransport;
|
|
});
|