fix(android-audio): spawn_blocking for Oboe restart — unblock tokio executor
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.
This commit is contained in:
@@ -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(())
|
||||
|
||||
Reference in New Issue
Block a user