React Native sdk - Audio is being output to phone'...
# ask-ai
s
React Native sdk - Audio is being output to phone's earpiece like in a phonecall, instead of the phone's loudspeaker, despite setting the
defaultOutput: "speaker"
in
AudioSession.configureAudio
. I'm even then setting
await AudioSession.selectAudioOutput("force_speaker")
and the setting is ignored, the audio seems to play through earpiece. I want the audio to default to either a compatible connected bluetooth device (e.g. headphones), or the phone's loudspeaker - never the earpiece
t
Thanks for reaching out, Sam. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I hear your frustration. You want audio to go to a connected Bluetooth headset or the phone loudspeaker (never the earpiece), and your calls to
AudioSession.configureAudio
/
AudioSession.selectAudioOutput
aren’t having any effect. I’m responding about audio routing behavior in LiveKit’s React Native SDK and next steps you can take. What’s happening • LiveKit’s React Native SDK does not expose an
AudioSession
API with
configureAudio
or
selectAudioOutput
. Audio routing is handled by the underlying WebRTC/native layer (
react-native-webrtc
→ platform Audio session) rather than by LiveKit’s public SDK APIs. • Because LiveKit doesn’t provide built-in APIs to select or force audio outputs, calling those
AudioSession
methods (which appear to be from another library or a custom module) will be ignored by the LiveKit SDK/WebRTC stack unless those calls actually change the native audio session used by WebRTC on the device. What you can try • Confirm the source of `AudioSession`: Identify whether
AudioSession
belongs to a third-party module or custom native code in your app. If it’s not a module that bridges to the same native audio session WebRTC is using, its calls won’t affect the WebRTC audio route. • Use native audio APIs (or a bridge) that target the same audio session WebRTC uses: ◦ On iOS you need to configure
AVAudioSession
category/mode and set the route to speaker when appropriate. ◦ On Android you need to use
AudioManager
to set speakerphone on/off or route to Bluetooth. • Use a React Native bridge library known to interact with native audio routing for WebRTC. Typical approaches are: ◦ Add small native modules that call
AVAudioSession.setCategory
/
AVAudioSession.overrideOutputAudioPort(.speaker)
on iOS. ◦ Use
AudioManager.setSpeakerphoneOn(true)
or
AudioManager.startBluetoothSco()
on Android, and ensure the app has appropriate Bluetooth permissions and SCO support. • Ensure timing: Make routing changes before starting playback or immediately after the remote track is attached; sometimes routing calls must run at the right point in the session lifecycle to take effect. • Test device behavior: Some Android OEMs and certain Bluetooth devices behave differently; test on both iOS and Android devices and with multiple Bluetooth devices. Questions to narrow this down 1. Which library provides your
AudioSession
calls (package name), or is it custom native code? 2. Are you on iOS, Android, or both? 3. Do you see different behavior when a Bluetooth device is already connected before joining the room vs connecting after the call starts? 4. Can you share a minimal snippet showing how and when you call
configureAudio
/
selectAudioOutput
relative to joining the LiveKit room and when you attach remote audio tracks? If you’d like, I can: • Walk through specific iOS
AVAudioSession
or Android
AudioManager
code snippets to force speaker / prefer Bluetooth. • Suggest where to place these calls in the React Native lifecycle so they affect WebRTC playback. • Review a short code snippet from your app to point out where routing calls should be inserted. Tell me the answers to the questions above or paste the relevant snippet and I’ll provide targeted steps for iOS and/or Android. Sources: Web and mobile frontends | LiveKit Documentation | Agent speech and audio | LiveKit Documentation | LiveKit coming to React Native! | LiveKit coming to React Native! | Launch Week! :rocket:
s
Which library provides your AudioSession calls (package name), or is it custom native code?
@livekit/react-native
—
AudioSession
is imported directly from the LiveKit React Native SDK:
Copy code
ts
import { AudioSession } from "@LiveKit/react-native";
Are you on iOS, Android, or both? Both. The issue is most noticeable on iOS (audio going to earpiece like a phone call). Do you see different behavior when a Bluetooth device is already connected before joining the room vs connecting after the call starts? If a bluetooth device is connected it normally routes to that. However if we disconnect bluetooth then it typically routes to earpiece instead of speaker. If join without a bluetooth device then it typically routes to earpiece instead of loudspeaker. If we connect a bluetooth device whilst already in a room, then it typically routes to bluetooth straight away. Can you share a minimal snippet showing how and when you call configureAudio / selectAudioOutput relative to joining the LiveKit room and when you attach remote audio tracks? Yes — see below. Snippet: Current Setup:
Copy code
ts
// 1. Audio is configured and started before room.connect
await AudioSession.configureAudio({
  ios: { defaultOutput: "speaker" },
  android: {
    preferredOutputList: ["bluetooth", "headset", "speaker"],
    audioTypeOptions: AndroidAudioTypePresets.communication,
  },
});

