From a92cdd69f847f8f583481b4ebbb0efc17cbbd702 Mon Sep 17 00:00:00 2001 From: sravan Date: Fri, 31 Jul 2026 19:46:48 +0530 Subject: [PATCH] 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 => , :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 --- mobile/plugins/native-call/NativeCall.podspec | 9 ++- .../NativeCallPlugin/NativeCallPlugin.swift | 27 +++++-- mobile/scripts/ios-patch.sh | 28 +++++++ pkg.swift | 78 +++++++++++++++++++ probe.txt | 1 + 5 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 pkg.swift create mode 100644 probe.txt diff --git a/mobile/plugins/native-call/NativeCall.podspec b/mobile/plugins/native-call/NativeCall.podspec index ccbc86b..8a86edb 100644 --- a/mobile/plugins/native-call/NativeCall.podspec +++ b/mobile/plugins/native-call/NativeCall.podspec @@ -18,9 +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. + # 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. + # NOTE ON VERSION: this constraint stays '~> 2.0', but the actual version is pinned to 2.15.3 by a git-tag + # `pod 'LiveKitClient', :git => ...` line that ios-patch.sh injects into the generated Podfile — because the + # CallKit audio API needs 2.1+, which LiveKit publishes SPM-only (the CocoaPods TRUNK caps at 2.0.18, but the + # repo still ships a valid podspec at tag 2.15.3, and its deps are on trunk). 2.15.3 satisfies '~> 2.0'. s.dependency 'LiveKitClient', '~> 2.0' # CallKit + PushKit + AVFoundation are system frameworks (no external pod). s.frameworks = 'CallKit', 'PushKit', 'AVFoundation' diff --git a/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift b/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift index 0e794aa..6d88f64 100644 --- a/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift +++ b/mobile/plugins/native-call/ios/Sources/NativeCallPlugin/NativeCallPlugin.swift @@ -54,16 +54,21 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega registry.delegate = self registry.desiredPushTypes = [.voIP] pushRegistry = registry - // NOTE: the CallKit↔LiveKit 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 CallKit↔LiveKit 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]) } } diff --git a/mobile/scripts/ios-patch.sh b/mobile/scripts/ios-patch.sh index 3d22234..2d3f585 100644 --- a/mobile/scripts/ios-patch.sh +++ b/mobile/scripts/ios-patch.sh @@ -123,6 +123,34 @@ if [ -f "$AD" ]; then node "$(dirname "$0")/inject-push.js" "$AD" || echo " (push forwarding patch skipped — non-fatal)" fi +# ── Pin LiveKit to 2.15.3 via its Git tag (stay on CocoaPods; no SPM migration) ───────────────────── +# The CallKit audio-session coordination API (AudioManager.audioSession / setEngineAvailability) exists only +# in LiveKit 2.1+, but the LiveKitClient CocoaPod PUBLISHED to trunk caps at 2.0.18 (2.1+ is SPM-only). 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 we point the Podfile straight at the git tag. The NativeCall +# podspec's `LiveKitClient ~> 2.0` is satisfied by 2.15.3 (2.15.3 ∈ [2.0, 3.0)). Idempotent (grep guard). +PODFILE="mobile/ios/App/Podfile" +if [ -f "$PODFILE" ]; then + if grep -q "client-sdk-swift.git" "$PODFILE"; then + echo "Podfile: LiveKitClient git pin already present" + else + ruby -e ' + p = "mobile/ios/App/Podfile" + s = File.read(p) + pin = " pod \x27LiveKitClient\x27, :git => \x27https://github.com/livekit/client-sdk-swift.git\x27, :tag => \x272.15.3\x27\n" + if s =~ /target ["\x27]App["\x27] do\n/ + s = s.sub(/target ["\x27]App["\x27] do\n/) { |m| m + pin } + File.write(p, s) + puts "Podfile: pinned LiveKitClient -> git tag 2.15.3" + else + STDERR.puts "WARN: could not find \"target App do\" in Podfile — LiveKit pin NOT applied" + exit 1 + end + ' + fi + grep -n "LiveKitClient" "$PODFILE" || true +fi + echo "Info.plist patched:" "$PB" -c "Print :NSCameraUsageDescription" "$PLIST" "$PB" -c "Print :NSMicrophoneUsageDescription" "$PLIST" diff --git a/pkg.swift b/pkg.swift new file mode 100644 index 0000000..e89f53d --- /dev/null +++ b/pkg.swift @@ -0,0 +1,78 @@ +// swift-tools-version:6.1 +// (Xcode16.3+) + +import PackageDescription + +let package = Package( + name: "LiveKit", + platforms: [ + .iOS(.v13), + .macOS(.v10_15), + .macCatalyst(.v14), + .tvOS(.v17), + ], + products: [ + .library( + name: "LiveKit", + targets: ["LiveKit"], + ), + ], + dependencies: [ + // LK-Prefixed Dynamic WebRTC XCFramework + .package(url: "https://github.com/livekit/webrtc-xcframework.git", exact: "144.7559.11"), + .package(url: "https://github.com/livekit/livekit-uniffi-xcframework.git", exact: "0.0.6"), + .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.31.0"), + // Only used for DocC generation + .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.3.0"), + ], + targets: [ + .target( + name: "LKObjCHelpers", + publicHeadersPath: "include", + ), + .target( + name: "LiveKit", + dependencies: [ + .product(name: "LiveKitWebRTC", package: "webrtc-xcframework"), + .product(name: "LiveKitUniFFI", package: "livekit-uniffi-xcframework"), + .product(name: "SwiftProtobuf", package: "swift-protobuf"), + "LKObjCHelpers", + ], + exclude: [ + "Broadcast/NOTICE", + ], + resources: [ + .process("PrivacyInfo.xcprivacy"), + ], + ), + .target( + name: "LiveKitTestSupport", + dependencies: [ + "LiveKit", + ], + path: "Tests/LiveKitTestSupport", + ), + .testTarget( + name: "LiveKitCoreTests", + dependencies: [ + "LiveKit", + "LiveKitTestSupport", + ], + ), + .testTarget( + name: "LiveKitAudioTests", + dependencies: [ + "LiveKit", + "LiveKitTestSupport", + ], + ), + .testTarget( + name: "LiveKitObjCTests", + dependencies: [ + "LiveKit", + "LiveKitTestSupport", + ], + ), + ], + swiftLanguageModes: [.v5, .v6], +) diff --git a/probe.txt b/probe.txt new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/probe.txt @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file