Previously: incoming calls silently popped an "Accept/Reject" panel. Easy to miss — no audible cue, no system-level alert if the app was backgrounded. Now the incoming-call path triggers both a synthesized ring tone and a system notification banner. ## Ring tone (desktop/src/main.ts) New `Ringer` class using Web Audio API directly — no external asset files, no new npm dep. Synthesizes a classic NANP two-tone cadence (440Hz + 480Hz sine mix, 2s tone + 4s silence, looped) through an envelope-gated gain node that ramps on/off to avoid clicks. Audible on every Tauri-supported platform because WebView carries Web Audio. - `start()` — lazily creates AudioContext on first use (platforms that require a user gesture for AudioContext creation still work because the incoming-call event is user-adjacent from the webview's perspective), starts setInterval(6000) loop. - `stop()` — clears the timer AND disconnects any active oscillators so there's no tail audio. - Active-nodes array is swept every cycle so it doesn't grow unbounded across long rings. Hooked into signal-event handlers: - `"incoming"` → `ringer.start()` + notifyIncomingCall - `"answered"`, `"setup"`, `"hangup"` → `ringer.stop()` - Accept/Reject button click handlers → `ringer.stop()` as the first thing they do (before any await) ## System notification (desktop/src-tauri + main.ts) Added `tauri-plugin-notification = "2"` to the Tauri app and registered in the builder. Capabilities updated with the four notification permissions. Frontend calls the plugin commands via the generic `invoke` instead of adding `@tauri-apps/plugin-notification` as a JS dep — Tauri plugins expose `plugin:notification|notify` etc. directly. Flow: 1. `is_permission_granted` — check cached 2. If not granted → `request_permission` (Android prompts the user once, cached thereafter) 3. `notify` with title="Incoming call", body="From <alias>" All wrapped in try/catch with console.debug fallback — plugin missing or permission denied is non-fatal, the visible panel + ring tone still alert the user. ## Known gaps (deferred) - Android native system ringtone (RingtoneManager) + full- screen intent for lockscreen-visible ringer. Requires platform-specific Java/Kotlin glue in the Tauri Android shell — bigger lift. - Desktop window flash / taskbar attention-seek on incoming call when app is backgrounded. - Vibration pattern on Android. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
109 lines
5.0 KiB
TOML
109 lines
5.0 KiB
TOML
[package]
|
|
name = "wzp-desktop"
|
|
version = "0.1.0"
|
|
edition = "2024"
|
|
description = "WarzonePhone Desktop — encrypted VoIP client"
|
|
default-run = "wzp-desktop"
|
|
|
|
# Library target — required for Tauri mobile (Android/iOS link the app as a cdylib)
|
|
# and also used by the desktop binary below.
|
|
#
|
|
# `staticlib` was DROPPED from crate-type because rust-lang/rust#104707
|
|
# documents that having staticlib alongside cdylib leaks non-exported
|
|
# symbols from staticlibs into the cdylib. Bionic's private `__init_tcb`
|
|
# / `pthread_create` symbols end up bound LOCALLY inside our .so instead
|
|
# of resolved dynamically against libc.so at dlopen time — which crashes
|
|
# at launch as soon as tao tries to std::thread::spawn() from the JNI
|
|
# onCreate callback. The legacy wzp-android crate uses ["cdylib", "rlib"]
|
|
# and runs fine on the same phone with the same NDK + Rust toolchain.
|
|
#
|
|
# iOS Tauri builds that actually need staticlib can re-add it behind a
|
|
# target cfg if we ever ship on iOS.
|
|
[lib]
|
|
name = "wzp_desktop_lib"
|
|
crate-type = ["cdylib", "rlib"]
|
|
|
|
[[bin]]
|
|
name = "wzp-desktop"
|
|
path = "src/main.rs"
|
|
|
|
[build-dependencies]
|
|
tauri-build = { version = "2", features = [] }
|
|
# cc is no longer needed — all C++ moved to crates/wzp-native (built with
|
|
# cargo-ndk and loaded via libloading at runtime). wzp-desktop's .so on
|
|
# Android is now pure Rust.
|
|
|
|
[dependencies]
|
|
tauri = { version = "2", features = [] }
|
|
tauri-plugin-shell = "2"
|
|
tauri-plugin-notification = "2"
|
|
serde = { version = "1", features = ["derive"] }
|
|
serde_json = "1"
|
|
tokio = { version = "1", features = ["full"] }
|
|
tracing = "0.1"
|
|
tracing-subscriber = "0.3"
|
|
anyhow = "1"
|
|
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
|
|
|
|
# WarzonePhone crates — protocol layer is platform-independent
|
|
wzp-proto = { path = "../../crates/wzp-proto" }
|
|
wzp-codec = { path = "../../crates/wzp-codec" }
|
|
wzp-fec = { path = "../../crates/wzp-fec" }
|
|
wzp-crypto = { path = "../../crates/wzp-crypto" }
|
|
wzp-transport = { path = "../../crates/wzp-transport" }
|
|
|
|
# wzp-client pulls in CPAL on every desktop target and, additionally on
|
|
# macOS, VoiceProcessingIO (coreaudio-rs behind the "vpio" feature). The
|
|
# vpio feature MUST NOT be enabled on Windows / Linux because coreaudio-rs
|
|
# is Apple-framework-only and will fail to build. Task #24 will add a
|
|
# matching Windows Voice Capture DSP path behind its own feature; until
|
|
# then, Windows desktops use plain CPAL with AEC disabled.
|
|
|
|
# macOS: CPAL + VoiceProcessingIO (hardware AEC via Core Audio).
|
|
[target.'cfg(target_os = "macos")'.dependencies]
|
|
wzp-client = { path = "../../crates/wzp-client", features = ["audio", "vpio"] }
|
|
|
|
# Windows: CPAL for playback + direct WASAPI for capture with OS-level
|
|
# AEC (AudioCategory_Communications). The wzp-client `windows-aec`
|
|
# feature swaps the default CPAL AudioCapture for a WASAPI one that
|
|
# opens the mic under AudioCategory_Communications, turning on Windows's
|
|
# communications audio processing chain (AEC, NS, AGC). The reference
|
|
# signal for AEC is the system render mix, so echo from our CPAL
|
|
# playback is cancelled automatically without extra plumbing.
|
|
[target.'cfg(target_os = "windows")'.dependencies]
|
|
wzp-client = { path = "../../crates/wzp-client", features = ["audio", "windows-aec"] }
|
|
|
|
# Linux: CPAL playback+capture baseline. AEC is enabled via the top-level
|
|
# `linux-aec` feature in wzp-desktop, which forwards to wzp-client/linux-aec.
|
|
# Keeping it opt-in at the wzp-desktop level (rather than forcing it always
|
|
# on here) lets `cargo tauri build` produce two variants from the same
|
|
# source tree — a noAEC baseline and an AEC build — by toggling the feature
|
|
# at build time: `cargo tauri build -- --features wzp-desktop/linux-aec`.
|
|
[target.'cfg(target_os = "linux")'.dependencies]
|
|
wzp-client = { path = "../../crates/wzp-client", features = ["audio"] }
|
|
|
|
# Android: no CPAL, no vpio — audio goes through the standalone wzp-native
|
|
# cdylib that we dlopen via libloading at runtime. See the wzp_native
|
|
# module in src/.
|
|
[target.'cfg(target_os = "android")'.dependencies]
|
|
wzp-client = { path = "../../crates/wzp-client", default-features = false }
|
|
# libloading: runtime dlopen of libwzp_native.so — the standalone cdylib
|
|
# crate that owns all C++ (Oboe bridge). Keeps wzp-desktop's .so free of
|
|
# any C/C++ static archives that would otherwise leak bionic's internal
|
|
# pthread_create into our cdylib and trigger the __init_tcb crash.
|
|
libloading = "0.8"
|
|
# jni + ndk-context: called from android_audio.rs to invoke
|
|
# AudioManager.setSpeakerphoneOn on the JVM side at runtime, so the
|
|
# Oboe playout stream (opened with Usage::VoiceCommunication) can route
|
|
# between earpiece and loud speaker without restarting.
|
|
jni = "0.21"
|
|
ndk-context = "0.1"
|
|
|
|
[features]
|
|
default = ["custom-protocol"]
|
|
custom-protocol = ["tauri/custom-protocol"]
|
|
# linux-aec: forwards to wzp-client/linux-aec so `cargo tauri build -- --features
|
|
# wzp-desktop/linux-aec` enables the WebRTC AEC3 backend on Linux. No-op on
|
|
# other targets because wzp-client/linux-aec is itself cfg(target_os = "linux").
|
|
linux-aec = ["wzp-client/linux-aec"]
|