@poolse/react-native-calls 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -171,6 +171,12 @@ final class CallCore {
|
|
|
171
171
|
// LiveKit's audio stays off until CallKit activates the session —
|
|
172
172
|
// configured now, at launch, before any activation can happen.
|
|
173
173
|
mediaPort.prepareAudio()
|
|
174
|
+
// A no-op mute transaction (same state) makes CallKit retry a failed
|
|
175
|
+
// audio-session activation.
|
|
176
|
+
mediaPort.requestAudioActivation = { [weak self] in
|
|
177
|
+
guard let self = self, let callId = self.runtime.currentState().callId else { return }
|
|
178
|
+
self.callKit.requestMute(callId: callId, muted: self.mutedSelf)
|
|
179
|
+
}
|
|
174
180
|
}
|
|
175
181
|
|
|
176
182
|
// MARK: - native entry points
|
|
@@ -86,6 +86,25 @@ final class CallKitController: NSObject, CXProviderDelegate {
|
|
|
86
86
|
/// or the runtime's SHOW_RING after the push path) is re-reported with
|
|
87
87
|
/// the SAME UUID: CallKit rejects it as a duplicate and nothing changes,
|
|
88
88
|
/// which still satisfies PushKit's "report a call for every push".
|
|
89
|
+
/// Put the shared session in the call's category BEFORE CallKit activates
|
|
90
|
+
/// it — at ring time, in the answer action, in the start action (Apple's
|
|
91
|
+
/// CallKit sample does the same). Setting the category does not activate
|
|
92
|
+
/// anything. Without it the session is still the default
|
|
93
|
+
/// SoloAmbientSound when CallKit activates it, and with the phone locked
|
|
94
|
+
/// and the app in the background iOS REFUSES that activation (seen in a
|
|
95
|
+
/// device log as an activate "early exit due to failure", -16980): no
|
|
96
|
+
/// didActivate, so no audio at all on a call answered with the app closed.
|
|
97
|
+
static func prepareSessionForCall(video: Bool) {
|
|
98
|
+
let s = AVAudioSession.sharedInstance()
|
|
99
|
+
let mode: AVAudioSession.Mode = video ? .videoChat : .voiceChat
|
|
100
|
+
guard s.category != .playAndRecord || s.mode != mode else { return }
|
|
101
|
+
do {
|
|
102
|
+
try s.setCategory(.playAndRecord, mode: mode, options: [.allowBluetooth, .allowBluetoothA2DP])
|
|
103
|
+
} catch {
|
|
104
|
+
NSLog("[PoolseCalls] pre-activation category failed: %@", "\(error)")
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
89
108
|
func reportIncoming(ring: RingContext) {
|
|
90
109
|
let hasVideo = ring.media == "video"
|
|
91
110
|
let title = ring.conversationName ?? ring.callerName
|
|
@@ -95,6 +114,7 @@ final class CallKitController: NSObject, CXProviderDelegate {
|
|
|
95
114
|
}
|
|
96
115
|
let t = Tracked(uuid: UUID(), callId: ring.callId, outgoing: false, hasVideo: hasVideo, title: title)
|
|
97
116
|
calls[t.uuid] = t
|
|
117
|
+
Self.prepareSessionForCall(video: hasVideo)
|
|
98
118
|
provider.reportNewIncomingCall(with: t.uuid, update: update(title: title, handle: ring.conversationId, video: hasVideo)) { [weak self] error in
|
|
99
119
|
guard let error = error else { return }
|
|
100
120
|
NSLog("[PoolseCalls] CallKit refused incoming call %@: %@", ring.callId, error.localizedDescription)
|
|
@@ -228,6 +248,8 @@ final class CallKitController: NSObject, CXProviderDelegate {
|
|
|
228
248
|
guard let t = calls[action.callUUID], !t.ended else { action.fail(); return }
|
|
229
249
|
let fromUs = t.answered
|
|
230
250
|
t.answered = true
|
|
251
|
+
// Before fulfil: CallKit activates the session right after.
|
|
252
|
+
Self.prepareSessionForCall(video: t.hasVideo)
|
|
231
253
|
if !fromUs {
|
|
232
254
|
guard let actions = actions else { action.fail(); return }
|
|
233
255
|
actions.callKitAnswer(callId: t.callId)
|
|
@@ -245,7 +267,8 @@ final class CallKitController: NSObject, CXProviderDelegate {
|
|
|
245
267
|
}
|
|
246
268
|
|
|
247
269
|
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
|
248
|
-
guard calls[action.callUUID]
|
|
270
|
+
guard let t = calls[action.callUUID] else { action.fail(); return }
|
|
271
|
+
Self.prepareSessionForCall(video: t.hasVideo)
|
|
249
272
|
provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil)
|
|
250
273
|
action.fulfill()
|
|
251
274
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import AVFoundation
|
|
2
|
+
import os
|
|
2
3
|
import Foundation
|
|
3
4
|
#if canImport(LiveKitClient)
|
|
4
5
|
import LiveKitClient // CocoaPods module name
|
|
@@ -29,10 +30,15 @@ protocol MediaEvents: AnyObject {
|
|
|
29
30
|
/// Step-by-step trace of the audio path, in the device log as
|
|
30
31
|
/// "[PoolseCalls] trace …" — the order of activation, engine availability,
|
|
31
32
|
/// connect and publish is what cold-answer audio failures hinge on.
|
|
33
|
+
///
|
|
34
|
+
/// os_log with a PUBLIC argument: NSLog's text is redacted to "<private>"
|
|
35
|
+
/// on release builds, which made the first device trace unreadable.
|
|
32
36
|
func pcTrace(_ message: @autoclosure () -> String) {
|
|
33
|
-
|
|
37
|
+
os_log("[PoolseCalls] trace %{public}@", log: pcLog, type: .default, message())
|
|
34
38
|
}
|
|
35
39
|
|
|
40
|
+
let pcLog = OSLog(subsystem: "dev.poolse.callcore", category: "calls")
|
|
41
|
+
|
|
36
42
|
func pcSessionDescription(_ s: AVAudioSession = .sharedInstance()) -> String {
|
|
37
43
|
let outs = s.currentRoute.outputs.map { $0.portType.rawValue }.joined(separator: ",")
|
|
38
44
|
let ins = s.currentRoute.inputs.map { $0.portType.rawValue }.joined(separator: ",")
|
|
@@ -70,8 +76,9 @@ final class LiveKitMediaPort: NSObject, MediaPort {
|
|
|
70
76
|
private let surfaces = NSHashTable<LiveKitVideoSurface>.weakObjects()
|
|
71
77
|
private var audioPrepared = false
|
|
72
78
|
private var audioActive = false
|
|
73
|
-
///
|
|
74
|
-
|
|
79
|
+
/// Asks CallKit to (re)try activating the audio session — set by
|
|
80
|
+
/// CallCore. Used when a room is live but activation never arrived.
|
|
81
|
+
var requestAudioActivation: (() -> Void)?
|
|
75
82
|
|
|
76
83
|
init(events: MediaEvents) {
|
|
77
84
|
self.events = events
|
|
@@ -112,48 +119,28 @@ final class LiveKitMediaPort: NSObject, MediaPort {
|
|
|
112
119
|
audioActive = true
|
|
113
120
|
if media != self.media && room == nil { self.media = media }
|
|
114
121
|
reassertAudio(session)
|
|
115
|
-
kickAudioIfReady()
|
|
116
122
|
}
|
|
117
123
|
|
|
118
|
-
///
|
|
119
|
-
///
|
|
120
|
-
///
|
|
121
|
-
///
|
|
122
|
-
///
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
guard audioActive, let r = room, connectedRoom == ObjectIdentifier(r),
|
|
132
|
-
audioKicked != ObjectIdentifier(r), !mutedWanted else { return }
|
|
133
|
-
guard r.localParticipant.isMicrophoneEnabled() else { return }
|
|
134
|
-
audioKicked = ObjectIdentifier(r)
|
|
135
|
-
pcTrace("kick: cycling engine availability + mic")
|
|
136
|
-
do {
|
|
137
|
-
try AudioManager.shared.setEngineAvailability(.none)
|
|
138
|
-
try AudioManager.shared.setEngineAvailability(.default)
|
|
139
|
-
} catch {
|
|
140
|
-
NSLog("[PoolseCalls] engine availability cycle failed: %@", "\(error)")
|
|
124
|
+
/// Connected with a live mic but CallKit never activated the session
|
|
125
|
+
/// (iOS refused the activation): nudge CallKit to retry — a transaction
|
|
126
|
+
/// on the call makes it attempt activation again (in the device log a
|
|
127
|
+
/// mute toggle did exactly that, and audio came up). Three tries, then
|
|
128
|
+
/// activate the session ourselves as a last resort.
|
|
129
|
+
private func ensureAudioActivated(_ r: Room, attempt: Int) {
|
|
130
|
+
guard room === r, !audioActive else { return }
|
|
131
|
+
let delays: [Double] = [1.2, 2.5, 4.0]
|
|
132
|
+
guard attempt <= delays.count else { return }
|
|
133
|
+
if attempt == delays.count {
|
|
134
|
+
pcTrace("audio never activated by CallKit; activating ourselves")
|
|
135
|
+
activateAudioWithoutCallKit(media: media)
|
|
136
|
+
return
|
|
141
137
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
guard still else { return }
|
|
149
|
-
do {
|
|
150
|
-
try await r.localParticipant.setMicrophone(enabled: false)
|
|
151
|
-
try await r.localParticipant.setMicrophone(enabled: true)
|
|
152
|
-
} catch {
|
|
153
|
-
NSLog("[PoolseCalls] mic cycle failed: %@", "\(error)")
|
|
154
|
-
}
|
|
155
|
-
pcTrace("kick done enabled=\(r.localParticipant.isMicrophoneEnabled()) \(pcSessionDescription())")
|
|
156
|
-
await MainActor.run { self?.reassertAudio() }
|
|
138
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + delays[attempt]) { [weak self] in
|
|
139
|
+
guard let self = self, self.room === r, !self.audioActive else { return }
|
|
140
|
+
pcTrace("audio not active \(attempt + 1)x after connect; nudging CallKit")
|
|
141
|
+
CallKitController.prepareSessionForCall(video: self.media == "video")
|
|
142
|
+
self.requestAudioActivation?()
|
|
143
|
+
self.ensureAudioActivated(r, attempt: attempt + 1)
|
|
157
144
|
}
|
|
158
145
|
}
|
|
159
146
|
|
|
@@ -257,7 +244,7 @@ final class LiveKitMediaPort: NSObject, MediaPort {
|
|
|
257
244
|
// then make sure the engine is actually carrying the mic.
|
|
258
245
|
await MainActor.run {
|
|
259
246
|
self.reassertAudio()
|
|
260
|
-
self.
|
|
247
|
+
self.ensureAudioActivated(r, attempt: 0)
|
|
261
248
|
}
|
|
262
249
|
let camera = await MainActor.run { self.cameraWanted ?? wantsVideo }
|
|
263
250
|
if camera { await self.enableCamera(r, true) }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poolse/react-native-calls",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "Native call core for poolse — Kotlin/Swift calling engine with a thin React Native bridge. Ring, answer and carry audio with the app killed and the phone locked.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|