When variant JS is loaded from featherChat (different origin), WASM
imports need to resolve via the /audio/ Caddy path, not root /.
All 4 variant files now use: (window.__WZP_BASE_URL || '') + '/wasm/...'
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
wasm-pack generated .gitignore was excluding all build output.
The WASM (337KB) and JS glue need to be in the repo so the
wzp-web static server can serve them without a build step.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ServeDir now falls back to index.html for unknown paths (SPA routing).
https://host:port/manwe loads the page with room input pre-filled as "manwe".
JS getRoom() already reads the path, now the page actually loads.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Room-based SFU relay:
- Clients join named rooms (room name from QUIC SNI)
- Each participant's packets forwarded to all others (no mixing)
- Multiple rooms run concurrently on one relay
- Web bridge passes room name from URL path to relay
Push-to-talk (radio mode):
- Toggle "Radio mode" checkbox after connecting
- Hold PTT button or spacebar to transmit
- Release to mute mic (receive-only)
- Works on desktop (spacebar) and mobile (touch)
URL routing:
- /myroom → joins room "myroom"
- Room name input field as fallback
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rooms:
- URL-based: open /myroom to join a room
- Two clients in same room get bridged through relay
- Input field for room name, also supports URL path and hash
- Each room creates independent relay connections
AudioWorklet (replaces deprecated ScriptProcessorNode):
- capture-processor.js: accumulates mic samples, sends 960-sample frames
- playback-processor.js: pull-based output with 200ms buffer cap
- Falls back to ScriptProcessor if AudioWorklet unavailable
- Eliminates drift: worklet runs on audio thread, not main thread
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bridge mode rewrite:
- First client echoes while waiting, checks every 100ms if paired
- Second client triggers bridge immediately, first exits echo loop
- After bridge ends, slot is cleared for the next pair
- No more two tasks competing for the same transport recv
Web client auto-reconnect:
- On WebSocket close/error, automatically reconnects after 1s
- Keeps retrying as long as the user hasn't clicked Disconnect
Test fix:
- Install rustls crypto provider in transport config tests
(fixes race condition when running full workspace tests)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
150ms cap was too tight for Iran relay (high jitter), causing constant
audio drops. Raised to 1s — packet bursts are absorbed smoothly,
drift reset only fires on real accumulation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pull-based ScriptProcessor approach broke audio completely.
Back to createBufferSource scheduling which worked, but with
tighter 200ms max drift (was 300ms). Snaps back when exceeded.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous version output 960 samples into 1024-sample callback frames,
causing 64 samples of silence per frame (choppy/robotic sound).
Now accumulates float samples in a continuous buffer, output callback
pulls exactly 1024 at a time regardless of input frame size.
Buffer capped at 200ms to prevent drift.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Web playback rewritten from push-scheduling to pull-based ring buffer:
- ScriptProcessorNode pulls frames from buffer every ~21ms
- Buffer capped at 10 frames (~200ms) — drops oldest on overflow
- Latency permanently bounded, no drift over time
Also: install ring crypto provider for rustls TLS on Linux,
build on debian-12 to match mequ glibc.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When playback buffer drifts beyond 300ms ahead of real-time, reset
to 40ms. This prevents the unbounded latency growth over long sessions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Schedule each playback buffer to start exactly where the last ended
(was causing gaps/overlaps with fixed 60ms offset)
- Log AudioContext sample rate to console for debugging
- Reset playback timeline when falling behind
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use power-of-2 buffer (1024) for ScriptProcessorNode
- Accumulate samples and send exact 960-sample frames
- Remove unused watch import from relay
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New wzp-web crate serves a web page with:
- Browser mic capture via Web Audio API (48kHz mono)
- WebSocket transport for raw PCM audio
- Server-side Opus encode/decode + FEC through wzp relay
- Real-time audio playback in browser
- Level meter and connection stats
Usage:
wzp-relay --listen 0.0.0.0:4433 # start relay
wzp-web --port 8080 --relay 127.0.0.1:4433 # start web bridge
Open http://localhost:8080 in browser
Two browsers connected to the same relay get bridged for a call.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>