2427630472db6e9f3e0ea70eb4b379682886ea0f
30 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2427630472 |
fix(connect): make peerLocalAddrs optional + skip handshake on direct P2P
Two regressions from Phase 5.5/5.6: 1. Room connect broken: the connect Tauri command required peerLocalAddrs as a Vec<String>, but the room-join JS path doesn't pass it (only the direct-call setup handler does). Error: "invalid args 'peerLocalAddrs' for command 'connect': command connect missing required key peerLocalAddrs". Fix: change to Option<Vec<String>>, unwrap_or_default() at usage sites. Room connect works again with zero peer addrs. 2. Direct P2P call connects but then CallEngine fails with "expected CallAnswer, got Discriminant(0)". Root cause: after the dual-path race picked a direct P2P transport, CallEngine still ran perform_handshake() on it. That handshake is a relay-specific protocol — sends a CallOffer signal and waits for CallAnswer back. On a direct QUIC connection to a phone, there's nobody running accept_handshake, so the handshake reads garbage from the peer's first media packet and errors. Fix: track is_direct_p2p = pre_connected_transport.is_some() and skip perform_handshake when true. The direct connection is already TLS-encrypted by QUIC, and both peers' identities were verified through the signal channel (DirectCallOffer/ Answer carry identity_pub + ephemeral_pub + signature). Both android and desktop branches updated. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
16793be36f |
fix(p2p): Phase 5.6 — direct-path head start + hangup propagation + media debug events
Three fixes from a field-test log where same-LAN calls were
still losing the dual-path race to the relay path, peers were
getting stuck on an empty call screen when the other side
hung up, and 1-way audio was hard to diagnose because the
GUI debug log had no media-level events.
## 1. Direct-path 500ms head start (dual_path.rs)
The race was resolving in ~105ms with Relay winning even when
both phones were on the same MikroTik LAN with valid IPv6 host
candidates. Root cause: the relay dial is a plain outbound QUIC
connect that completes in whatever the client→relay RTT is
(~100ms), while the direct path needs the PEER to also process
its CallSetup, spin up its own race, and complete at least one
LAN dial back to us. That cross-client sequence reliably takes
longer than 100ms, so relay always won.
Fix: delay the relay_fut with `tokio::time::sleep(500ms)` before
starting its connect. Same-LAN direct dials complete in 30-50ms
typically, so the head start gives direct plenty of time to win
cleanly. Users on setups where direct genuinely can't work
(LTE-to-LTE cross-carrier) pay 500ms extra on the relay fallback,
which is invisible for a call setup.
## 2. Hangup propagation via a new hangup_call command (lib.rs + main.ts)
The hangup button was calling `disconnect` which stopped the
local media engine but never sent a SignalMessage::Hangup to
the relay. The peer never got notified and was stuck on the
call screen with silent audio. My earlier fix (commit
|
||
|
|
59ce52f8e8 |
feat(p2p): Phase 3.5 dual-path QUIC race + GUI call-flow debug logs
Two features in one commit because they ship and test together:
Phase 3.5 closes the hole-punching loop and the call-flow debug
logs give the user live visibility into every step of a call so
real-hardware testing of the new P2P path is debuggable.
## Phase 3.5 — dual-path QUIC connect race
Completes the hole-punching work Phase 3 scaffolded. On receiving
a CallSetup with peer_direct_addr, the client now actually races a
direct QUIC handshake against the relay dial and uses whichever
completes first. Symmetric role assignment avoids the two-conns-
per-call problem:
- Both peers compare `own_reflex_addr` vs `peer_reflex_addr`
lexicographically.
- Smaller addr → **Acceptor** (A-role): builds a server-capable
dual endpoint, awaits an incoming QUIC session. Does NOT dial.
- Larger addr → **Dialer** (D-role): builds a client-only
endpoint, dials the peer's addr with `call-<id>` SNI. Does NOT
listen.
- Both sides always dial the relay in parallel as fallback.
- `tokio::select!` with `biased` preference for direct, `tokio::pin!`
so each branch can await the losing opposite as fallback.
- Direct timeout 2s, relay fallback timeout 5s (so 7s worst case
from CallSetup to "no media path" error).
New crate module `wzp_client::dual_path::{race, WinningPath}`
(moved here from desktop/src-tauri so it's testable from a
workspace test). `determine_role` in `wzp_client::reflect` is
pure-function and unit-tested.
### CallEngine integration
- New `pre_connected_transport: Option<Arc<QuinnTransport>>` arg
on both android + desktop `CallEngine::start` branches. Skips
the internal wzp_transport::connect step when Some. Backward-
compat: None keeps Phase 0 relay-only behavior.
- `connect` Tauri command reads own_reflex_addr from SignalState,
computes role, runs the race, passes the winning transport
into CallEngine. If ANY input is missing (no peer addr, no own
addr, equal addrs), falls back to classic relay path —
identical to pre-Phase-3.5 behavior.
### Tests (9 new, all passing)
- 6 unit tests for `determine_role` truth table in
`wzp-client/src/reflect.rs` (smaller=Acceptor, larger=Dialer,
port-only diff, equal, missing-side, symmetry)
- 3 integration tests in `crates/wzp-client/tests/dual_path.rs`:
* `dual_path_direct_wins_on_loopback` — two-endpoint test
rig, Dialer wins direct path vs loopback mock relay
* `dual_path_relay_wins_when_direct_is_dead` — dead peer
port, 2s direct timeout, relay fallback wins
* `dual_path_errors_cleanly_when_both_paths_dead` — <10s
error, no hang
## GUI call-flow debug logs
Runtime-toggled structured events at every step of a call so the
user can see where a call progressed or stalled on real hardware.
Modeled on the existing DRED_VERBOSE_LOGS pattern.
### Rust side
- `static CALL_DEBUG_LOGS: AtomicBool` + `emit_call_debug(&app,
step, details)` helper. Always logs via `tracing::info!`
(logcat always has a copy); GUI Tauri `call-debug-log` event
only fires when the flag is on.
- Tauri commands `set_call_debug_logs` / `get_call_debug_logs`.
### Instrumented steps (24 emit_call_debug sites)
- `register_signal`: start, identity loaded, endpoint created,
connect failed/ok, RegisterPresence sent, ack received/failed,
recv loop spawning
- Recv loop: CallRinging, DirectCallOffer (w/ caller_reflexive_addr),
DirectCallAnswer (w/ callee_reflexive_addr), CallSetup (w/
peer_direct_addr), Hangup
- `place_call`: start, reflect query start/ok/none, offer sent,
send failed
- `answer_call`: start, reflect query start/ok/none or privacy
skip, answer sent, send failed
- `connect`: start, dual_path_race_start (w/ role), won (w/
path), failed, skipped (w/ reasons), call_engine_starting/
started/failed
### JS side
- New `callDebugLogs: boolean` field on Settings type.
- Boot-time hydrate of the Rust flag from localStorage so the
choice survives restarts (like `dredDebugLogs`).
- Settings panel: new "Call flow debug logs" checkbox alongside
the DRED toggle.
- New "Call Debug Log" section that ONLY shows when the flag is
on. Rolling in-memory buffer of the last 200 events, rendered
as monospace `HH:MM:SS.mmm step {details}` lines with auto-
scroll and a Clear button.
- `listen("call-debug-log", ...)` subscribed at app startup,
appends to the buffer, re-renders on every event.
Full workspace test goes from 404 → 413 passing. Clippy clean
on touched crates.
PRD: .taskmaster/docs/prd_phase35_dual_path_race.txt
Tasks: 61-69 all completed
Next: APK + desktop build carrying everything — Phase 2 NAT
detect, Phase 3 advertising, Phase 3.5 dual-path + call debug
logs, plus the earlier Android first-join diagnostics — so the
user can validate the P2P path on real hardware with live
per-step visibility into where any failures happen.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
7e7968b2f9 |
diag(android-engine): first-join no-audio ordering instrumentation
Adds a single call_t0 = Instant::now() at the top of the Android
CallEngine::start path, threaded through send + recv tasks as
send_t0 / recv_t0, and tags the following milestones with
t_ms_since_call_start so we can build a clean side-by-side log of
first-call vs rejoin:
1. QUIC connection established
2. handshake complete
3. wzp-native audio_start returned (+ how long audio_start itself took)
4. send task spawned
5. send: first full capture frame read (+ short_reads_before count)
6. send: first non-zero capture RMS
7. recv task spawned
8. recv: first media packet received
9. recv: first successful decode
10. recv: first playout-ring write
Combined with the existing C++-side cb#0 logs in
crates/wzp-native/cpp/oboe_bridge.cpp ("capture cb#0", "playout
cb#0") this gives us full-pipeline ordering with no native-side
changes needed.
PRD: .taskmaster/docs/prd_android_first_join_no_audio.txt
Task: 32 (first task in the chain — diagnostics before any fix
attempts so we know which of the 5 suspect causes is real).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
578ff8cff4 |
feat(debug): GUI toggle for DRED verbose logs + macOS mic permission
DRED verbose logs (off by default — keeps logcat clean in normal use): - wzp-codec: DRED_VERBOSE_LOGS atomic flag with dred_verbose_logs() / set_dred_verbose_logs() helpers - opus_enc: gate "DRED enabled" + libopus version logs behind the flag - desktop/src-tauri/engine.rs: gate DredRecvState parse log, reconstruction log, classical PLC log, and DRED-counter fields in the Android recv heartbeat (non-verbose path still logs basic recv stats) - Tauri commands set_dred_verbose_logs / get_dred_verbose_logs - Settings panel gets a "DRED debug logs (verbose, dev only)" checkbox; preference persists in wzp-settings localStorage and is pushed to Rust on save and on app boot macOS mic permission: - Add desktop/src-tauri/Info.plist with NSMicrophoneUsageDescription. Without it, modern macOS silently denies CoreAudio capture for ad-hoc-signed Tauri builds — capture starts but every callback hands you zeros. Symptom: phones could not hear desktop client, desktop could still hear phones (playout has no TCC gate). The Tauri 2 bundler auto-merges this file into WarzonePhone.app's Contents/Info.plist on the next build, so first launch will pop the standard mic prompt. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
16890576fb |
feat(observability): logcat-visible DRED proof of life on Android
Adds enough INFO-level logging that an opus-DRED-v2 APK on Android can
be verified end-to-end by reading logcat alone — no debugger, no
Prometheus, no telemetry pipeline required. Three observation points:
1. Encoder construction (opus_enc.rs)
- Bumped the "DRED enabled" log from debug! to info! so the
per-call DRED config is in logcat by default. Each call's first
OpusEncoder construction logs codec, dred_frames, dred_ms,
loss_floor_pct.
- Added a one-shot static OnceLock that logs `opusic_c::version()`
the first time an OpusEncoder is built in the process. This is
the smoking gun for "is the new libopus actually loaded" — pre-
Phase-0 audiopus shipped libopus 1.3 with no DRED, post-Phase-0
should print 1.5.2 here.
2. DRED state ingest (DredRecvState::ingest_opus in
desktop/src-tauri/src/engine.rs)
- First successful parse on a call logs immediately so we can see
"DRED is on the wire" in logcat.
- Subsequent parses sample every 100th to confirm steady-state
samples_available without drowning the log.
- New parses_total / parses_with_data counters track the parse
rate vs the success rate (a packet without DRED in it returns
`available == 0`, so a low ratio means the encoder isn't
emitting DRED bytes).
3. DRED reconstruction events (DredRecvState::fill_gap_to)
- Every DRED reconstruction logs at INFO with missing_seq,
anchor_seq, offset_samples, offset_ms, samples_available,
gap_size, and the running total. These events are rare on a
clean network and we want to know exactly which gap was filled.
- First three classical PLC fills + every 50th thereafter log so
we can see when DRED couldn't cover a gap (offset out of range,
no good state, or reconstruct error).
4. Recv heartbeat (Android start() in engine.rs)
- Existing 2-second heartbeat now includes dred_recv,
classical_plc, dred_parses_with_data, dred_parses_total
so a steady-state call shows the cumulative counters in
logcat without parsing.
How to verify on a real call:
adb logcat -s 'RustStdoutStderr:*' | grep -i 'dred\|libopus version'
Expected output sequence on a successful Opus call:
- "linked libopus version libopus_version=libopus 1.5.2-..." (once per process)
- "opus encoder: DRED enabled codec=Opus24k dred_frames=20 dred_ms=200 loss_floor_pct=15" (per call)
- "DRED state parsed from Opus packet seq=N samples_available=4560 ms=95 ..." (after first DRED-bearing packet)
- "recv heartbeat (android) ... dred_recv=0 classical_plc=0 dred_parses_with_data=58 dred_parses_total=58" (every 2s)
If you see "linked libopus version libopus 1.3" — the FFI swap didn't
take. If dred_parses_with_data stays at 0 while dred_parses_total
climbs — the sender isn't emitting DRED (check the encoder's loss
floor and the receiver's libopus version). If gaps trigger
"classical PLC fill" instead of "DRED reconstruction fired" —
DRED state coverage is too small for the observed loss pattern,
and the loss floor or DRED duration policy needs tuning.
Verification:
- cargo check -p wzp-codec -p wzp-client: 0 errors
- cargo check -p wzp-desktop: 0 Rust errors (only the pre-existing
tauri::generate_context!() proc macro panic on missing ../dist
which fires at host check time, irrelevant on the remote build)
- cargo test -p wzp-codec --lib: 69 passing (no regressions)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
dfbe21fe6e |
feat(tauri-engine): Phase 3b/3c re-port — DRED reconstruction on the live Tauri mobile engine
The original Phase 3b landed on wzp-client/CallDecoder and Phase 3c
landed on wzp-android/src/engine.rs. Both of those are DEAD CODE on
feat/desktop-audio-rewrite: the legacy Kotlin app in android/app/ is
not built by the Tauri mobile pipeline, and the Tauri engine bypasses
CallDecoder by calling wzp_codec::create_decoder directly.
The live Android call engine lives at desktop/src-tauri/src/engine.rs
with two `pub async fn start<F>` functions — one cfg-gated on Android
(Oboe via wzp-native) and one for desktop (CPAL). Both recv tasks
were using `let mut decoder = wzp_codec::create_decoder(...)` which
returns `Box<dyn AudioDecoder>` and doesn't expose the inherent
`reconstruct_from_dred` method.
Changes:
New helper struct `DredRecvState` at the top of engine.rs, wrapping:
- DredDecoderHandle (libopus DRED side-channel parser)
- DredState scratch (for parse_into)
- DredState last_good (cached valid state, swapped on success)
- last_good_seq: Option<u16> (DRED anchor sequence)
- expected_seq: Option<u16> (for gap detection)
- dred_reconstructions / classical_plc_invocations counters
With three methods:
- ingest_opus(seq, payload): parse DRED, swap on success
- fill_gap_to(decoder, current_seq, frame_samples, scratch, emit):
detect gap back from expected_seq, reconstruct each missing
frame via DRED if state covers it, fall through to classical
decoder.decode_lost() when it doesn't. Calls emit() once per
frame with a slice the caller uses for AGC + playout write.
- reset_on_profile_switch(): invalidate tracking when codec changes
Both recv tasks (Android @ ~line 297 and desktop @ ~line 907):
- Decoder type changed from `Box<dyn AudioDecoder>` via
`wzp_codec::create_decoder` to concrete `AdaptiveDecoder::new(profile)`
so we can call the inherent reconstruct_from_dred method.
- Added `use wzp_proto::traits::AudioDecoder;` at the top of
engine.rs to bring decode/decode_lost/set_profile trait methods
into scope on the concrete type.
- New `current_profile` local alongside `current_codec` (used for
frame_duration lookups that drive the DRED sample offset math).
- On codec/profile switch, call dred_recv.reset_on_profile_switch()
because the cached DRED state is tied to the old profile's
frame rate.
- For each arriving Opus source packet:
1. dred_recv.ingest_opus(seq, payload) — parse DRED
2. dred_recv.fill_gap_to(...) — detect gap and reconstruct
missing frames, each emitted through a closure that does
AGC + playout write (wzp_native on Android, playout_ring
on desktop)
3. Normal decoder.decode() fallthrough for the current packet
(unchanged)
- Codec2 packets skip the DRED path entirely (is_opus() gate) —
libopus can't reconstruct Codec2 audio.
Ordering invariant: gap reconstruction writes to playout BEFORE the
current packet's decoded audio, preserving temporal order since the
playout ring is FIFO. The closure captures the `spk_muted` flag once
before the gap loop to avoid mid-gap-fill state changes.
Kept `crates/wzp-android/src/engine.rs` and `crates/wzp-android/src/
stats.rs` from the earlier Phase 3c commit as-is — they're dead code
on feat/desktop-audio-rewrite but harmless, and deleting them would
diverge this branch from an independently-useful intermediate state.
The old Phase 3c commit (
|
||
|
|
2fd94651e4 |
fix(desktop): direct calls used wrong identity file — mac identity mismatch
The non-Android branch of CallEngine::start loaded the seed from \$HOME/.wzp/identity directly, while register_signal in lib.rs goes through the shared load_or_create_seed() helper which resolves via APP_DATA_DIR → Tauri's app_data_dir(). On macOS those are two completely different files: register_signal → ~/Library/Application Support/com.wzp.desktop/.wzp/identity CallEngine::start (old) → ~/.wzp/identity On a fresh install they end up holding two different random seeds. Register and CallEngine then derive two different fingerprints from those seeds, and when a direct call comes in the relay routes it to "you" under the register_signal fingerprint, but once CallEngine tries to join the call-* room it advertises a DIFFERENT fingerprint — which fails the call_registry ACL check on the relay side (only the two authorised participants of a call can join its room). Silent hang, the call never completes. Android hit this bug earlier in the week and was fixed by switching its CallEngine::start branch to `crate::load_or_create_seed()`. Backport the same single-line change to the desktop branch so both platforms share one identity source of truth. Also bring the desktop branch up to parity with the android branch on diagnostic logging: - log CallEngine::start entry with relay/room/alias/quality/has_reuse - log endpoint.local_addr on reuse / create - log "QUIC connection established, performing handshake" between connect() and perform_handshake() so a hang at either step is immediately localisable - map_err all three potential failure points (create_endpoint, connect, perform_handshake) to an explicit error! trace |
||
|
|
cfa9ff67cf |
fix(android-audio): VoIP mode + speakerphone + debug PCM recorder
Build
|
||
|
|
96be740fd9 |
diag(android-audio): aggressive logging across the whole Oboe pipeline
User confirmed: mac hears android, android does not hear mac. So Oboe
capture works end-to-end but Oboe playout on Android silently drops
audio even though QUIC forwards the packets. Archaeology on the legacy
wzp-android crate also revealed that the "last known good" Android audio
path NEVER used Oboe in production — it used Kotlin AudioRecord +
AudioTrack via JNI, and cpp/oboe_bridge.cpp was dead code. So every time
we've "tested" Oboe end-to-end this week was the first production use,
and any of its config knobs could be the bug.
Instrumenting every stage of the pipeline so one smoke-test log dump can
isolate the layer at fault:
C++ (oboe_bridge.cpp)
- Log the ACTUAL stream parameters after openStream for both capture
and playout (sample rate, channels, format, framesPerBurst,
framesPerDataCallback, bufferCapacityInFrames, sharing, perf mode).
Oboe may silently override values we requested — e.g. if we ask for
48kHz mono but the device gives us 44.1kHz stereo our 960-sample
frames are the wrong duration and the pipeline drifts.
- Capture callback: on cb#0 log sample range+RMS of the first frame
to prove we get real mic data (not zeros). Every 50 callbacks
(~1s at 20ms burst) log calls, numFrames, ring available_write,
bytes actually written, ring_full_drops, total_written.
- Playout callback: on cb#0 log numFrames + ring state. On the FIRST
non-empty read log sample range+RMS so we can tell if the samples
coming out of the ring are real audio or zeros. Every 50 callbacks
log calls, nonempty count, numFrames, ring available_read,
underrun_frames, total_played_real.
Rust wzp-native (src/lib.rs)
- wzp_native_audio_write_playout now logs the first 3 writes and then
every 50th: in_len, written, sample range, RMS, ring write/read
cursors before, available_read and available_write after. Reveals
ring-overflow and whether the engine is actually handing us audio.
- Minimal android logcat shim via __android_log_write extern — no
new crate dependency.
- AudioBackend grows a `playout_write_log_count` AtomicU64 to gate
the write-side log throttle.
Rust engine.rs (android branch)
- Recv task: log sample range + RMS for the first 3 decoded PCM
frames and then every 100th. Reveals whether decoder.decode is
producing real audio or silent buffers.
- Recv task: if audio_write_playout returns fewer samples than we
handed it (partial write → ring nearly full) warn about it in the
first 10 frames.
- Recv heartbeat every 2s: recv_fr, decoded_frames, last_decode_n,
last_written, written_samples, decode_errs, codec.
Expected flow in a healthy log:
capture cb#0: numFrames=960 range=[-1200..900] rms=180 ← mic OK
capture stream opened: actualSR=48000 Ch=1 ... ← no override
playout stream opened: actualSR=48000 Ch=1 ...
CallEngine::start invoked ... → connected → audio started
recv: first media packet received ...
recv: decoded PCM sample range decoded_frames=1 range=[-300..250] rms=92
playout WRITE #0: in_len=960 written=960 range=[-300..250] rms=92
playout FIRST nonempty read: to_read=960 range=[-300..250] rms=92
playout heartbeat: calls=50 nonempty=50 underrun=0 ...
recv heartbeat: decoded_frames=100 last_written=960 ...
If any of those are missing/zero we know the exact stage to fix.
|
||
|
|
8c4d640f89 |
fix(android): playout Usage::Media + relay CallSetup advertises real IP
Three real bugs, one smoke-test session's worth of progress. 1. RELAY: wrong advertised addr in CallSetup The direct-call CallSetup computed `relay_addr = addr.ip()` where `addr = connection.remote_address()` — i.e. the CLIENT'S IP, not the relay's. So the relay was telling both parties "the call room is at the answerer's IP:4433", which meant each client dialed either the other client (no server listening) or themselves. Both endpoint.connect calls hung forever and the call never happened. Fix: compute the relay's own advertised IP once at startup. If the listen addr is 0.0.0.0, probe the primary outbound interface via the classic UDP-bind-and-connect(8.8.8.8:80) trick to discover the LAN IP the OS would use to reach external hosts. Thread the resulting advertised_addr_str into the CallSetup sender for both parties. 2. RELAY: accept loop serialized QUIC handshakes Previously the main accept loop called `wzp_transport::accept` which did both `endpoint.accept().await` AND `incoming.await` (the server- side QUIC handshake). A single slow handshake therefore blocked every subsequent client from being accepted. Unroll the helper here and move `incoming.await` into the per-connection spawned task, so every handshake runs in parallel. Also log "accept queue: new Incoming", "QUIC handshake complete", and "QUIC handshake failed" so we can tell immediately whether a client's packets are reaching the relay at all. 3. ANDROID: playout was routed to the silent in-call stream The Oboe playout stream was configured with Usage::VoiceCommunication, which routes to the Android in-call earpiece stream. That stream is silent unless the Activity has called AudioManager.setMode( IN_COMMUNICATION) and, even then, only the earpiece/BT headset get audio (not the loud speaker). Result: android→mac calls worked because mac had a normal media output, but mac→android calls were silent even though packets flowed through the relay just fine. Switch to Usage::Media + ContentType::Speech so Oboe routes to the loud speaker and uses the media volume slider. A later polish step will wire setMode + setSpeakerphoneOn from MainActivity.kt so we can go back to VoiceCommunication for AEC and proximity-sensor routing. Plus: heartbeat tracing every 2s in the send/recv tasks — frames_sent, last_rms, last_pkt_bytes, short_reads on the send side; decoded_frames, last_decode_n, last_written, decode_errs on the recv side. Will make the next "no sound" regression trivial to localize. |
||
|
|
49f101d785 |
fix(android): reuse signal endpoint for direct-call media connection
Direct-call accept hangs forever at the QUIC handshake on Android. Logs
from
|
||
|
|
d7b37a5749 |
diag: tracing for direct-call signal loop + CallEngine::start stages
User reports tapping "answer" on an incoming direct call does nothing
visible, and suspects the same may affect desktop. The signal recv loop
had no tracing at all, so we can't tell whether CallSetup is being
received, whether the recv loop died silently, or whether
CallEngine::start is failing between "identity loaded" and
"connected to relay, handshake complete".
- register_signal recv loop now logs every message type with fields
(CallRinging, DirectCallOffer, DirectCallAnswer, CallSetup, Hangup,
unhandled), plus a warn! on recv errors and a final warn when the
loop exits.
- place_call / answer_call commands log entry + success / error. The
answer_call error path logs the underlying send_signal error so we
can see it in logcat instead of only in the JS error toast.
- CallEngine::start android branch logs relay/room/alias on entry,
logs "endpoint created, dialing relay" between create_endpoint and
connect, "QUIC connection established, performing handshake" between
connect and perform_handshake, and promotes all three potential
failures to explicit error! logs so a silent hang / error becomes
visible in logcat.
No functional changes — pure diagnostics. Stacks on
|
||
|
|
5beea7de40 |
phase 3(android): unify connect/disconnect/toggle_*/get_status commands
Step 3 of the Tauri Android rewrite was still returning "audio backend not
yet wired on Android (step 3)" because the cfg-gated Android stubs for
connect/disconnect/toggle_mic/toggle_speaker/get_status were shadowing the
real commands. Now that CallEngine::start() has a real Android body (phase
3, commit
|
||
|
|
fdbe502524 |
phase 3(android): wire CallEngine::start to wzp-native audio FFI
Replaces the Android-side CallEngine::start() stub with a real implementation that mirrors the desktop start() body but routes all PCM through the standalone wzp-native cdylib loaded at startup via libloading instead of using CPAL. - desktop/src-tauri/src/wzp_native.rs: new module with a static OnceLock<libloading::Library> + cached raw fn pointers for every symbol we need (version, hello, audio_start/stop, read_capture, write_playout, is_running, capture/playout_latency_ms). init() resolves everything once at startup; accessors return default values if init() never ran. - desktop/src-tauri/src/lib.rs: drop the inline dlopen smoke test, add `mod wzp_native;` behind target_os="android", and invoke wzp_native::init() from the Tauri setup() callback so the library is loaded + all symbols cached before any CallEngine can touch audio. - desktop/src-tauri/src/engine.rs: the Android #[cfg] branch of CallEngine::start() now does the full QUIC handshake + signal loop + Opus send/recv tasks, calling wzp_native::audio_start() / audio_read_capture() / audio_write_playout() instead of the desktop CPAL rings. SyncWrapper now holds a placeholder Box<()> on Android because the audio backend lives in a process-global singleton inside libwzp_native.so rather than being owned per-engine. Next step: build #39 on the remote docker builder and smoke-test on Pixel 6 that the Connect button in the UI successfully brings up Oboe and streams audio through the dlopen boundary. |
||
|
|
19fd3dd9cc |
step C fix: ungate wzp_proto imports used by resolve_quality() on Android
Build #20 failed to compile on Android because I over-gated the wzp_proto imports to non-Android. resolve_quality() is compiled on every platform (it's outside the CallEngine impl) and references QualityProfile + CodecId — both platform-independent types from wzp_proto. Move those back to an unconditional import. tracing stays gated (only the desktop start() body logs; the Android stub is silent). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
c69195fe06 |
step C(android): compile engine.rs on Android with a stub CallEngine::start
Third incremental variable. Previously the engine module was cfg-gated out of the Android build entirely (`#[cfg(not(target_os = "android"))] mod engine;` in lib.rs). Now it's always compiled, so any link-time effect of having engine.rs in the compilation unit can be measured against the working baseline from build #19. Changes kept deliberately small: - lib.rs: drop the cfg gate on `mod engine;`. `use engine::CallEngine` stays gated because the Android-specific connect/disconnect/... stubs in lib.rs don't reference the type. - engine.rs: the `wzp_client::{audio_io, call}` imports + CodecId + QualityProfile are gated to non-Android (they require the `audio` feature on wzp-client which Android doesn't pull in). On Android we keep only the MediaTransport import for transport.close(). The impl block now has two `start()` methods: the full CPAL-backed one for desktop, and a 6-line Android stub that returns `Err("audio engine not yet wired on Android")` so attempts to `connect` from the UI fail cleanly. Goal: verify that linking in the compiled engine module (plus the types it references) on Android doesn't regress the working baseline. Home screen should still render and register_signal should still work. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
530993854f |
revert(android): roll back to build #6 (35642d1) — pre-oboe known-good state
Spent 10+ builds chasing a __init_tcb+4 / pthread_create SIGSEGV after adding the oboe audio backend. Every "fix" made things worse. Reverting all Android-specific files to the state at |
||
|
|
b314138caf |
feat(android): oboe/AAudio audio backend + runtime mic permission (step 3)
This is the big one — the Tauri Android app now has a real audio stack
capable of full-duplex VoIP, reusing the proven C++ Oboe bridge from the
legacy wzp-android crate.
Architecture:
- desktop/src-tauri/cpp/ — copies of oboe_bridge.{h,cpp}, oboe_stub.cpp,
and getauxval_fix.c from crates/wzp-android/cpp/. build.rs clones
google/oboe@1.8.1 into OUT_DIR and compiles the bridge + all Oboe
sources as "oboe_bridge" static lib, linking against shared libc++
(static would pull broken libc stubs that SIGSEGV in .so libraries).
- src/oboe_audio.rs — Rust side: an SPSC ring buffer matching the C++
bridge's AtomicI32 layout, plus OboeHandle::start() which returns
(capture_ring, playout_ring, owning_handle). The ring exposes the same
(available / read / write) methods as wzp_client::audio_ring::AudioRing
so CallEngine treats both backends interchangeably.
- src/engine.rs — compiled on every platform now. A cfg-switched type
alias picks wzp_client::audio_ring::AudioRing on desktop and
crate::oboe_audio::AudioRing on Android. The audio setup block has
three branches: VPIO/CPAL on macOS, CPAL on Linux/Windows, Oboe on
Android. Send/recv tasks are identical across platforms.
- src/lib.rs — removes all the "step 3 not done" Android stubs. The
engine module is no longer cfg-gated; connect / disconnect / toggle_mic
/ toggle_speaker / get_status are single implementations used by both
desktop and Android. Identity path resolves via app.path().app_data_dir()
from the Tauri setup() callback (already wired in step 1).
Runtime mic permission:
- scripts/build-tauri-android.sh now injects RECORD_AUDIO + MODIFY_AUDIO_
SETTINGS into gen/android/app/src/main/AndroidManifest.xml after init,
and overwrites MainActivity.kt with a version that calls
ActivityCompat.requestPermissions in onCreate. This is idempotent:
every build re-applies the patches so tauri re-init can't regress them.
Cargo.toml:
- cc is now an unconditional build-dep (build.rs runs on the host, so
target-gating build-deps doesn't work).
- wzp-client is now a dep on every platform. On Android it gets default
features only (no "audio"/"vpio") so CPAL isn't dragged in — oboe_audio
provides the capture/playout rings instead.
- tracing-android is added on Android so tracing events flow into logcat.
build.rs also gained embedded git hash (WZP_GIT_HASH) capture, which is
shown under the fingerprint on the home screen — already committed in
|
||
|
|
395a0c557e |
feat: TX/RX codec badges on desktop call screen
Desktop now shows codec badges like Android: - Green TX badge: e.g. "Opus64k" - Blue RX badge: e.g. "Opus24k" Displayed in the stats line below the call controls. Engine tracks tx_codec (set on encoder init) and rx_codec (updated from incoming packet headers). Passed through EngineStatus → CallStatus → frontend. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
da593f9510 |
feat: relay-grouped participant rendering + relay_label in protocol
RoomParticipant now has optional relay_label field. Desktop client groups participants by relay: "This Relay" (green dot) for local, peer label (blue dot) for federated. Shows all relays in the chain including intermediate ones. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
a8c2011445 |
feat: add Opus 32k/48k/64k studio quality tiers
Adds three new codec IDs (Opus32k=6, Opus48k=7, Opus64k=8) and corresponding STUDIO_32K, STUDIO_48K, STUDIO_64K quality profiles. All use 20ms frames with minimal FEC (10%) for maximum quality on good networks. Updated across: wire protocol (codec_id.rs), encoder/decoder (opus_enc/dec.rs), adaptive codec switch (call.rs), CLI (--profile studio-64k), desktop engine + UI slider (8 quality levels from Studio 64k green to Codec2 1.2k red). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
369347ce54 |
fix: remove unused FRAME_SAMPLES_20MS constant in desktop engine
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
85c2146760 |
feat: quality profile selection in desktop settings
Adds a Quality dropdown (Auto / Opus 24k / Opus 6k / Codec2 3.2k / Codec2 1.2k) to both the connect screen and settings panel. The selected profile is passed through to the engine which configures the encoder and decoder accordingly. The desktop engine recv path now auto-switches the decoder codec when incoming packets use a different codec than expected, enabling cross-codec interop between clients on different quality settings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
dddf5d2e2d |
feat: relay ping with RTT display, fix dead_code warning
Some checks failed
Build Release Binaries / build-amd64 (push) Has been cancelled
- New ping_relay Tauri command: QUIC connect with 3s timeout, returns RTT ms - Relay status shown next to input field: "42ms" (green) or "offline" (red) - Auto-pings on app startup and debounced on relay input change - Fix SyncWrapper dead_code warning with #[allow(dead_code)] Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
21f5b24cbf |
fix: keep audio handles alive for call duration, fix Send+Sync
Some checks failed
Build Release Binaries / build-amd64 (push) Failing after 3m39s
The VPIO/CPAL audio handles were dropped at the end of start(), killing the audio unit immediately. Audio I/O stopped working after the first frame. - Store audio handle in CallEngine via SyncWrapper - Drop MutexGuard before returning from status() (Send future) - Audio streams now live for the entire call duration Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
9b733010ab |
fix: blocking_lock panic in status(), fingerprint copy-to-clipboard
Some checks failed
Build Release Binaries / build-amd64 (push) Failing after 3m13s
- Change status() from blocking_lock to async lock().await — fixes "Cannot block the current thread from within a runtime" panic that froze the call timer and broke audio - Click fingerprint to copy to clipboard (both connect and settings screens) - Show "Copied!" feedback on click Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
80d5bd7628 |
fix: survive QUIC congestion — drop packets instead of killing send task
Some checks failed
Build Release Binaries / build-amd64 (push) Failing after 3m14s
send_datagram() returns Err(Blocked) when the QUIC congestion window is full. This is transient — the window reopens once ACKs arrive. Previously, all send paths treated this as fatal (break/return), which killed the send task and cascaded via tokio::select! to kill the entire call. Now: log warning, drop the packet, continue. Brief audio glitch (20-100ms) instead of complete call death. FEC on the receiver side recovers most dropped packets. Fixed in: - CLI run_live send task (continue + error counter) - CLI run_file_mode send paths (2 locations) - Desktop engine send task Also hardened recv tasks: transient errors (non-closed/reset) are survived instead of causing exit. Matches the fix applied to Android client (engine.rs). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
f726f8cfa4 |
feat: desktop GUI enhancements — audio level, call timer, VPIO, settings
Some checks failed
Build Release Binaries / build-amd64 (push) Failing after 3m47s
- Audio level meter with log-scale RMS visualization - Call duration timer - VPIO (OS AEC) wired through to engine with fallback to CPAL - "You" badge on own participant entry - Recent rooms list (click to reuse) - Enter key to connect from form fields - Improved dark theme with pulse animation on status dot - Settings persistence via localStorage (relay, room, alias, AEC, recent rooms) - Fingerprint display on connect screen - Keyboard shortcuts skip input fields Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
e468454464 |
feat: Tauri desktop GUI app with call engine
Some checks failed
Build Release Binaries / build-amd64 (push) Failing after 3m27s
- New desktop/ directory with Tauri v2 + Vite + TypeScript - Rust backend: CallEngine wrapping wzp-client audio + transport - Web frontend: connect screen, in-call screen with participants, mic/speaker mute, keyboard shortcuts (m/s/q) - Dark theme UI, settings persistence via localStorage - Platform-aware --os-aec: warns on Windows/Linux (not yet implemented) - Workspace updated to include desktop/src-tauri Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |