fix(bluetooth): wait for SCO link before Oboe restart + detect A2DP devices
Three fixes for Bluetooth audio not working: 1. is_bluetooth_available() now checks for TYPE_BLUETOOTH_A2DP (8) in addition to TYPE_BLUETOOTH_SCO (7) — many headsets only register as A2DP until SCO is explicitly started. 2. set_bluetooth_sco(on=true) polls isBluetoothScoOn() for up to 3s before restarting Oboe. startBluetoothSco() is async — the SCO link takes 500ms-2s to establish. Without waiting, Oboe opens against earpiece and audio goes nowhere. 3. Frontend skips redundant set_speakerphone(false) when transitioning to BT — start_bluetooth_sco() handles speaker-off internally, avoiding a double Oboe restart. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -185,10 +185,11 @@ pub fn is_bluetooth_sco_on() -> Result<bool, String> {
|
||||
.map_err(|e| format!("isBluetoothScoOn: {e}"))
|
||||
}
|
||||
|
||||
/// Check whether a Bluetooth SCO-capable device is currently connected.
|
||||
/// Check whether a Bluetooth audio device is currently connected.
|
||||
///
|
||||
/// Iterates `AudioManager.getDevices(GET_DEVICES_OUTPUTS)` and looks for
|
||||
/// `TYPE_BLUETOOTH_SCO` (7).
|
||||
/// any Bluetooth device type. Many headsets only register as A2DP until
|
||||
/// SCO is explicitly started, so we check for both SCO and A2DP types.
|
||||
pub fn is_bluetooth_available() -> Result<bool, String> {
|
||||
let (vm, activity) = jvm_and_activity()?;
|
||||
let mut env = vm
|
||||
@@ -220,8 +221,9 @@ pub fn is_bluetooth_available() -> Result<bool, String> {
|
||||
.call_method(&device, "getType", "()I", &[])
|
||||
.and_then(|v| v.i())
|
||||
.unwrap_or(0);
|
||||
// TYPE_BLUETOOTH_SCO = 7
|
||||
if device_type == 7 {
|
||||
// TYPE_BLUETOOTH_SCO = 7, TYPE_BLUETOOTH_A2DP = 8
|
||||
if device_type == 7 || device_type == 8 {
|
||||
tracing::info!(device_type, idx = i, "is_bluetooth_available: found BT device");
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -779,6 +779,10 @@ async fn is_speakerphone_on() -> Result<bool, String> {
|
||||
|
||||
/// Enable or disable Bluetooth SCO audio routing. Like speakerphone toggling,
|
||||
/// this requires an Oboe stream restart so AAudio picks up the new route.
|
||||
///
|
||||
/// `startBluetoothSco()` is asynchronous — the SCO link takes 500ms-2s to
|
||||
/// establish. We poll `isBluetoothScoOn()` up to 3 seconds before restarting
|
||||
/// Oboe, so the streams open against the BT device rather than earpiece.
|
||||
#[tauri::command]
|
||||
#[allow(unused_variables)]
|
||||
async fn set_bluetooth_sco(on: bool) -> Result<(), String> {
|
||||
@@ -786,6 +790,21 @@ async fn set_bluetooth_sco(on: bool) -> Result<(), String> {
|
||||
{
|
||||
if on {
|
||||
android_audio::start_bluetooth_sco()?;
|
||||
// Wait for SCO link to actually connect before restarting Oboe.
|
||||
// startBluetoothSco() is async — jumping straight to Oboe restart
|
||||
// would open streams against earpiece, not the BT device.
|
||||
let mut connected = false;
|
||||
for i in 0..30 {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
if android_audio::is_bluetooth_sco_on().unwrap_or(false) {
|
||||
tracing::info!(polls = i + 1, "set_bluetooth_sco: SCO connected");
|
||||
connected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !connected {
|
||||
tracing::warn!("set_bluetooth_sco: SCO did not connect within 3s, proceeding anyway");
|
||||
}
|
||||
} else {
|
||||
android_audio::stop_bluetooth_sco()?;
|
||||
}
|
||||
|
||||
@@ -939,15 +939,17 @@ async function cycleAudioRoute() {
|
||||
const idx = routes.indexOf(currentAudioRoute);
|
||||
const next = routes[(idx + 1) % routes.length];
|
||||
|
||||
// Tear down current route
|
||||
// Tear down current route, then activate next.
|
||||
// start_bluetooth_sco() already calls setSpeakerphoneOn(false)
|
||||
// internally, so we skip the separate speakerphone toggle when
|
||||
// transitioning to BT to avoid a redundant Oboe restart.
|
||||
if (currentAudioRoute === "bluetooth") {
|
||||
await invoke("set_bluetooth_sco", { on: false });
|
||||
}
|
||||
// Activate next route
|
||||
if (next === "speaker") {
|
||||
await invoke("set_speakerphone", { on: true });
|
||||
} else if (next === "bluetooth") {
|
||||
await invoke("set_speakerphone", { on: false });
|
||||
// BT start handles speaker-off internally + waits for SCO link
|
||||
await invoke("set_bluetooth_sco", { on: true });
|
||||
} else {
|
||||
// earpiece — turn everything off
|
||||
|
||||
Reference in New Issue
Block a user