if (Platform.OS === "ios") {
  await AudioSession.selectAudioOutput("force_speaker");
}

await AudioSession.startAudioSession();
await room.connect(serverUrl, token, { autoSubscribe: false });
We also use
useIOSAudioManagement(room, true, customConfig)
with: -
audioMode: "videoChat"
-
audioCategory: "playAndRecord"
-
audioCategoryOptions: ["mixWithOthers", "defaultToSpeaker", "allowBluetooth"]
t
Thanks — this is very helpful detail. I’m responding to your points about AudioSession coming from
@livekit/react-native
, the iOS-heavy symptom (audio routing to the earpiece), and your snippet showing configure → start → connect. Short answer •
AudioSession
in
@livekit/react-native
is the SDK API you should use to control the native audio session. • Your code mostly follows the right pattern, but iOS can revert to the receiver (earpiece) when routes change (e.g., no Bluetooth connected). The reliable fix is to (A) only use
AudioSession
(avoid conflicting hooks), (B) ensure the session is started before connecting, and (C) detect route changes and re-apply speaker forcing when needed. What I recommend you try (concise, actionable) 1. Remove the
useIOSAudioManagement
hook to avoid conflicts. 2. Ensure ordering: configure → start session → force speaker → connect. a. Call
await AudioSession.configureAudio(...)
. b. Call
await AudioSession.startAudioSession()
and wait for it to resolve. c. Immediately call
await AudioSession.selectAudioOutput("force_speaker")
on iOS. d. Then
await room.connect(...)
. 1. Handle route changes at runtime: ◦ Listen for audio-route change events and re-apply
selectAudioOutput("force_speaker")
when Bluetooth disconnects or when the system switches to the receiver. ◦ Example pattern: ▪︎ Add a listener:
AudioSession.addEventListener("audioRouteChanged", handler)
. ▪︎ In the handler, detect a Bluetooth→builtin change (or any change to receiver) and call
AudioSession.selectAudioOutput("force_speaker")
. 1. Add logging and error handling: ◦ Log results/errors from
startAudioSession()
and
selectAudioOutput()
to confirm they succeed. ◦ If
startAudioSession()
fails or is delayed, the route-change calls won’t take effect. 1. Test variations: ◦ Join with Bluetooth connected (should route to BT). ◦ Join with no BT (should route to speaker after forcing). ◦ Disconnect BT while in a room (listener should reapply speaker). Why this helps • iOS will sometimes pick the receiver when no external device is present unless
defaultToSpeaker
/ forcing to speaker is applied to the active AVAudioSession. Starting the LiveKit
AudioSession
early and re-applying the forced output on route changes ensures the active WebRTC/native session uses the speaker instead of the earpiece. • Removing
useIOSAudioManagement
avoids competing native session changes. Questions to narrow further (if you want me to dig deeper) 1. After
startAudioSession()
and
selectAudioOutput("force_speaker")
, do your logs show those calls succeed (no errors)? 2. Do you already have an
audioRouteChanged
listener added, and if so what events do you see when Bluetooth disconnects? 3. Which iOS versions/devices do you see the problem on? Some OEM/OS behaviors differ. If you’d like, paste a short snippet of your new sequence (configure → start → select → connect) with any logs you get and I’ll review it and suggest exact placement for listeners and any defensive checks to make it robust. Sources: LiveKit coming to React Native!
s
Does livekit have an
audioRouteChanged
listener?
t
Thanks — I’m following up on whether LiveKit’s React Native SDK exposes an
audioRouteChanged
listener. Short answer • The LiveKit React Native SDK does not document or expose a built-in
audioRouteChanged
event in the React Native API. What that means and next steps • LiveKit’s React Native SDK uses the native WebRTC/audio stack under the hood, and the Android client includes an
AudioSwitchHandler
(native helper) to manage routing automatically. However, the React Native surface does not provide a documented callback named
audioRouteChanged
for you to subscribe to directly. • To reliably react to route changes (Bluetooth disconnects, headset plug/unplug, etc.) from React Native, you have two practical options: ◦ Use a React Native module that exposes native audio-route events: ▪︎ On iOS, that module should forward AVAudioSession route-change notifications. ▪︎ On Android, it should forward AudioManager / Bluetooth SCO events. ▪︎ Examples: community packages that expose audio route changes or a small custom native bridge that listens to AVAudioSessionRouteChangeNotification / AudioManager broadcasts and emits events to JS. ◦ Reapply your desired route proactively: ▪︎ After calling
AudioSession.startAudioSession()
, call
AudioSession.selectAudioOutput("force_speaker")
(iOS) and log success/errors. ▪︎ If you cannot observe route events, consider periodically checking the active route on your side (or add UI controls) and re-applying the speaker selection when you detect the audio is on the receiver. How I can help next • I can suggest specific native event names and tiny code snippets for iOS (AVAudioSessionRouteChangeNotification) and Android (AudioManager ACTION_HEADSET_PLUG, Bluetooth events) so you can implement a small bridge. • Or I can review a short snippet of your app and propose the simplest place to add route-change handling and re-apply
selectAudioOutput("force_speaker")
. Which would you prefer — a ready-to-add React Native snippet that listens for route changes on iOS/Android, or starter code for a simple native bridge? Sources: client-sdk-android | livekit/livekit_composite | DeepWiki | LiveKit coming to React Native! | LiveKit coming to React Native!
s
What we’ve tried (all succeed, but no effect) 1. Configuration • AudioSession.configureAudio({ ios: { defaultOutput: "speaker" } }) before connect • AudioSession.startAudioSession() before connect • AudioSession.selectAudioOutput("force_speaker") before connect 2. Re-applying selectAudioOutput("force_speaker") at 2s and 4s after connect Re-applying every 500ms for 15 seconds after connect • Re-applying on TrackSubscribed and TrackPublished 1. Native patch Switched selectAudioOutput to use RTCAudioSession.sharedInstance() instead of AVAudioSession.sharedInstance() • Used lockForConfiguration() / unlockForConfiguration() when setting the output 1. Manual route picker • AudioSession.showAudioRoutePicker() shows Speaker selected; Earpiece is not listed. Only Speaker and some AirPlay devices appear. • Despite that, audio still plays through the physical earpiece. Observation The system says Speaker is selected, but output goes to the earpiece. This suggests either: WebRTC uses a different audio session / path than the one we’re configuring, or • WebRTC’s playback path ignores overrideOutputAudioPort, or • Some other layer overrides the route after we set it.
t
Thanks — this is a great, thorough summary of everything you’ve tried. I can hear your frustration; you’ve covered the obvious approaches and the system still routes audio to the earpiece even though the UI/sessions report “Speaker.” I’ll focus on the likely root causes and give concrete next steps and tests you can run to pinpoint/fix the issue. Why this is happening (most likely causes) • WebRTC may be using a different native audio path/session for playback than the AVAudioSession you’re changing. On iOS there are multiple ways WebRTC can own or interact with the audio layer (RTCAudioSession vs. AVAudioSession), and if playback is attached to a different session/object, an override on the other session will not affect audio playback. • Some WebRTC implementations (or the specific native wrapper) may reconfigure or re-activate the audio session during media start, track attach/detach, or route change events. That reconfiguration can revert your earlier overrides. • overrideOutputAudioPort / force speaker calls can be ignored if the audio session category/mode is not compatible or if the session is not locked/configured at the exact moment WebRTC starts audio I/O. • The system route picker showing “Speaker” while audio comes from receiver indicates a mismatch between the system-level route state and the actual playback path used by WebRTC — often meaning WebRTC’s internal audio engine is using a different audio session/IO path. Concrete tests to narrow the cause (run these in order) 1. Verify which native session WebRTC is using at runtime ◦ Add quick native logs in both places: ▪︎ Log AVAudioSession.sharedInstance().currentRoute and category/mode immediately after your configure/start/select calls. ▪︎ Log RTCAudioSession.sharedInstance().category/mode/currentRoute (or the equivalent in the WebRTC wrapper) right after the remote audio begins playing. ◦ If the RTCAudioSession route differs or is reconfigured after your calls, WebRTC is overriding your changes. 1. Force the same session object WebRTC uses ◦ You mentioned a native patch switching to RTCAudioSession.sharedInstance(); that’s the right idea. Ensure you: ▪︎ Acquire lockForConfiguration() on RTCAudioSession before changing the route. ▪︎ Make the override immediately after acquiring the lock and before any WebRTC audio starts. ◦ Test: call RTCAudioSession override code right before you attach remote audio playback (or right after connect but before subscribing). 1. Apply route change right at the moment audio IO begins ◦ Add hooks to run selectAudioOutput("force_speaker") at these moments: ▪︎ Immediately after you receive the first AudioTrackSubscribed event and before playback begins. ▪︎ Immediately after the remote track is attached to the audio element/renderer. ◦ If you see success only when run exactly at audio start, timing/ordering is the issue. 1. Confirm no later reconfiguration resets the route ◦ Add an observer in native code for AVAudioSessionRouteChangeNotification and for any WebRTC-internal events (if accessible). Log whenever the session category/mode/route changes and who triggered it. ◦ If you see a route change after your “force_speaker” call, identify the caller and re-apply forcing after that event. 1. Test RTCAudioSession-only approach ◦ Temporarily remove all AVAudioSession/AudioSession.configureAudio calls and only use RTCAudioSession (the exact calls you use in the native patch) to set category/options + force speaker. If that fixes it, keep using RTCAudioSession in your app as the single source of truth. Practical code/placement tips • Always lock/unlock when configuring RTCAudioSession: ◦ lockForConfiguration() ◦ set category/mode/options and overrideOutputAudioPort(.speaker) ◦ unlockForConfiguration() • Do the above right before subscribing to audio or immediately when you attach the remote track to the audio output. • Keep one authoritative flow: either AVAudioSession or RTCAudioSession — do not set both (they can fight and produce race conditions). If you want a short debugging checklist to run now 1. Add logs: AVAudioSession.currentRoute and RTCAudioSession.currentRoute at these points: ◦ After configureAudio/startAudioSession/selectAudioOutput ◦ After room.connect ◦ After first TrackSubscribed 1. If RTCAudioSession route differs, change your selectAudioOutput to call RTCAudioSession APIs and test. 2. Add a route-change observer and log every change to see what resets the route. When to escalate to a LiveKit bug (what to collect first) • If you confirm RTCAudioSession immediately reconfigures the route after your changes and you cannot prevent it via lockForConfiguration/use-before-subscribe, collect: ◦ Native logs showing sequence of route/category changes (timestamps). ◦ The minimal repro (small app or steps) that shows configure → start → select succeed but audio still on receiver. ◦ Device model and iOS version where this reproduces. I can help you format those logs/steps for a bug report. Would you like: • Example native snippets to perform RTCAudioSession locking + override and where to place them in RN lifecycle? OR • Help interpreting logs if you paste the AVAudioSession / RTCAudioSession traces you get from the checks above? Pick one and I’ll give the specific code or help parse your logs.