On Android, calling setAudioRoute('speaker') (via @livekit/react-native) resolves without error and correctly lists 'speaker' as an available output, but the native audio route never actually changes — confirmed via dumpsys audio’s “Active communication device,” which stays on earpiece before and after the call. Traced through source to AudioSwitchManager.java’s dependency on com.github.davidliu/audioswitch, which routes speaker toggling through the legacy audioManager.isSpeakerphoneOn = enable, a pre-Android-12 API that Google’s docs mark as superseded by setCommunicationDevice(). There’s no error, exception, or event fired on failure, and no public method to read back the actual applied route — so the app-layer has no way to detect this without an external signal (in our case, Android’s own Telecom didChangeAudioRoute callback, which is unrelated to the SDK).
Repro steps: join a room on Android, call setAudioRoute('speaker'), check dumpsys audio | grep "Active communication device" before and after.
Confirmed on: Samsung Galaxy S24 (Android 16 / API 36) and Samsung A03s (Android 13 / API 33) — two different device generations, same result on both, so this doesn’t look device-specific.
Expected: the native audio route actually switches to speaker, or the call rejects/errors so the app can detect and surface the failure.
@jeanphil.marck, Your trace is right, and it sits in the dependency exactly where you point. audioswitch’s enableSpeakerphone is one line, audioManager.isSpeakerphoneOn = enable (AudioDeviceManager.kt#L81-L82), and nothing in that class promotes it to setCommunicationDevice on newer Android. isSpeakerphoneOn was deprecated in Android 12 (API 31) in favor of setCommunicationDevice, so on your S24 (API 36) and A03s (API 33) the write is accepted and silently ignored. That matches your dumpsys result and the missing readback, since the setter returns nothing and the class never queries the applied device.
It is not tracked yet. The closest open report is #966, earpiece routing after a reused Room on Android 12+, the same setCommunicationDevice gap from a different trigger (client-sdk-android#966). Yours is worth its own issue on client-sdk-android, since that is where the audioswitch dependency is owned. Your repro and the two API levels are already a clean report, and linking #966 helps whoever picks it up see it is one root cause..
Until the SDK migrates, you can force the route yourself on API 31+ with a small native module: call AudioManager.setCommunicationDevice with the TYPE_BUILTIN_SPEAKER entry from getAvailableCommunicationDevices, and confirm it with getCommunicationDevice so you are no longer blind to failures. Call it after setAudioRoute, since the SDK’s isSpeakerphoneOn write is a no-op on 31+ and will not fight you..
Just trying to reproduce this:
- GitHub - livekit-examples/agent-starter-react-native · GitHub modified at this line: agent-starter-react-native/app/assistant/index.tsx at main · livekit-examples/agent-starter-react-native · GitHub to add:
await AudioSession.selectAudioOutput('speaker');
- Running on physical Pixel device (I don’t have a Samsung device)
Behaviour of adb shell dumpsys audio | grep -A2 -i "Active communication device" :
- When not on a call: Active communication device: AudioDeviceAttributes: role:output type:bt_a2dp addr:XX:XX:XX:XX:D2:62 name:LE-Darryn Headphones profiles:
- When I join a call: Active communication device: AudioDeviceAttributes: role:output type:speaker addr: name:Pixel 9a profiles:
So, I’m unable to reproduce this - perhaps it is device specific, I know there have been Samsung-specific issues in the past.
Thank you, @Muhammad Usman Bashir — your root cause was exactly right, and the trace matched what we found independently. One important addition worth flagging for anyone else hitting this on a Telecom self-managed ConnectionService (e.g. via react-native-callkeep’s Android Self Managed mode): calling AudioManager.setCommunicationDevice() directly, as suggested above, worked on one of our test devices (Samsung A03s, API 33) but silently failed on another (Samsung Galaxy S24, API 36) — the call returned success, but the real hardware route never changed.
Root cause of that second failure: Android’s own docs describe raw AudioManager routing and Telecom’s Connection.setAudioRoute() as two separate, non-overlapping mechanisms — they don’t cross-reference each other anywhere. If an app is already a Telecom self-managed call (which react-native-callkeep’s Self Managed mode is), a stricter Android build appears to silently reassert its own route ownership over a raw AudioManager write, while still returning success from that call. Our A03s “working” was a looser/older enforcement, not confirmation the approach was actually correct.
The fix that ended up working reliably on both devices: route through Connection.setAudioRoute() itself — for anyone on react-native-callkeep, that’s the already-exposed toggleAudioRouteSpeaker(uuid, routeSpeaker) method. Since that is Telecom routing the call, it can’t conflict with Telecom’s own ownership the way a raw AudioManager call can. Confirmed bidirectionally on both devices with three independently-agreeing signals (the native call, Telecom’s own didChangeAudioRoute event, and a dumpsys audio hardware readback).
Worth a note in #966 or wherever this ends up tracked: the fix likely needs to branch on whether the app is a Telecom self-managed call at all, not just on API level — setCommunicationDevice() may be fine for apps that aren’t using ConnectionService, but is the wrong tool for ones that are.