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>
PTT mode was causing delayed/lost audio because:
1. Silence suppression ate the start of speech after PTT release
2. Jitter buffer target depth was too high for interactive use
Web bridge now uses:
- suppression_enabled: false (PTT handles silence at browser level)
- jitter_target: 3 (60ms vs ~1s default)
- jitter_max: 20 (400ms cap)
- jitter_min: 1 (start playing after 20ms)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Client: sends SignalMessage::Hangup(Normal) before closing in all modes
(send-tone, file mode, silence mode) so the relay knows the session ended.
Relay: downgrades "timed out" / "reset" / "closed" recv errors from
ERROR to INFO since these are normal disconnect scenarios.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused imports in featherchat.rs (tracing, QualityProfile)
- Remove unused comfort_noise field from CallEncoder (cn_level is used instead)
- Prefix unused _metrics_file in CliArgs
- Prefix unused _addr in Participant
- Remove unused RoomSlot struct and rooms field from web AppState
- Remove unused HashMap import from web main
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaces the single-shot ephemeral VM approach:
- --prepare: create VM, install deps (Rust, cmake, etc), upload source
- --build: build on VM with full output (iterate on errors)
- --transfer: download binaries to target/linux-x86_64/
- --destroy: delete VM when done
- --upload: re-upload source to existing VM
- --all: prepare + build + transfer (VM persists)
VM reuse: --prepare detects existing wzp-builder VM and just re-uploads.
All steps get VM IP from hcloud server list (last created).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
T6 wiring: Trunking in relay hot path
- TrunkedForwarder wraps transport with TrunkBatcher
- run_participant uses 5ms flush timer when trunking enabled
- send_trunk/recv_trunk on QuinnTransport
- --trunking flag on relay config
- 2 new tests: forwarder batches, auto-flush on full
T7 wiring: Mini-frames in encoder/decoder
- MediaPacket::encode_compact/decode_compact with MiniFrameContext
- CallEncoder sends mini-headers for consecutive frames (full every 50th)
- CallDecoder auto-detects full vs mini on receive
- mini_frames_enabled in CallConfig (default true)
- 3 new tests: encode/decode sequence, periodic full, disabled mode
Noise suppression (nnnoiseless/RNNoise)
- NoiseSupressor in wzp-codec: pure Rust ML-based noise removal
- Processes 960-sample frames as two 480-sample halves
- Integrated in CallEncoder before silence detection
- noise_suppression in CallConfig (default true)
- 4 new tests: creation, processing, SNR improvement, passthrough
T1-S4: Adaptive playout delay
- AdaptivePlayoutDelay: EMA-based jitter tracking (NetEq-inspired)
- Computes target_delay from observed inter-arrival jitter
- JitterBuffer::new_adaptive() uses adaptive delay
- adaptive_jitter in CallConfig (default true)
- 5 new tests: stable, jitter increase, recovery, clamping, estimate
272 tests passing across all crates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WZP-S-4: Room access control
- hash_room_name() in wzp-crypto: SHA-256("featherchat-group:"+name)[:16]
- CLI --room flag hashes before SNI, web bridge does the same
- RoomManager gains ACL: with_acl(), allow(), is_authorized()
- join() returns Result, rejects unauthorized fingerprints
WZP-S-5: Crypto handshake wired into all live paths
- CLI: perform_handshake() after connect, before any mode
- Relay: accept_handshake() after auth, before room join
- Web bridge: perform_handshake() after auth, before audio
- Relay generates ephemeral identity at startup
WZP-S-6: Web bridge featherChat auth
- --auth-url flag: browsers send {"type":"auth","token":"..."} as first WS msg
- Validates against featherChat, passes token to relay
- --cert/--key flags for production TLS (replaces self-signed)
WZP-S-7: wzp-proto standalone
- Cargo.toml uses explicit versions (no workspace inheritance)
- FC can use as git dependency
WZP-S-9: All 6 hardcoded assumptions resolved
- Auth, hashed rooms, mandatory handshake, real TLS certs,
profile negotiation, token validation
CLI also gains --room and --token flags.
179 tests passing across all crates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Identity (6 tests):
- Same seed → same Ed25519/X25519 keys, same fingerprint, same display
- Random seed, raw HKDF output verified
BIP39 Mnemonic (3 tests):
- Roundtrip both directions, identical strings
CallSignal Interop (4 tests):
- Offer/Answer/Hangup roundtrip through FC bincode serialization
- Signal type mapping verified
Auth Contract (2 tests):
- Request/response shapes match between WZP and FC
Uses warzone-protocol v0.0.21 as real dependency.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WZP-S-2: Relay token authentication
- New --auth-url flag: relay calls POST {url} with bearer token
- Clients must send SignalMessage::AuthToken as first signal
- Relay validates against featherChat's /v1/auth/validate endpoint
- Rejects unauthenticated clients before they join rooms
- New auth.rs module with validate_token() + tests
WZP-S-3: featherChat signaling bridge
- New featherchat.rs module for CallSignal interop
- WzpCallPayload: wraps SignalMessage + relay_addr + room name
- encode_call_payload/decode_call_payload for JSON serialization
- CallSignalType enum mirrors featherChat's variant
- signal_to_call_type maps WZP signals to FC types
Protocol: Added SignalMessage::AuthToken { token } variant
129 tests passing across all crates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
featherChat commit 064a730 implements:
- CallSignal WireMessage variant with Offer/Answer/ICE/Hangup/Reject/Ringing/Busy
- POST /v1/auth/validate endpoint returning fingerprint + alias
WZP can now:
- Send SignalMessage as JSON in CallSignal.payload through FC's E2E channel
- Verify FC bearer tokens on the relay via the validate endpoint
Next: WZP-S-2 (relay auth) and WZP-S-3 (signaling bridge in client)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Defines 5 tasks (FC-CRATE-1/2/3, WZP-CRATE-1/2) to make both
projects' crates importable by each other:
featherChat side:
- FC-CRATE-1: Make warzone-protocol standalone (replace workspace deps)
- FC-CRATE-2: Add CallSignal variant using wzp-proto types
- FC-CRATE-3: Extract warzone-identity micro-crate (optional)
WZP side (after FC-CRATE-1):
- WZP-CRATE-1: Replace identity mirror with real warzone-protocol dep
- WZP-CRATE-2: Verify wzp-proto works as git dep from featherChat
Priority: FC-CRATE-1 first (30 min, unblocks everything).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New identity module (wzp-crypto/src/identity.rs) mirrors featherChat's
warzone-protocol identity.rs exactly:
- Seed: 32 bytes, from hex or BIP39 mnemonic (24 words)
- HKDF derivation: same salt (None), same info strings
- Fingerprint: SHA-256(Ed25519 pub)[:16], same xxxx:xxxx format
- Cross-verified: test proves identity module matches KeyExchange trait
CLI flags:
- --seed <64 hex chars>: use a specific identity
- --mnemonic <24 words>: use BIP39 mnemonic from featherChat
- Without either: generates ephemeral identity
Also adds featherChat as git submodule at deps/featherchat for reference.
32 crypto tests passing (27 original + 5 identity tests).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Maps all WZP-S-* (our side) and WZP-FC-* (featherChat side) tasks
with status tracking and priority order.
Key findings:
- WZP-S-1 (HKDF alignment): DONE — both use None salt, info strings match
- WZP-S-9: 6 hardcoded assumptions documented for fixing
- Priority: identity test → CLI seed → CallSignal variant → auth → handshake
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Changed HKDF expand info strings to match featherChat's identity.rs:
- "warzone-ed25519-identity" → "warzone-ed25519"
- "warzone-x25519-identity" → "warzone-x25519"
Same BIP39 seed now produces identical Ed25519/X25519 keypairs in both
featherChat and WZP. This is the prerequisite for shared identity.
Also added FEATHERCHAT_INTEGRATION.md (1209 lines) from featherChat repo
documenting the full integration plan with confirmed code references.
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>
Generates a self-signed certificate at startup for HTTPS.
Required for mic access on Android/remote browsers (getUserMedia
needs a secure context).
Usage: wzp-web --port 9090 --relay 127.0.0.1:4433 --tls
Browser: accept the self-signed cert warning, then mic works.
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>
The --record recv loop was using while-drain which exhausted the jitter
buffer and stopped decoding after the first burst. Now decodes once per
source packet, matching the live mode fix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- --send-file <file.raw> sends a raw PCM file (48kHz mono s16le) through relay
- Combine with --record: --send-file talk.raw --record echo.raw <relay>
- Fixed all unused import warnings in echo_test.rs
Convert any audio to test format:
ffmpeg -i input.mp3 -ar 48000 -ac 1 -f s16le input.raw
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New --echo-test <secs> flag sends a 440Hz tone through relay echo,
records the return, and analyzes quality in 5-second windows:
- Per-window: frames sent/received, loss %, SNR (dB), correlation
- Detects quality degradation over time (compares first vs second half)
- Reports jitter buffer stats (depth, lost, late packets)
- Diagnoses jitter buffer drift and packet loss accumulation
Also exposes jitter_stats() on CallDecoder for diagnostics.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When no --remote is configured, the relay now operates in bridge mode:
- First client connects → echoes while waiting for a peer
- Second client connects → both clients are bridged bidirectionally
- A's packets go to B, B's packets go to A
- Stats logged every 5 seconds (a_to_b / b_to_a packet counts)
- Falls back to echo if only one client connects
This enables the core use case: two clients on different networks
calling each other through a single relay.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Reduced jitter buffer min_depth from 25 (500ms) to 3 (60ms) for fast start
- Fixed live recv loop: decode once per source packet instead of draining
the jitter buffer dry (which advanced seq past future packets)
- Fixed Ok(None) handling: connection closed, not "no packet yet"
Live echo test confirmed working with continuous audio.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- --record now handles Ctrl+C: saves PCM file before exiting
- Relay without --remote runs in echo mode (loops packets back to sender)
instead of sink mode, enabling single-relay audio testing
- recv task returns collected PCM via channel for clean file write
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>