505a834c5b04e978b514befcee2a0e3e7cc903cb
Phase 3c mirrors Phase 3b on the Android receive path. With Phase 0-3b
landed on desktop + Android encoder, this commit completes codec-layer
loss recovery on the Android decoder side.
Architectural difference vs desktop: engine.rs has NO jitter buffer.
The recv task reads packets directly from the transport via
recv_media().await and writes decoded audio straight into the playout
ring. There is no PlayoutResult::Missing equivalent. Gap detection
therefore has to be done via sequence-number tracking — when a packet
arrives with seq > expected_seq, the frames in between are missing and
we attempt to reconstruct them via DRED before decoding the newly-
arrived packet.
Implementation:
Imports & types:
- Added wzp_codec::AdaptiveDecoder, wzp_codec::dred_ffi::{
DredDecoderHandle, DredState} imports.
- Changed the `decoder` local from Box<dyn AudioDecoder> (via
wzp_codec::create_decoder) to concrete AdaptiveDecoder::new(profile).
Same reasoning as Phase 3b: reconstruct_from_dred is an inherent
method, not a trait method, so we need the concrete type.
Recv task state (all task-local, no new struct fields):
- dred_decoder: DredDecoderHandle
- dred_parse_scratch: DredState (reused, overwritten per parse)
- last_good_dred: DredState (cached most-recent valid state)
- last_good_dred_seq: Option<u16>
- expected_seq: Option<u16> (for gap detection)
- dred_reconstructions: u64 (telemetry)
- classical_plc_invocations: u64 (telemetry)
Recv loop body (Opus source packets only):
1. Parse DRED from the new packet first so last_good_dred reflects
the freshest state available for gap recovery.
2. Detect a gap: gap = pkt.seq.wrapping_sub(expected_seq). Cap at
MAX_GAP_FRAMES = 16 (320 ms) to avoid huge wraparound scenarios.
3. For each missing seq in the gap:
offset = (last_good_dred_seq - missing_seq) * frame_samples
if 0 < offset <= last_good_dred.samples_available():
reconstruct_from_dred + write to playout ring
bump dred_reconstructions
else:
decoder.decode_lost (classical PLC) + write + bump plc counter
4. Decode the current packet normally and write to playout ring
(unchanged from Phase 2).
5. Update expected_seq = pkt.seq.wrapping_add(1).
Profile-switch handling: when the incoming codec changes (triggering
decoder.set_profile), reset last_good_dred_seq and expected_seq to
None. The cached DRED state is tied to the old profile's frame rate
and would produce wrong offsets after the switch; starting fresh is
correct.
Decode-error fallback: the existing `Err(e) => decode_lost` branch
now also increments classical_plc_invocations so the counter
accurately reflects all PLC invocations (gap-detected AND decode-
error-triggered).
Telemetry (CallStats additions):
- stats.dred_reconstructions: u64
- stats.classical_plc_invocations: u64
Both updated on every packet arrival in the existing stats.lock()
block alongside frames_decoded/fec_recovered, so the Android UI and
JNI bridge already have these values without any further plumbing.
The periodic recv stats log now includes both counters.
Ordering note: DRED gap reconstruction happens BEFORE decoding the new
packet's audio because the playout ring is FIFO. Gap samples must be
written before the new packet's samples so temporal order is preserved.
Out-of-order late arrivals (seq < expected_seq) are naturally dropped
as stale by the gap detection (gap would be a large wraparound value
exceeding MAX_GAP_FRAMES).
Verification:
- cargo check --workspace: zero errors
- cargo test -p wzp-codec --lib: 68 passing (unchanged from Phase 3b)
- cargo test -p wzp-client --lib: 35 passing (unchanged from Phase 3b)
- cargo check -p wzp-android --lib: zero errors
- cargo test -p wzp-android cannot run on macOS host (pre-existing
-llog linker dep, unrelated). Real end-to-end verification happens
via the Android APK build on the remote Docker builder
(scripts/build-and-notify.sh).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WarzonePhone
Custom lossy VoIP protocol built in Rust. E2E encrypted, FEC-protected, adaptive quality, designed for hostile network conditions.
Quick Start
# Build
cargo build --release
# Run relay
./target/release/wzp-relay --listen 0.0.0.0:4433
# Send a test tone
./target/release/wzp-client --send-tone 5 relay-addr:4433
# Web bridge (browser calls)
./target/release/wzp-web --port 8080 --relay 127.0.0.1:4433 --tls
# Open https://localhost:8080/room-name in two browser tabs
Architecture
See docs/ARCHITECTURE.md for the full system architecture with Mermaid diagrams covering:
- System overview and data flow
- Crate dependency graph (8 crates)
- Wire formats (MediaHeader, MiniHeader, TrunkFrame, SignalMessage)
- Cryptographic handshake (X25519 + Ed25519 + ChaCha20-Poly1305)
- Identity model (BIP39 seed, featherChat compatible)
- Quality profiles (GOOD/DEGRADED/CATASTROPHIC)
- FEC protection (RaptorQ with interleaving)
- Adaptive jitter buffer (NetEq-inspired)
- Telemetry stack (Prometheus + Grafana)
- Deployment topology
Features
- 3 quality tiers: Opus 24k (28.8 kbps) / Opus 6k (9 kbps) / Codec2 1200 (2.4 kbps)
- RaptorQ FEC: Recovers from 20-100% packet loss depending on tier
- E2E encryption: ChaCha20-Poly1305 with X25519 key exchange
- Adaptive jitter buffer: EMA-based playout delay tracking
- Silence suppression: VAD + comfort noise (~50% bandwidth savings)
- ML noise removal: RNNoise (nnnoiseless pure Rust port)
- Mini-frames: 67% header compression for steady-state packets
- Trunking: Multiplex sessions into batched datagrams
- featherChat integration: Shared BIP39 identity, token auth, call signaling
- Prometheus metrics: Relay, web bridge, inter-relay probes
- Grafana dashboard: Pre-built JSON with 18 panels
Documentation
| Document | Description |
|---|---|
| ARCHITECTURE.md | Full system architecture with diagrams |
| TELEMETRY.md | Prometheus metrics specification |
| INTEGRATION_TASKS.md | featherChat integration tracker |
| WZP-FC-SHARED-CRATES.md | Shared crate strategy |
| grafana-dashboard.json | Importable Grafana dashboard |
Binaries
| Binary | Description |
|---|---|
wzp-relay |
Relay daemon (SFU room mode, forward mode, probes) |
wzp-client |
CLI client (send-tone, record, live mic, echo-test, drift-test, sweep) |
wzp-web |
Browser bridge (HTTPS + WebSocket + AudioWorklet) |
wzp-bench |
Component benchmarks |
Linux Build
./scripts/build-linux.sh --prepare # Create Hetzner VM + install deps
./scripts/build-linux.sh --build # Build release binaries
./scripts/build-linux.sh --transfer # Download to target/linux-x86_64/
./scripts/build-linux.sh --destroy # Delete VM
Tests
cargo test --workspace # 272 tests
License
MIT OR Apache-2.0
Description
Languages
Rust
78%
Kotlin
7.9%
Shell
6.7%
TypeScript
3.2%
C++
1.5%
Other
2.6%