Pass AppHandle into run_signal_task so it can emit call-debug events
and Tauri events directly. On each RoomUpdate:
- emit connect:media:room_update debug event with participant list
- emit call-event/participants Tauri event for JS-side diagnostics
Helps diagnose whether room join and participant sync is working
independently of audio startup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
spawn_blocking uses arbitrary thread-pool threads that don't have the
Android JNI context initialized, causing ndk_context::android_context()
to panic. Switch to run_on_main_thread (where the context is always
valid) via a oneshot channel, with a 2s timeout. Panic is caught and
forwarded as an Err so the debug log captures it rather than crashing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The JNI call into AudioManager.setMode() was running directly on the
tokio async thread. If the Android audio policy service is slow (e.g.
immediately after mic permission grant), this could block the runtime.
Moved to spawn_blocking with a 2s timeout; timeout and panic cases are
logged as connect:audio_mode_timeout / connect:audio_mode_panic debug
events and treated as non-fatal (we continue to audio_start).
Also removes the has_record_audio_permission call from the preflight
debug event — it was a redundant JNI round-trip that added latency and
is now captured separately in the preflight_start event context.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The legacy event_cb("connected") call between handshake and audio
preflight was a no-op on the frontend (it enters voice only after the
command resolves) but added noise to failing traces. Replaced with a
connect:connected_event_skipped debug event and added an explicit
connect:android_audio_preflight_start marker so the debug log shows a
clear boundary between handshake completion and audio startup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- engine.rs: wrap spawn_blocking(audio_start) in an 8s tokio timeout so
the connect command fails fast with a clear error if the Oboe HAL
never returns, instead of blocking the JS 45s timer
- lib.rs: emit_call_debug now always forwards connect: and
register_signal: steps to the JS overlay regardless of the debug-logs
toggle — needed because app-data clears reset the toggle to false,
making join failures invisible on first install
- main.ts: JS timeout bumped to 45s (Rust 8s fires first); timeout
message now includes last native connect: step so the toast is
actionable without opening the debug log
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add emit_call_debug events at every step of the Android connect/audio
path so failures are visible in the Settings debug log without needing
adb logcat:
- connect:handshake_start/done/failed (with timing)
- connect:android_audio_preflight (wzp_native loaded + RECORD_AUDIO
permission check via new has_record_audio_permission() JNI helper)
- connect:audio_stop_start/done
- connect:audio_mode_start/done/failed
- connect:audio_start_start/failed/panic/done (with oboe error code)
- connect:reuse_endpoint (endpoint reuse diagnostic)
Also adds has_record_audio_permission() to android_audio.rs — used in
the preflight event to confirm the OS has granted mic access before
wzp_oboe_start is called.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- oboe_bridge.cpp: return -6 (instead of silent 0) when streams do not
reach Started within the 2s poll deadline; also clean up streams on
that path so a retry can succeed
- main.ts: shared connectWithTimeout() so room-join and direct-call
auto-connect both get the 15s JS timeout; shared errorMessage() so
Tauri error objects don't show as [object Object] in toasts
- docs/bugs/001-android-join-voice-hang.md: comprehensive bug report
with root cause chain, evidence, return code table, and next steps
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
wzp_oboe_start is a sync FFI call that can block the OS thread
indefinitely waiting on the Android audio HAL. Calling it directly
from an async context freezes all tokio tasks including Rust-side
timeouts. Fix: run it via spawn_blocking so tokio stays responsive.
Also add a 15s Promise.race timeout in JS so a frozen audio_start
surfaces as "connect timed out — check audio permissions" instead of
the join button staying stuck in "Connecting…" forever.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- handshake.rs: add 10s timeout on recv_signal() waiting for CallAnswer —
previously hung forever if relay didn't respond, making join button
disappear with no feedback
- main.ts: keep join button visible + show "Connecting…" state instead of
hiding it before the await; button restores correctly on error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- main.ts: add showToast() — surfaces Rust connect errors that were
previously swallowed silently (key for diagnosing "never joins calls")
- main.ts: connectPending flag prevents double-tap race on Join Voice
and CallSetup auto-connect; hides button while connect is in-flight
- build-linux-docker.sh: send ntfy notification per-server after each
relay deploy (shows host + version deployed)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Convert Hold/Unhold/Mute/Unmute/TransferAck from unit variants to struct
variants with `version: u8` (serde default = 2). Every SignalMessage
variant now carries a version field, enabling future semantic versioning
and clean rejection of deprecated variants during federation routing.
305 tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deploys wzp-relay to both relay servers after building:
- manwe@manwehs:/home/manwe/wzp (tmux session 5)
- manwe@pangolin.manko.yoga:/home/manwe/wzp-linux (tmux session 0)
Captures current relay args from /proc, stops via tmux C-c, restarts
with same args. Also fixes hardcoded branch default to use current git branch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
C2: Add EncryptingTransport wrapper — all media I/O now goes through
ChaChaSession encrypt/decrypt before hitting the QUIC datagram path.
cli.rs run_live/run_silence/run_file_mode accept Arc<dyn MediaTransport>
and receive a wrapped transport after the handshake.
C3: Wire VideoScorer::observe() into both plain and trunked forwarding
loops in room.rs. Packets from participants with Abusive verdict are
dropped before forwarding. last_bwe_kbps tracked from quality reports.
M4: Widen FEC repair symbol index from u8 to u16 throughout
(FecEncoder::generate_repair, FecDecoder::add_symbol, all call sites in
call.rs, bench.rs, pipeline.rs, wzp-android). Eliminates theoretical
wrapping when num_source + repair_count > 255.
M5: Track last_encrypt_timestamp in ChaChaSession. debug_assert in
encrypt() that timestamp is non-decreasing across calls (including post-
rekey). complete_rekey() explicitly preserves last_encrypt_timestamp to
prevent accidental timestamp reset regressions.
583 tests passing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace buffer.index() with buffer.buffer_mut()/buffer.buffer() (ndk 0.9 RAII API)
- Replace queue_input_buffer_by_index/release_output_buffer_by_index with
queue_input_buffer/release_output_buffer taking buffer objects
- Fix MaybeUninit<u8> copy using .write() instead of copy_from_slice
- Add BITRATE_MODE_CBR and AMEDIACODEC_BUFFER_FLAG_KEY_FRAME local constants
(removes ndk_sys dependency for these values)
- Add unsafe impl Send for all six MediaCodec wrapper structs
- Pin @tauri-apps/api to ^2.11 to match Cargo.lock tauri 2.11.1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Audit:
- docs/AUDIT-2026-05-25.md: full protocol audit covering 8 findings
(4 critical, 2 high, 5 medium, 4 low) with code references and fix
effort estimates
- vault/Audit/Tasks.md: Obsidian Tasks plugin file tracking all audit
items with priorities, due dates, and per-step checklists
Architecture docs updated for Wire format v2 and Wave 5/6 features:
- ARCHITECTURE.md: adds wzp-video to dependency graph and project
structure; wire format updated to v2 (16B header, 5B MiniHeader);
relay concurrency section corrected (DashMap+RwLock is current, not
a future optimization); test count 571→702; Android note
- PROGRESS.md: Wave 5 and Wave 6 sections appended; test count 372→702;
current status and open blockers as of 2026-05-25
- ROAD-TO-VIDEO.md: implementation status table inserted (✅/🟡/🔴/🔲
per phase); 6-step critical path to first video call
- WZP-SPEC.md: MediaHeader updated to v2 (16B byte-aligned); MiniHeader
updated to 5B with seq_delta; codec IDs 9-12 added (H.264/H.265/AV1);
version negotiation section added
Obsidian vault (vault/):
- 114 files across Architecture/, PRDs/, Reports/, Android/,
Reference/, Audit/ with YAML frontmatter
- 00 - Home.md index note with wiki links
- .obsidian/app.json config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous scheme built ChaCha20-Poly1305 nonces from an internal
recv_seq counter that incremented once per decrypt() call. Under
in-order delivery recv_seq stayed in sync with the sender's send_seq,
but any out-of-order or lost packet caused them to diverge permanently —
every subsequent packet then used the wrong nonce and AEAD decryption
failed for the rest of the session.
Fix: parse the MediaHeader at the top of both encrypt() and decrypt()
and use header.seq as the nonce input. Both sides now derive the nonce
from the same wire field, surviving reordering by construction.
send_seq / recv_seq are kept as pure packet counters for the rekey
interval trigger; they no longer affect nonce derivation.
All tests updated to pass valid v2 MediaHeader bytes instead of raw
byte literals (the new code requires a parseable header for nonce
derivation). New test decrypt_survives_out_of_order_delivery encrypts
5 packets and delivers them out of order (indices 0,2,1,4,3); this
test would have failed under the old counter-based scheme.
Fixes audit finding C1 from AUDIT-2026-05-25.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
shiguredo_dav1d and shiguredo_svt_av1 build scripts panic with
'unsupported target: os=android, arch=aarch64'. The AV1 SW fallback
is only needed on macOS / Linux desktop — Android uses MediaCodec
for AV1 anyway.
- Cargo.toml: AV1 SW deps moved under cfg(not(target_os = "android"))
- lib.rs: cfg-gate the dav1d and svt_av1 modules and re-exports
- factory.rs: on Android, Av1Main paths return NotInitialized when
HW MediaCodec is also unavailable (only path on Android)
- factory tests: assert NotInitialized on Android, Ok elsewhere
Unblocks T4.3.1.1 (Android target-compile of wzp-video / mediacodec).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two pre-existing PASTE_AUTH tokens in scripts/build.sh and
scripts/build-linux-notify.sh are real and should be rotated if the
paste.tbs.amn.gg / paste.dk.manko.yoga endpoints still authenticate
— this allowlist only silences the pre-push hook, it does not
remove the exposure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Refactor should_forward_pli(room, stream_id) -> should_forward_pli(room, stream_id, now: Instant)
so the 200 ms dedup window is deterministically testable.
- Update the one caller in run_participant_signals to pass Instant::now().
- Add 6 PLI unit tests covering:
* first PLI forwards
* duplicate within 200 ms suppressed
* after 200 ms forwards again
* different streams independent
* different rooms independent
* no stream owner returns None
Addresses reviewer CR on T4.7 (line drawn at T4.6 — stateful relay features must
have state-transition tests).
wzp-relay tests: 93 -> 99 pass.