Native calls: get LiveKit 2.15.3 via git-tag pin (stay on CocoaPods) + audio fix

The LiveKitClient CocoaPod on trunk caps at 2.0.18 (2.1+ is SPM-only), so the
2.15 CallKit audio API was unreachable. BUT the repo still ships a valid podspec
at tag 2.15.3, and its deps (LiveKitWebRTC 144.7559.11, LiveKitUniFFI 0.0.6,
SwiftProtobuf) ARE on trunk. So instead of a risky SPM migration:
- ios-patch.sh injects `pod 'LiveKitClient', :git => <repo>, :tag => '2.15.3'`
  into the generated Podfile (after cap sync, before pod install). The NativeCall
  podspec's '~> 2.0' is satisfied by 2.15.3. Idempotent; hard-fails if it can't
  find the App target so we never silently fall back to 2.0.18.
- Plugin re-adds the CallKit<->LiveKit audio-session coordination, now compilable:
  auto-config OFF + engine OFF at load; configure session + enable engine in
  didActivate; disable in didDeactivate; request mic permission on connect.

Core Room APIs (connect/disconnect/setMicrophone) verified compatible between
2.0.18 and 2.15.3 against the real source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 19:46:48 +05:30
parent c3de5ac8e3
commit a92cdd69f8
5 changed files with 133 additions and 10 deletions
@@ -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 (LiveKit 2.15+, pinned to the git tag in ios-patch.sh). LiveKit's automatic
// AVAudioSession config 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
// 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)
notifyListeners("audioDeactivated", data: ["ok": true])
}
}