gorgeous-ghost-69970
03/03/2026, 9:48 AMtall-airline-16756
03/03/2026, 9:49 AMtall-airline-16756
03/03/2026, 9:49 AMoverrideOutputAudioPort(.speaker) may have no effect unless done at the correct time and with the right session settings.
What you can try next
1. Delay publishing the local audio track until CallKit reports the call is connected.
◦ Only call room.localParticipant.publishAudio() (or create/publish the LocalAudioTrack) after you observe the call state as connected in your CXProviderDelegate or CXCallObserver.
◦ This prevents LiveKit from activating AVAudioSession prematurely.
1. Configure AVAudioSession before CallKit activates the session (so CallKit will honor your preferences when it activates):
◦ Set category and options such as:
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: [.defaultToSpeaker, .allowBluetooth])
• Do this before CallKit activation, not after.
1. Route to speaker at the correct time:
◦ After CallKit activates the audio session (you can detect this via your CXProviderDelegate callbacks), then call:
try AVAudioSession.sharedInstance().setActive(true)
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
• If you need a UI toggle for speaker/earpiece, call overrideOutputAudioPort when the call is active.
1. If CallKit still controls routing, ensure category/options are set before CallKit activation and use defaultToSpeaker option to prefer the speaker when the session becomes active.
One targeted question to narrow this down
• Could you paste the exact code snippet where you:
1) configure AVAudioSession, 2) call overrideOutputAudioPort(.speaker), and 3) create/publish your local audio track? Also tell me when (relative to CallKit events) each of those actions runs — for example, do you publish audio in your answer to provider(_:perform:) or immediately when starting the call?
If you share those snippets I’ll point out the minimal changes to ensure CallKit is in control first and the speaker override takes effect.
Sources: client-sdk-swift | livekit/livekit_composite | DeepWiki | Stream music over WebRTC using React and WebAudio | client-sdk-js | livekit/livekit_composite | DeepWikigorgeous-ghost-69970
03/03/2026, 9:52 AMtall-airline-16756
03/03/2026, 9:53 AMtry AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: [.defaultToSpeaker, .allowBluetooth])
• Do this before CallKit activation, not after.
1. Publish audio only after CallKit activation and then explicitly set routing if needed.
◦ In your CallKit delegate (for example in the callback where the call becomes active/connected), do:
// 1. Activate and route
try AVAudioSession.sharedInstance().setActive(true)
try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker)
// 2. Then create and publish your local audio track
let localAudioTrack = LocalAudioTrack() // or however you create it
try room.localParticipant?.publishTrack(localAudioTrack)
• If you prefer toggling speaker at runtime, run overrideOutputAudioPort(.speaker) when the call is active.
Why this works
• LiveKit only activates AVAudioSession when you publish an audio track. By delaying publish, you ensure CallKit is the primary manager of the session lifecycle.
• Setting category/options early with .defaultToSpeaker increases the chance the speaker is selected when the session becomes active.
• Calling overrideOutputAudioPort(.speaker) after CallKit activates the session enforces the route when CallKit allows it.
Edge cases and tips
• If the user can join rooms well before answering/starting a CallKit call (background sync, pre-join), keep audio muted/unpublished until the call is actually connected.
• If you need to start publishing earlier for app logic (e.g., to send presence audio), consider publishing with microphone muted and only unmuting/publishing audio input when CallKit is active. But note: any publishing at all can trigger AVAudioSession activation depending on implementation—testing is required.
• Use .allowBluetooth if you need Bluetooth headset support; CallKit may still choose route based on connected accessories.
• If CallKit still overrides routing, ensure you set the AVAudioSession category/options before CallKit activation—order matters.
A small question to help finalize a precise code suggestion
• Do you currently create the LocalAudioTrack immediately when connecting, or do you create it only when you publish? If you already create it, can you delay creation until after CallKit activation (so the SDK never has a track to publish until the right time)?
Share that detail and I’ll give the minimal code change to your existing flow to make this reliable.gorgeous-ghost-69970
03/03/2026, 10:09 AMAVAudioSession and the SDK’s audio engine is crucial.
1. Disable the SDK’s automatic AVAudioSession configuration, and prevent the audio engine from starting outside CallKit’s provider(_:didActivate:) and provider(_:didDeactivate:) window.
// As early as possible, before connecting to a Room.
AudioManager.shared.audioSession.isAutomaticConfigurationEnabled = false
try AudioManager.shared.setEngineAvailability(.none)
2. Coordinate audio engine availability with CallKit in your CXProviderDelegate implementation:
func provider(_: CXProvider, didActivate session: AVAudioSession) {
do {
try session.setCategory(.playAndRecord, mode: .voiceChat, options: [.mixWithOthers])
try AudioManager.shared.setEngineAvailability(.default)
} catch {
// Error
}
}
func provider(_: CXProvider, didDeactivate _: AVAudioSession) {
do {
try AudioManager.shared.setEngineAvailability(.none)
} catch {
// Error
}
}
what about this?tall-airline-16756
03/03/2026, 10:10 AMAudioManager.shared.audioSession.isAutomaticConfigurationEnabled = false
try AudioManager.shared.setEngineAvailability(.none)
• Only enabling the engine inside CallKit’s activation window (provider(_didActivate) / provider(_didDeactivate)) ensures the system (CallKit) is the primary manager of the audio session and routing. That avoids race conditions where LiveKit activates the session too early.
Small refinements and recommendations
• Set a clear session category/options when CallKit activates. Include Bluetooth and speaker options if you want those supported:
func provider(_ provider: CXProvider, didActivate session: AVAudioSession) {
do {
try session.setCategory(.playAndRecord, mode: .voiceChat,
options: [.mixWithOthers, .allowBluetooth, .defaultToSpeaker])
try session.setActive(true)
try AudioManager.shared.setEngineAvailability(.default)
} catch {
print("CallKit activation failed: \(error)")
}
}
• Use .defaultToSpeaker if your app should prefer speakerphone when the session becomes active.
• Use .allowBluetooth if you want Bluetooth headset support.
• Deactivation: ensure you fully stop the engine and restore availability when CallKit deactivates or the call ends:
func provider(_ provider: CXProvider, didDeactivate session: AVAudioSession) {
do {
try AudioManager.shared.setEngineAvailability(.none)
try session.setActive(false)
} catch {
print("CallKit deactivation failed: \(error)")
}
}
• Error handling & recovery: if setting engine availability fails, add retry/fallback logic or surface an error so you don’t leave the app in a partial audio state.
Speaker override specifics
• Calling AVAudioSession.overrideOutputAudioPort(.speaker) can be ineffective if done before CallKit activation or if CallKit has already selected a route. Your pattern (set category/options and call setActive + setEngineAvailability inside didActivate) is the correct place to ensure speaker routing is applied.
• If you need a runtime speaker toggle (user taps a button), call overrideOutputAudioPort(.speaker) or overrideOutputAudioPort(.none) only while the session is active (i.e., after didActivate).
Edge-case notes
• If you must join rooms before call activation for app logic, avoid creating/publishing a LocalAudioTrack (or create it muted) to prevent LiveKit from activating AVAudioSession prematurely.
• Monitor interruptions (phone calls, Siri) and ensure you pause/resume engine availability appropriately.
One quick question to confirm everything
• Do you currently create a LocalAudioTrack at room join time, or only when you publish? If you create the track immediately but keep it unpublished, the SDK may still prepare audio resources. If possible, delay creating/publishing the audio track until after CallKit activation to guarantee the SDK won’t touch the session beforehand.
If you share that small detail I’ll give the minimal code tweak to your current flow to ensure speaker routing and CallKit coordination work reliably.
Sources: client-sdk-swift | livekit/livekit_composite | DeepWiki | client-sdk-flutter | livekit/livekit_composite | DeepWiki