diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index f5fb090..5552113 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -354,13 +354,31 @@ async fn get_status(state: tauri::State<'_, Arc>) -> Result Result<(), String> { #[cfg(target_os = "android")] { - android_audio::set_speakerphone(on) + android_audio::set_speakerphone(on)?; + if wzp_native::is_loaded() && wzp_native::audio_is_running() { + tracing::info!(on, "set_speakerphone: restarting Oboe for route change"); + wzp_native::audio_stop(); + // Give AAudio a tick to finalise the stop before we re-open. + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + wzp_native::audio_start() + .map_err(|code| format!("audio_start after speakerphone toggle: code {code}"))?; + tracing::info!("set_speakerphone: Oboe restarted"); + } + Ok(()) } #[cfg(not(target_os = "android"))] { diff --git a/desktop/src-tauri/src/wzp_native.rs b/desktop/src-tauri/src/wzp_native.rs index 7c492f0..4f73e98 100644 --- a/desktop/src-tauri/src/wzp_native.rs +++ b/desktop/src-tauri/src/wzp_native.rs @@ -123,7 +123,6 @@ pub fn audio_write_playout(input: &[i16]) -> usize { unsafe { f(input.as_ptr(), input.len()) } } -#[allow(dead_code)] pub fn audio_is_running() -> bool { AUDIO_IS_RUNNING.get().map(|f| unsafe { f() } != 0).unwrap_or(false) } diff --git a/desktop/src/main.ts b/desktop/src/main.ts index fa37f96..57f3caf 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -495,6 +495,12 @@ function showCallScreen() { roomName.textContent = roomInput.value; callStatus.className = "status-dot"; statusInterval = window.setInterval(pollStatus, 250); + // Sync the Speaker/Earpiece label with the OS state (Android only; on + // desktop the command is a no-op returning false so we land on "Earpiece" + // which is fine because desktop has no routing concept). + invoke("is_speakerphone_on") + .then((on) => { speakerphoneOn = !!on; updateSpkLabel(); }) + .catch(() => { speakerphoneOn = false; updateSpkLabel(); }); } function showConnectScreen() { @@ -511,23 +517,36 @@ micBtn.addEventListener("click", async () => { try { const m: boolean = await invoke("toggle_mic"); micBtn.classList.toggle("muted", m); micIcon.textContent = m ? "Mic Off" : "Mic"; } catch {} }); -// Speaker routing (Android) — toggles AudioManager.setSpeakerphoneOn so the -// same Oboe VoiceCommunication stream swaps between earpiece and -// loudspeaker without restarting. Desktop callers get a no-op command so -// the same UI works everywhere. +// Speaker routing (Android) — toggles AudioManager.setSpeakerphoneOn + then +// stops and restarts the Oboe streams so AAudio reconfigures with the new +// routing. The Rust-side Tauri command handles the restart, we just swap +// the button label. +// +// Earpiece is NOT a "muted" state, so DO NOT add the `.muted` CSS class +// (which would tint the button red); that was a bug in 0178cbd that made +// earpiece mode look like playback was off. A separate `.speaker-on` class +// is available for css styling if we want to visually indicate loud mode. let speakerphoneOn = false; +let speakerphoneBusy = false; function updateSpkLabel() { - spkBtn.classList.toggle("muted", !speakerphoneOn); - spkIcon.textContent = speakerphoneOn ? "Speaker" : "Earpiece"; + spkBtn.classList.toggle("speaker-on", speakerphoneOn); + spkBtn.classList.remove("muted"); + spkIcon.textContent = speakerphoneOn ? "🔊 Speaker" : "🔈 Earpiece"; } spkBtn.addEventListener("click", async () => { + if (speakerphoneBusy) return; // debounce — the restart takes ~60ms + speakerphoneBusy = true; const next = !speakerphoneOn; + spkBtn.disabled = true; try { await invoke("set_speakerphone", { on: next }); speakerphoneOn = next; updateSpkLabel(); } catch (e) { console.error("set_speakerphone failed:", e); + } finally { + spkBtn.disabled = false; + speakerphoneBusy = false; } }); hangupBtn.addEventListener("click", async () => {