Native calls: upgrade to LiveKit 2.15 + add CallKit audio-session coordination

Root cause of the intermittent dead-mic / no-audio / late-speaker: LiveKit's
automatic AVAudioSession config races CallKit's activation. The fix needs the
2.15+ audio API, which the build wasn't getting ('~> 2.0' resolved an older 2.x
from a stale spec cache). So:
- NativeCall.podspec: pin LiveKitClient '~> 2.15'.
- codemagic.yaml: 'pod install --repo-update' so the spec repo knows 2.15.x.
- Plugin: disable LiveKit auto audio-session config + keep the engine OFF; in
  CXProvider didActivate set the session category and enable the engine; in
  didDeactivate disable it. Request mic permission on connect so enabling the
  engine in didActivate doesn't block on undetermined permission.

All AudioManager APIs verified against the raw 2.15.3 source (setEngineAvailability,
AudioEngineAvailability.default/.none, audioSession.isAutomaticConfigurationEnabled).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 19:21:39 +05:30
parent 155e89c6c0
commit 455d16abcc
3 changed files with 29 additions and 12 deletions
+3 -1
View File
@@ -93,7 +93,9 @@ workflows:
- name: Install CocoaPods
script: |
cd mobile/ios/App
pod install
# --repo-update refreshes the CocoaPods spec repo so it knows the latest LiveKitClient (the build box's
# cached spec repo was stale, so '~> 2.x' resolved an OLD version missing the CallKit audio API).
pod install --repo-update
- name: Build the signed IPA
script: |
@@ -18,10 +18,12 @@ Pod::Spec.new do |s|
s.ios.deployment_target = '14.0'
s.dependency 'Capacitor'
# LiveKit iOS SDK — carries the call media NATIVELY so audio survives backgrounding AND coordinates with
# CallKit's audio session (auto-config OFF via AudioManager.shared.audioSession.isAutomaticConfigurationEnabled;
# we configure the session in CXProvider didActivate), which the WebView's WebRTC could not do. Pulls in the
# WebRTC binary transitively.
s.dependency 'LiveKitClient', '~> 2.0'
# CallKit's audio session (auto-config OFF via AudioManager.shared.audioSession.isAutomaticConfigurationEnabled
# + setEngineAvailability in CXProvider didActivate/didDeactivate), which the WebView's WebRTC could not do.
# Pulls in the WebRTC binary transitively. PINNED to 2.15+: the CallKit audio-session coordination API
# (AudioManager.audioSession / setEngineAvailability) only exists in 2.15+, and '~> 2.0' was resolving an
# older 2.x on the build box (see the `pod install --repo-update` in codemagic.yaml).
s.dependency 'LiveKitClient', '~> 2.15'
# CallKit + PushKit + AVFoundation are system frameworks (no external pod).
s.frameworks = 'CallKit', 'PushKit', 'AVFoundation'
s.swift_version = '5.1'
@@ -54,16 +54,21 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
registry.delegate = self
registry.desiredPushTypes = [.voIP]
pushRegistry = registry
// NOTE: the CallKitLiveKit audio-session coordination (AudioManager.audioSession /
// setEngineAvailability) needs LiveKit 2.15+, but the build currently resolves an older 2.x that lacks
// those symbols. So we rely on LiveKit's default audio handling for now and only configure the session
// with plain AVFoundation in didActivate. Revisit once the pod is pinned/upgraded to 2.15+.
// CallKit audio coordination (needs LiveKit 2.15+, pinned in NativeCall.podspec). LiveKit's automatic
// AVAudioSession configuration RACES CallKit's own activation intermittent dead mic / no audio + the
// output route only settling once audio flows ("speaker turns on late"). Fix: turn auto-config OFF and
// keep the audio engine OFF; we configure the session and enable the engine ONLY in didActivate.
AudioManager.shared.audioSession.isAutomaticConfigurationEnabled = false
try? AudioManager.shared.setEngineAvailability(.none)
}
// MARK: - LiveKit media
private func connectRoom(url: String, token: String) {
guard !url.isEmpty, !token.isEmpty else { return }
// Ask for mic permission now (we still connect MUTED). If it stays "undetermined", enabling the audio
// engine in didActivate can block on first use determining it up front avoids that.
AVAudioSession.sharedInstance().requestRecordPermission { _ in }
let old = room
let r = Room()
room = r
@@ -285,12 +290,20 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
action.fulfill()
}
// Keep the last known-good audio behavior (rely on LiveKit's default session handling). We only report the
// state to JS. The proper CallKitLiveKit session coordination is deferred until the pod is on 2.15+.
// CallKit owns the AVAudioSession lifecycle. Since LiveKit auto-config is OFF, configure the session HERE
// when CallKit activates it and enable LiveKit's audio engine. This is the fix for the intermittent
// dead mic / no-audio and late speaker routing. Don't call setActive(true): CallKit already activated it.
public func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
notifyListeners("audioActivated", data: ["ok": true])
do {
try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.mixWithOthers])
try AudioManager.shared.setEngineAvailability(.default)
notifyListeners("audioActivated", data: ["ok": true])
} catch {
notifyListeners("audioActivated", data: ["ok": false, "error": String(describing: error)])
}
}
public func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
try? AudioManager.shared.setEngineAvailability(.none) // keep the engine off until the next call activates
notifyListeners("audioDeactivated", data: ["ok": true])
}
}