diff --git a/desktop/src-tauri/src/engine.rs b/desktop/src-tauri/src/engine.rs index 62ee8f8..2cdda6c 100644 --- a/desktop/src-tauri/src/engine.rs +++ b/desktop/src-tauri/src/engine.rs @@ -601,8 +601,15 @@ impl CallEngine { } } + // Run audio_start on a blocking thread — wzp_oboe_start is a + // sync FFI call that can stall waiting for the Android audio + // HAL. Calling it directly blocks the tokio worker thread, + // which freezes all async tasks including our own timeouts. let t_pre_audio = call_t0.elapsed().as_millis(); - if let Err(code) = crate::wzp_native::audio_start() { + let audio_start_result = tokio::task::spawn_blocking(crate::wzp_native::audio_start) + .await + .map_err(|e| anyhow::anyhow!("audio_start task panic: {e}"))?; + if let Err(code) = audio_start_result { return Err(anyhow::anyhow!( "wzp_native_audio_start failed: code {code}" )); diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 9161d10..dbef306 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -335,13 +335,19 @@ joinVoiceBtn.addEventListener("click", async () => { joinVoiceBtn.textContent = "Connecting…"; (joinVoiceBtn as HTMLButtonElement).disabled = true; try { - await invoke("connect", { - relay: relay.address, - room: s.room || "general", - alias: s.alias || "", - osAec: s.osAec, - quality: s.quality || "auto", - }); + const connectRace = Promise.race([ + invoke("connect", { + relay: relay.address, + room: s.room || "general", + alias: s.alias || "", + osAec: s.osAec, + quality: s.quality || "auto", + }), + new Promise((_, reject) => + setTimeout(() => reject("connect timed out (15s) — check audio permissions"), 15000) + ), + ]); + await connectRace; enterVoice(false); } catch (e: any) { console.error("connect failed:", e);