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:
@@ -18,9 +18,12 @@ Pod::Spec.new do |s|
|
|||||||
s.ios.deployment_target = '14.0'
|
s.ios.deployment_target = '14.0'
|
||||||
s.dependency 'Capacitor'
|
s.dependency 'Capacitor'
|
||||||
# LiveKit iOS SDK — carries the call media NATIVELY so audio survives backgrounding AND coordinates with
|
# 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;
|
# 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
|
# + setEngineAvailability in CXProvider didActivate/didDeactivate), which the WebView's WebRTC could not do.
|
||||||
# WebRTC binary transitively.
|
# 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'
|
s.dependency 'LiveKitClient', '~> 2.0'
|
||||||
# CallKit + PushKit + AVFoundation are system frameworks (no external pod).
|
# CallKit + PushKit + AVFoundation are system frameworks (no external pod).
|
||||||
s.frameworks = 'CallKit', 'PushKit', 'AVFoundation'
|
s.frameworks = 'CallKit', 'PushKit', 'AVFoundation'
|
||||||
|
|||||||
@@ -54,16 +54,21 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
|||||||
registry.delegate = self
|
registry.delegate = self
|
||||||
registry.desiredPushTypes = [.voIP]
|
registry.desiredPushTypes = [.voIP]
|
||||||
pushRegistry = registry
|
pushRegistry = registry
|
||||||
// NOTE: the CallKit↔LiveKit audio-session coordination (AudioManager.audioSession /
|
// CallKit audio coordination (LiveKit 2.15+, pinned to the git tag in ios-patch.sh). LiveKit's automatic
|
||||||
// setEngineAvailability) needs LiveKit 2.15+, but the build currently resolves an older 2.x that lacks
|
// AVAudioSession config RACES CallKit's own activation → intermittent dead mic / no audio + the output
|
||||||
// those symbols. So we rely on LiveKit's default audio handling for now and only configure the session
|
// route only settling once audio flows ("speaker turns on late"). Fix: turn auto-config OFF and keep the
|
||||||
// with plain AVFoundation in didActivate. Revisit once the pod is pinned/upgraded to 2.15+.
|
// 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
|
// MARK: - LiveKit media
|
||||||
|
|
||||||
private func connectRoom(url: String, token: String) {
|
private func connectRoom(url: String, token: String) {
|
||||||
guard !url.isEmpty, !token.isEmpty else { return }
|
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 old = room
|
||||||
let r = Room()
|
let r = Room()
|
||||||
room = r
|
room = r
|
||||||
@@ -285,12 +290,20 @@ public class NativeCallPlugin: CAPPlugin, CAPBridgedPlugin, PKPushRegistryDelega
|
|||||||
action.fulfill()
|
action.fulfill()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep the last known-good audio behavior (rely on LiveKit's default session handling). We only report the
|
// CallKit owns the AVAudioSession lifecycle. Since LiveKit auto-config is OFF, configure the session HERE —
|
||||||
// state to JS. The proper CallKit↔LiveKit session coordination is deferred until the pod is on 2.15+.
|
// 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) {
|
public func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
|
||||||
|
do {
|
||||||
|
try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.mixWithOthers])
|
||||||
|
try AudioManager.shared.setEngineAvailability(.default)
|
||||||
notifyListeners("audioActivated", data: ["ok": true])
|
notifyListeners("audioActivated", data: ["ok": true])
|
||||||
|
} catch {
|
||||||
|
notifyListeners("audioActivated", data: ["ok": false, "error": String(describing: error)])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
|
public func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
|
||||||
|
try? AudioManager.shared.setEngineAvailability(.none)
|
||||||
notifyListeners("audioDeactivated", data: ["ok": true])
|
notifyListeners("audioDeactivated", data: ["ok": true])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,6 +123,34 @@ if [ -f "$AD" ]; then
|
|||||||
node "$(dirname "$0")/inject-push.js" "$AD" || echo " (push forwarding patch skipped — non-fatal)"
|
node "$(dirname "$0")/inject-push.js" "$AD" || echo " (push forwarding patch skipped — non-fatal)"
|
||||||
fi
|
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:"
|
echo "Info.plist patched:"
|
||||||
"$PB" -c "Print :NSCameraUsageDescription" "$PLIST"
|
"$PB" -c "Print :NSCameraUsageDescription" "$PLIST"
|
||||||
"$PB" -c "Print :NSMicrophoneUsageDescription" "$PLIST"
|
"$PB" -c "Print :NSMicrophoneUsageDescription" "$PLIST"
|
||||||
|
|||||||
@@ -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],
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user