From 76a4c53e21c5723c8667c495d1ba6eeb5bdf576e Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Fri, 10 Apr 2026 08:45:54 +0400 Subject: [PATCH] =?UTF-8?q?fix(android-audio):=20spawn=5Fblocking=20for=20?= =?UTF-8?q?Oboe=20restart=20=E2=80=94=20unblock=20tokio=20executor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build 4c6aac6 added a stop+sleep+start Oboe restart inside the set_speakerphone Tauri command, but calling wzp_native::audio_stop() and audio_start() synchronously from an async fn blocks the tokio executor thread — those FFI calls wait for AAudio to finalise the stream teardown/bringup, which takes ~400ms each on Nothing phone (Pixel is fast enough to hide the bug). Reproduced on Nothing: 7 rapid Speaker button clicks across ~30 seconds, each restarting Oboe. After the 5th click the engine send and recv tokio tasks froze for 22 seconds — decoded_frames stuck at 1159 across 9 heartbeats, send_drops growing from 148 to 1720 as encoded frames couldn't make it past `send_t.send_media(pkt).await`. At 08:40:48 the runtime finally caught up and processed a 911-frame burst at once (buffered QUIC datagrams flooding through). Classic "blocking sync call in async context" anti-pattern. Fix: run the stop + start sequence inside tokio::task::spawn_blocking so the Oboe teardown + reopen happens on a dedicated blocking thread, leaving the tokio runtime free to keep driving the send and recv tasks. AAudio's requestStop returns only after the stream is actually in Stopped state, so the explicit sleep that bridged stop and start is no longer needed and is dropped. Send and recv tasks still see a ~500ms window of empty reads / partial writes during the blocking restart, but they get SCHEDULED through it — network packets keep being received + decoded + dropped into the playout ring, and captured mic samples keep being encoded + sent through quinn. No more executor starvation, no more 22-second audio dropouts, no more send_drops burst. Pixel still worked before this fix only because its AAudio teardown is fast enough to not exceed the scheduler's cooperative yield interval — same bug was latent on both devices, Nothing just made it visible. --- desktop/src-tauri/src/lib.rs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 5552113..b18aa26 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -371,11 +371,26 @@ async fn set_speakerphone(on: bool) -> Result<(), String> { 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}"))?; + // Oboe's stop/start are sync C-FFI calls that block for ~400ms + // on Nothing-class devices (Pixel is faster). Calling them + // directly from an async Tauri command stalls the tokio + // executor — the send/recv engine tasks were observed to + // freeze for ~20 seconds across a few rapid speaker toggles, + // piling up buffered QUIC datagrams and then flooding them + // all at once when the runtime finally caught up. + // + // Fix: run the audio teardown + reopen on a dedicated + // blocking thread so the runtime keeps scheduling everything + // else. AAudio's requestStop returns only after the stream + // is actually in Stopped state, so no explicit inter-call + // sleep is needed. + tokio::task::spawn_blocking(|| { + wzp_native::audio_stop(); + wzp_native::audio_start() + .map_err(|code| format!("audio_start after speakerphone toggle: code {code}")) + }) + .await + .map_err(|e| format!("spawn_blocking join: {e}"))??; tracing::info!("set_speakerphone: Oboe restarted"); } Ok(())