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 wsUrl = proto + '//' + location.host + '/ws/' + encodeURIComponent(room);
|
||||
|
||||
// Create client (currently always WZPPureClient; future: switch on variant)
|
||||
client = new WZPPureClient({
|
||||
// Create client based on selected variant
|
||||
var variant = WZPCore.detectVariant();
|
||||
var clientOpts = {
|
||||
wsUrl: wsUrl,
|
||||
room: room,
|
||||
onAudio: function(pcm) {
|
||||
@@ -130,7 +131,17 @@ function wzpBoot() {
|
||||
onStats: function(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 {
|
||||
await client.connect();
|
||||
|
||||
@@ -34,12 +34,14 @@ class WZPFullClient {
|
||||
*/
|
||||
constructor(options) {
|
||||
this.url = options.url;
|
||||
this.wsUrl = options.wsUrl; // WS fallback URL
|
||||
this.room = options.room;
|
||||
this.onAudio = options.onAudio || null;
|
||||
this.onStatus = options.onStatus || null;
|
||||
this.onStats = options.onStats || null;
|
||||
|
||||
this.wt = null; // WebTransport instance
|
||||
this.ws = null; // WebSocket fallback
|
||||
this.datagramWriter = null; // WritableStreamDefaultWriter
|
||||
this.datagramReader = null; // ReadableStreamDefaultReader
|
||||
this.cryptoSession = null; // WzpCryptoSession (WASM)
|
||||
@@ -48,6 +50,7 @@ class WZPFullClient {
|
||||
this.sequence = 0;
|
||||
this._wasmModule = null;
|
||||
this._connected = false;
|
||||
this._useWebTransport = false; // true if WT connected, false = WS fallback
|
||||
this._startTime = 0;
|
||||
this._statsInterval = null;
|
||||
this._recvLoopRunning = false;
|
||||
@@ -61,49 +64,45 @@ class WZPFullClient {
|
||||
async connect() {
|
||||
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...');
|
||||
|
||||
// 1. Load WASM
|
||||
// 1. Load WASM (FEC + crypto)
|
||||
this._wasmModule = await import(WZP_WASM_PATH);
|
||||
await this._wasmModule.default();
|
||||
|
||||
this._status('Connecting via WebTransport to ' + this.url + '...');
|
||||
|
||||
// 2. WebTransport connection
|
||||
// The URL should include the room, e.g. https://host:port/room
|
||||
// 2. Try WebTransport first, fall back to WebSocket
|
||||
let wtSuccess = false;
|
||||
if (typeof WebTransport !== 'undefined' && this.url) {
|
||||
try {
|
||||
this._status('Trying WebTransport...');
|
||||
const wtUrl = this.url + '/' + encodeURIComponent(this.room);
|
||||
this.wt = new WebTransport(wtUrl);
|
||||
|
||||
this.wt.closed.then(() => {
|
||||
const wasConnected = this._connected;
|
||||
this._cleanup();
|
||||
if (wasConnected) {
|
||||
this._status('WebTransport closed');
|
||||
}
|
||||
}).catch((err) => {
|
||||
this._cleanup();
|
||||
this._status('WebTransport error: ' + err.message);
|
||||
});
|
||||
|
||||
await this.wt.ready;
|
||||
|
||||
// 3. Get datagram streams (unreliable, QUIC DATAGRAM frames)
|
||||
await Promise.race([
|
||||
this.wt.ready,
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 3000)),
|
||||
]);
|
||||
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();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Initialise FEC (5 source symbols per block, 256-byte symbols)
|
||||
if (!wtSuccess) {
|
||||
// WebSocket fallback (same as hybrid — WASM loaded but uses WS transport)
|
||||
this._useWebTransport = false;
|
||||
await this._connectWebSocket();
|
||||
}
|
||||
|
||||
// 3. Initialise FEC
|
||||
this.fecEncoder = new this._wasmModule.WzpFecEncoder(5, 256);
|
||||
this.fecDecoder = new this._wasmModule.WzpFecDecoder(5, 256);
|
||||
|
||||
@@ -113,10 +112,50 @@ class WZPFullClient {
|
||||
this._startTime = Date.now();
|
||||
this._startStatsTimer();
|
||||
|
||||
// 6. Start receive loop (runs until disconnect)
|
||||
// 4. Start receive loop (WebTransport only — WS uses onmessage)
|
||||
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 */ }
|
||||
this.wt = null;
|
||||
}
|
||||
if (this.ws) {
|
||||
try { this.ws.close(); } catch (_) { /* ignore */ }
|
||||
this.ws = null;
|
||||
}
|
||||
this._cleanup();
|
||||
}
|
||||
|
||||
@@ -139,7 +182,19 @@ class WZPFullClient {
|
||||
* @param {ArrayBuffer} pcmBuffer 960-sample Int16 PCM (1920 bytes)
|
||||
*/
|
||||
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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user