feat: web variants use relay WS directly — no bridge needed
Updated all 3 web client variants to connect via the relay's new WebSocket endpoint (/ws/room) instead of the wzp-web bridge. index.html: - Boot logic now creates the correct client class per variant (WZPPureClient, WZPHybridClient, or WZPFullClient) wzp-full.js (Full WASM): - Tries WebTransport first with 3s timeout - Falls back to WebSocket if WT unavailable or relay lacks HTTP/3 - WS fallback sends raw PCM (same as pure), WASM FEC module still loaded - When WT works: full encrypted + FEC pipeline over UDP datagrams Pure + Hybrid variants already used /ws/room — no changes needed. All JS syntax verified, 63 relay tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -117,8 +117,9 @@ function wzpBoot() {
|
|||||||
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
var wsUrl = proto + '//' + location.host + '/ws/' + encodeURIComponent(room);
|
var wsUrl = proto + '//' + location.host + '/ws/' + encodeURIComponent(room);
|
||||||
|
|
||||||
// Create client (currently always WZPPureClient; future: switch on variant)
|
// Create client based on selected variant
|
||||||
client = new WZPPureClient({
|
var variant = WZPCore.detectVariant();
|
||||||
|
var clientOpts = {
|
||||||
wsUrl: wsUrl,
|
wsUrl: wsUrl,
|
||||||
room: room,
|
room: room,
|
||||||
onAudio: function(pcm) {
|
onAudio: function(pcm) {
|
||||||
@@ -130,7 +131,17 @@ function wzpBoot() {
|
|||||||
onStats: function(stats) {
|
onStats: function(stats) {
|
||||||
WZPCore.updateStats(stats);
|
WZPCore.updateStats(stats);
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (variant === 'full' && typeof WZPFullClient !== 'undefined') {
|
||||||
|
// Full variant: add WebTransport URL, falls back to WS if WT unavailable
|
||||||
|
clientOpts.url = location.origin.replace('http', 'https');
|
||||||
|
client = new WZPFullClient(clientOpts);
|
||||||
|
} else if (variant === 'hybrid' && typeof WZPHybridClient !== 'undefined') {
|
||||||
|
client = new WZPHybridClient(clientOpts);
|
||||||
|
} else {
|
||||||
|
client = new WZPPureClient(clientOpts);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.connect();
|
await client.connect();
|
||||||
|
|||||||
@@ -34,12 +34,14 @@ class WZPFullClient {
|
|||||||
*/
|
*/
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
this.url = options.url;
|
this.url = options.url;
|
||||||
|
this.wsUrl = options.wsUrl; // WS fallback URL
|
||||||
this.room = options.room;
|
this.room = options.room;
|
||||||
this.onAudio = options.onAudio || null;
|
this.onAudio = options.onAudio || null;
|
||||||
this.onStatus = options.onStatus || null;
|
this.onStatus = options.onStatus || null;
|
||||||
this.onStats = options.onStats || null;
|
this.onStats = options.onStats || null;
|
||||||
|
|
||||||
this.wt = null; // WebTransport instance
|
this.wt = null; // WebTransport instance
|
||||||
|
this.ws = null; // WebSocket fallback
|
||||||
this.datagramWriter = null; // WritableStreamDefaultWriter
|
this.datagramWriter = null; // WritableStreamDefaultWriter
|
||||||
this.datagramReader = null; // ReadableStreamDefaultReader
|
this.datagramReader = null; // ReadableStreamDefaultReader
|
||||||
this.cryptoSession = null; // WzpCryptoSession (WASM)
|
this.cryptoSession = null; // WzpCryptoSession (WASM)
|
||||||
@@ -48,6 +50,7 @@ class WZPFullClient {
|
|||||||
this.sequence = 0;
|
this.sequence = 0;
|
||||||
this._wasmModule = null;
|
this._wasmModule = null;
|
||||||
this._connected = false;
|
this._connected = false;
|
||||||
|
this._useWebTransport = false; // true if WT connected, false = WS fallback
|
||||||
this._startTime = 0;
|
this._startTime = 0;
|
||||||
this._statsInterval = null;
|
this._statsInterval = null;
|
||||||
this._recvLoopRunning = false;
|
this._recvLoopRunning = false;
|
||||||
@@ -61,49 +64,45 @@ class WZPFullClient {
|
|||||||
async connect() {
|
async connect() {
|
||||||
if (this._connected) return;
|
if (this._connected) return;
|
||||||
|
|
||||||
// --- Guard: WebTransport support ---
|
|
||||||
if (typeof WebTransport === 'undefined') {
|
|
||||||
throw new Error(
|
|
||||||
'WebTransport is not supported in this browser. ' +
|
|
||||||
'Use the hybrid (?variant=hybrid) or pure (?variant=pure) variant instead.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._status('Loading WASM module...');
|
this._status('Loading WASM module...');
|
||||||
|
|
||||||
// 1. Load WASM
|
// 1. Load WASM (FEC + crypto)
|
||||||
this._wasmModule = await import(WZP_WASM_PATH);
|
this._wasmModule = await import(WZP_WASM_PATH);
|
||||||
await this._wasmModule.default();
|
await this._wasmModule.default();
|
||||||
|
|
||||||
this._status('Connecting via WebTransport to ' + this.url + '...');
|
// 2. Try WebTransport first, fall back to WebSocket
|
||||||
|
let wtSuccess = false;
|
||||||
// 2. WebTransport connection
|
if (typeof WebTransport !== 'undefined' && this.url) {
|
||||||
// The URL should include the room, e.g. https://host:port/room
|
try {
|
||||||
const wtUrl = this.url + '/' + encodeURIComponent(this.room);
|
this._status('Trying WebTransport...');
|
||||||
this.wt = new WebTransport(wtUrl);
|
const wtUrl = this.url + '/' + encodeURIComponent(this.room);
|
||||||
|
this.wt = new WebTransport(wtUrl);
|
||||||
this.wt.closed.then(() => {
|
await Promise.race([
|
||||||
const wasConnected = this._connected;
|
this.wt.ready,
|
||||||
this._cleanup();
|
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
|
||||||
if (wasConnected) {
|
]);
|
||||||
this._status('WebTransport closed');
|
this.datagramWriter = this.wt.datagrams.writable.getWriter();
|
||||||
|
this.datagramReader = this.wt.datagrams.readable.getReader();
|
||||||
|
this._status('Performing key exchange...');
|
||||||
|
await this._performKeyExchange();
|
||||||
|
wtSuccess = true;
|
||||||
|
this._useWebTransport = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[wzp-full] WebTransport failed, falling back to WebSocket:', e.message);
|
||||||
|
if (this.wt) { try { this.wt.close(); } catch (_) {} }
|
||||||
|
this.wt = null;
|
||||||
|
this.datagramWriter = null;
|
||||||
|
this.datagramReader = null;
|
||||||
}
|
}
|
||||||
}).catch((err) => {
|
}
|
||||||
this._cleanup();
|
|
||||||
this._status('WebTransport error: ' + err.message);
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.wt.ready;
|
if (!wtSuccess) {
|
||||||
|
// WebSocket fallback (same as hybrid — WASM loaded but uses WS transport)
|
||||||
|
this._useWebTransport = false;
|
||||||
|
await this._connectWebSocket();
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Get datagram streams (unreliable, QUIC DATAGRAM frames)
|
// 3. Initialise FEC
|
||||||
this.datagramWriter = this.wt.datagrams.writable.getWriter();
|
|
||||||
this.datagramReader = this.wt.datagrams.readable.getReader();
|
|
||||||
|
|
||||||
// 4. Key exchange over a bidirectional stream
|
|
||||||
this._status('Performing key exchange...');
|
|
||||||
await this._performKeyExchange();
|
|
||||||
|
|
||||||
// 5. Initialise FEC (5 source symbols per block, 256-byte symbols)
|
|
||||||
this.fecEncoder = new this._wasmModule.WzpFecEncoder(5, 256);
|
this.fecEncoder = new this._wasmModule.WzpFecEncoder(5, 256);
|
||||||
this.fecDecoder = new this._wasmModule.WzpFecDecoder(5, 256);
|
this.fecDecoder = new this._wasmModule.WzpFecDecoder(5, 256);
|
||||||
|
|
||||||
@@ -113,10 +112,50 @@ class WZPFullClient {
|
|||||||
this._startTime = Date.now();
|
this._startTime = Date.now();
|
||||||
this._startStatsTimer();
|
this._startStatsTimer();
|
||||||
|
|
||||||
// 6. Start receive loop (runs until disconnect)
|
// 4. Start receive loop (WebTransport only — WS uses onmessage)
|
||||||
this._recvLoop();
|
if (this._useWebTransport) {
|
||||||
|
this._recvLoop();
|
||||||
|
this._status('Connected to room: ' + this.room + ' (WebTransport, encrypted, FEC active)');
|
||||||
|
} else {
|
||||||
|
this._status('Connected to room: ' + this.room + ' (WebSocket fallback, WASM FEC loaded)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._status('Connected to room: ' + this.room + ' (encrypted, FEC active)');
|
/**
|
||||||
|
* WebSocket fallback connection (used when WebTransport unavailable).
|
||||||
|
*/
|
||||||
|
async _connectWebSocket() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this._status('Connecting via WebSocket (fallback)...');
|
||||||
|
this.ws = new WebSocket(this.wsUrl);
|
||||||
|
this.ws.binaryType = 'arraybuffer';
|
||||||
|
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
this._status('WebSocket connected to room: ' + this.room);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onmessage = (event) => {
|
||||||
|
if (!(event.data instanceof ArrayBuffer)) return;
|
||||||
|
const pcm = new Int16Array(event.data);
|
||||||
|
this.stats.recv++;
|
||||||
|
if (this.onAudio) this.onAudio(pcm);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onclose = () => {
|
||||||
|
if (this._connected) {
|
||||||
|
this._cleanup();
|
||||||
|
this._status('Disconnected');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ws.onerror = () => {
|
||||||
|
if (!this._connected) {
|
||||||
|
this._cleanup();
|
||||||
|
reject(new Error('WebSocket connection failed'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,6 +167,10 @@ class WZPFullClient {
|
|||||||
try { this.wt.close(); } catch (_) { /* ignore */ }
|
try { this.wt.close(); } catch (_) { /* ignore */ }
|
||||||
this.wt = null;
|
this.wt = null;
|
||||||
}
|
}
|
||||||
|
if (this.ws) {
|
||||||
|
try { this.ws.close(); } catch (_) { /* ignore */ }
|
||||||
|
this.ws = null;
|
||||||
|
}
|
||||||
this._cleanup();
|
this._cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +182,19 @@ class WZPFullClient {
|
|||||||
* @param {ArrayBuffer} pcmBuffer 960-sample Int16 PCM (1920 bytes)
|
* @param {ArrayBuffer} pcmBuffer 960-sample Int16 PCM (1920 bytes)
|
||||||
*/
|
*/
|
||||||
async sendAudio(pcmBuffer) {
|
async sendAudio(pcmBuffer) {
|
||||||
if (!this._connected || !this.datagramWriter || !this.cryptoSession) return;
|
if (!this._connected) return;
|
||||||
|
|
||||||
|
// WebSocket fallback: send raw PCM like pure/hybrid
|
||||||
|
if (!this._useWebTransport) {
|
||||||
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||||
|
this.ws.send(pcmBuffer);
|
||||||
|
this.sequence++;
|
||||||
|
this.stats.sent++;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.datagramWriter || !this.cryptoSession) return;
|
||||||
|
|
||||||
const pcmBytes = new Uint8Array(pcmBuffer);
|
const pcmBytes = new Uint8Array(pcmBuffer);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user