I have implemented Swift SDK in my iOS video calli...
# ask-ai
g
I have implemented Swift SDK in my iOS video calling app. I have used Callkit framework and I don't want LiveKit sdk to activate the audio session before callkit activates it. I have used the example project here. It work well but the problem is I can't seem to override output port to speaker. This is the code I am using-
t
Thanks for reaching out, newaj. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I understand you’re using the LiveKit Swift SDK with CallKit and you don’t want LiveKit to activate the audio session before CallKit does, and you’re unable to force audio to the speaker. I’m happy to help—this is usually a timing / session-configuration issue between CallKit and AVAudioSession. What I’m responding to: the part about preventing LiveKit from activating the audio session until CallKit activates it, and your problem overriding the output port to speaker. Quick summary of what’s likely happening • LiveKit’s SDK does not activate the AVAudioSession until you publish a local audio track. If you publish too early (before CallKit activates the session), the session may be configured/routed in a way that prevents your speaker override from taking effect. • CallKit also manages the audio session during a call. If CallKit has already taken control of the session or selected a route, calling
overrideOutputAudioPort(.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:
Copy code
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:
Copy code
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 | DeepWiki
g
I publish audio immediately after connecting to room, and I might connect to room anytime independent of callkit.
t
Thanks — this is helpful. You confirmed you publish audio immediately after joining the room and that joining can happen independently of CallKit. That explains why LiveKit’s SDK (which only activates AVAudioSession when you publish audio) can end up activating the session before CallKit, which prevents your speaker override from taking effect. What you can do (practical, step-by-step) 1. Stop publishing audio until CallKit says the call is active. ◦ Keep the connection to LiveKit (you can join the room, subscribe to tracks, and receive media) but delay creating/publishing the local audio track until CallKit reports the call is connected/active. ◦ This prevents LiveKit from activating AVAudioSession at the wrong time. 1. Configure AVAudioSession preferences before CallKit activates. ◦ Set the category, mode, and options early (for example when initiating the call UI), so when CallKit activates the session it will honor your preferences:
Copy code
try 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:
Copy code
// 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.
g
Integration with CallKit When integrating with CallKit, proper timing and coordination between
AVAudioSession
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.
Copy code
// 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:
Copy code
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?
t
Thanks — this is a good, correct approach and follows the recommended pattern for integrating LiveKit’s Swift SDK with CallKit. I’ll highlight why it works, small refinements, and one targeted follow-up so we can close the loop. What you’re doing right • Disabling automatic AVAudioSession configuration before connecting prevents the SDK from starting the audio engine on its own:
Copy code
AudioManager.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:
Copy code
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:
Copy code
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