Skip to content

Conversation

@mikaelwills
Copy link

iOS CallKit Audio Session Race Condition Fix - PR Description

Incoming calls on iOS with CallKit were failing because of a race condition between WebRTC (used internally by Siprix) and CallKit competing for audio session control. WebRTC needs to initialize audio immediately when the SIP INVITE arrives (to generate the SDP answer), but CallKit expects to control when the audio session activates. When WebRTC wins the race, iOS doesn't call the didActivate callback, which means we never actually accept the call even though the UI shows "connected".

Changes

1. Defer SIP Accept Until didActivate

Instead of accepting the SIP call immediately in CXAnswerCallAction, we now:

  • Store the call in _callPendingAnswer
  • Wait for didActivate to fire
  • Accept the call once audio is ready

Why it's critical: Accepting the call before the audio session is ready leads to no audio routing. iOS needs to know the audio path exists before the SIP connection completes.

2. Fulfill CXAnswerCallAction Immediately

Fulfill the action right away to tell CallKit we're ready to proceed:

action.fulfill()  // Triggers CallKit's audio activation flow

Why it's critical: If we don't fulfill, CallKit won't proceed with audio activation, so didActivate never fires and we're stuck waiting forever.

3. Force Reset Interrupted Audio Sessions

In didActivate, detect when WebRTC has already activated the audio session:

if !audioSession.isInputAvailable || audioSession.isOtherAudioPlaying {
    // Force reset to clear interrupted state
    try audioSession.setActive(false)
    try audioSession.setActive(true)
}

Why it's critical: WebRTC activating audio before CallKit leaves the session in an "interrupted" state. Resetting clears this and lets us reconfigure for VoIP properly.

4. Audio Configuration in didActivate (Not onSipConnected)

Added ensureAudioSessionConfigured() method that sets up VoIP audio settings:

try audioSession.setCategory(.playAndRecord, options: [.allowBluetooth, .allowBluetoothA2DP])
try audioSession.setMode(.voiceChat)
try audioSession.setActive(true)

Called from didActivate (before SIP accept), not onSipConnected (after connection).

Why it's critical: Configuring audio AFTER the SIP connection is too late - iOS has already determined there's no audio path. Must happen in didActivate which fires BEFORE we send the SIP accept.

Why This Approach

We can't prevent WebRTC from initializing early - it's required for SIP to work (needs to check hardware capabilities to generate the SDP answer). So instead, we work around it:

  • Let WebRTC initialize when needed (required for SDP)
  • Defer SIP accept until CallKit activates audio
  • Fulfill CXAnswerCallAction immediately to trigger CallKit's flow
  • Detect when WebRTC conflicts with CallKit
  • Reset the audio state to fix the conflict
  • Configure for VoIP before accepting
  • Then proceed with the call

The deferred answer pattern ensures everything happens in the right order: fulfill action → activate session → configure audio → accept call. Doing it any other way leaves the audio routing broken.

@siprix let me know your thoughts on whether this is all appropriate or not. After these changes ive personally noticed a dramatic increase in call reliability when receiving a call from the foreground/background and from an app killed state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant