@poolse/react-native-calls 0.4.2 → 0.4.4
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.
|
@@ -224,6 +224,7 @@ final class CallKitController: NSObject, CXProviderDelegate {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
|
227
|
+
pcTrace("CallKit answer action")
|
|
227
228
|
guard let t = calls[action.callUUID], !t.ended else { action.fail(); return }
|
|
228
229
|
let fromUs = t.answered
|
|
229
230
|
t.answered = true
|
|
@@ -259,10 +260,12 @@ final class CallKitController: NSObject, CXProviderDelegate {
|
|
|
259
260
|
}
|
|
260
261
|
|
|
261
262
|
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
|
|
263
|
+
pcTrace("CallKit didActivate \(pcSessionDescription(audioSession))")
|
|
262
264
|
actions?.callKitAudioActivated(audioSession)
|
|
263
265
|
}
|
|
264
266
|
|
|
265
267
|
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
|
|
268
|
+
pcTrace("CallKit didDeactivate")
|
|
266
269
|
actions?.callKitAudioDeactivated(audioSession)
|
|
267
270
|
}
|
|
268
271
|
}
|
|
@@ -26,6 +26,21 @@ protocol MediaEvents: AnyObject {
|
|
|
26
26
|
func onLocalCamera(on: Bool, facing: String?)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/// Step-by-step trace of the audio path, in the device log as
|
|
30
|
+
/// "[PoolseCalls] trace …" — the order of activation, engine availability,
|
|
31
|
+
/// connect and publish is what cold-answer audio failures hinge on.
|
|
32
|
+
func pcTrace(_ message: @autoclosure () -> String) {
|
|
33
|
+
NSLog("[PoolseCalls] trace %@", message())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func pcSessionDescription(_ s: AVAudioSession = .sharedInstance()) -> String {
|
|
37
|
+
let outs = s.currentRoute.outputs.map { $0.portType.rawValue }.joined(separator: ",")
|
|
38
|
+
let ins = s.currentRoute.inputs.map { $0.portType.rawValue }.joined(separator: ",")
|
|
39
|
+
return "category=\(s.category.rawValue) mode=\(s.mode.rawValue) in=[\(ins)] out=[\(outs)] " +
|
|
40
|
+
"otherAudio=\(s.isOtherAudioPlaying) inputAvailable=\(s.isInputAvailable) " +
|
|
41
|
+
"record=\(s.recordPermission.rawValue)"
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
/// The shipped MediaPort over LiveKit's Swift SDK — LiveKitMediaPort.kt
|
|
30
45
|
/// for iOS. Joins with the server-minted token, publishes the mic (and
|
|
31
46
|
/// the camera for video), tracks the roster and reports room transitions.
|
|
@@ -55,6 +70,8 @@ final class LiveKitMediaPort: NSObject, MediaPort {
|
|
|
55
70
|
private let surfaces = NSHashTable<LiveKitVideoSurface>.weakObjects()
|
|
56
71
|
private var audioPrepared = false
|
|
57
72
|
private var audioActive = false
|
|
73
|
+
/// The room whose audio engine has been kicked (once per room).
|
|
74
|
+
private var audioKicked: ObjectIdentifier?
|
|
58
75
|
|
|
59
76
|
init(events: MediaEvents) {
|
|
60
77
|
self.events = events
|
|
@@ -81,6 +98,7 @@ final class LiveKitMediaPort: NSObject, MediaPort {
|
|
|
81
98
|
func prepareAudio() {
|
|
82
99
|
guard !audioPrepared else { return }
|
|
83
100
|
audioPrepared = true
|
|
101
|
+
pcTrace("prepareAudio (automatic config off, engine .none)")
|
|
84
102
|
AudioManager.shared.audioSession.isAutomaticConfigurationEnabled = false
|
|
85
103
|
do { try AudioManager.shared.setEngineAvailability(.none) } catch {
|
|
86
104
|
NSLog("[PoolseCalls] setEngineAvailability(.none) failed: %@", "\(error)")
|
|
@@ -89,10 +107,54 @@ final class LiveKitMediaPort: NSObject, MediaPort {
|
|
|
89
107
|
|
|
90
108
|
/// CallKit activated the session (or, without CallKit, we did).
|
|
91
109
|
func audioActivated(_ session: AVAudioSession, media: String) {
|
|
110
|
+
pcTrace("audioActivated media=\(media) room=\(room != nil) connected=\(connectedRoom != nil) \(pcSessionDescription(session))")
|
|
92
111
|
prepareAudio()
|
|
93
112
|
audioActive = true
|
|
94
113
|
if media != self.media && room == nil { self.media = media }
|
|
95
114
|
reassertAudio(session)
|
|
115
|
+
kickAudioIfReady()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Once per call, the first moment the session is active AND the mic is
|
|
119
|
+
/// published (in whichever order they happen): make LiveKit's audio
|
|
120
|
+
/// engine rebuild around the live mic.
|
|
121
|
+
///
|
|
122
|
+
/// On a call answered with the app closed, the process is brand new and
|
|
123
|
+
/// LiveKit's engine never started for a mic published right after
|
|
124
|
+
/// CallKit made audio available — no capture, no playback, though the
|
|
125
|
+
/// session was correct (video, which needs no audio engine, worked). A
|
|
126
|
+
/// user-side mute → unmute cured it, because toggling the mic makes the
|
|
127
|
+
/// engine re-evaluate and start. This does the same, automatically: an
|
|
128
|
+
/// availability cycle first, then a mic off/on. A deliberate mute is
|
|
129
|
+
/// left alone. The remote hears at most a sub-second blip at connect.
|
|
130
|
+
private func kickAudioIfReady() {
|
|
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)")
|
|
141
|
+
}
|
|
142
|
+
Task { [weak self] in
|
|
143
|
+
try? await Task.sleep(nanoseconds: 250_000_000)
|
|
144
|
+
let still = await MainActor.run { () -> Bool in
|
|
145
|
+
guard let self = self else { return false }
|
|
146
|
+
return self.room === r && self.audioActive && !self.mutedWanted
|
|
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() }
|
|
157
|
+
}
|
|
96
158
|
}
|
|
97
159
|
|
|
98
160
|
/// Put the call's audio back the way the call needs it: play-and-record
|
|
@@ -104,6 +166,7 @@ final class LiveKitMediaPort: NSObject, MediaPort {
|
|
|
104
166
|
/// no-CallKit fallback) holds the session active.
|
|
105
167
|
func reassertAudio(_ session: AVAudioSession = .sharedInstance()) {
|
|
106
168
|
guard audioActive else { return }
|
|
169
|
+
pcTrace("reassertAudio before: \(pcSessionDescription(session))")
|
|
107
170
|
let mode: AVAudioSession.Mode = media == "video" && speakerWanted != false ? .videoChat : .voiceChat
|
|
108
171
|
if session.category != .playAndRecord || session.mode != mode {
|
|
109
172
|
do {
|
|
@@ -120,6 +183,7 @@ final class LiveKitMediaPort: NSObject, MediaPort {
|
|
|
120
183
|
}
|
|
121
184
|
|
|
122
185
|
func audioDeactivated() {
|
|
186
|
+
pcTrace("audioDeactivated")
|
|
123
187
|
audioActive = false
|
|
124
188
|
guard audioPrepared else { return }
|
|
125
189
|
do { try AudioManager.shared.setEngineAvailability(.none) } catch {
|
|
@@ -138,6 +202,7 @@ final class LiveKitMediaPort: NSObject, MediaPort {
|
|
|
138
202
|
// MARK: - MediaPort
|
|
139
203
|
|
|
140
204
|
func join(bundle: CallSessionBundle) {
|
|
205
|
+
pcTrace("join call=\(bundle.callId) media=\(bundle.media) audioActive=\(audioActive)")
|
|
141
206
|
prepareAudio()
|
|
142
207
|
if let joined = joinedCallId, joined != bundle.callId { resetWanted() }
|
|
143
208
|
joinedCallId = bundle.callId
|
|
@@ -181,12 +246,19 @@ final class LiveKitMediaPort: NSObject, MediaPort {
|
|
|
181
246
|
return
|
|
182
247
|
}
|
|
183
248
|
let muted = await MainActor.run { self.mutedWanted }
|
|
249
|
+
let activeNow = await MainActor.run { self.audioActive }
|
|
250
|
+
pcTrace("connected; publishing mic muted=\(muted) audioActive=\(activeNow)")
|
|
184
251
|
do { try await r.localParticipant.setMicrophone(enabled: !muted) } catch {
|
|
185
252
|
NSLog("[PoolseCalls] microphone publish failed: %@", "\(error)")
|
|
186
253
|
}
|
|
187
254
|
// Connected and publishing: make sure nothing changed the session
|
|
188
|
-
|
|
189
|
-
|
|
255
|
+
pcTrace("mic published enabled=\(r.localParticipant.isMicrophoneEnabled()) \(pcSessionDescription())")
|
|
256
|
+
// while we connected (a cold answer boots JS in exactly this window),
|
|
257
|
+
// then make sure the engine is actually carrying the mic.
|
|
258
|
+
await MainActor.run {
|
|
259
|
+
self.reassertAudio()
|
|
260
|
+
self.kickAudioIfReady()
|
|
261
|
+
}
|
|
190
262
|
let camera = await MainActor.run { self.cameraWanted ?? wantsVideo }
|
|
191
263
|
if camera { await self.enableCamera(r, true) }
|
|
192
264
|
await MainActor.run {
|
|
@@ -328,18 +400,21 @@ final class LiveKitMediaPort: NSObject, MediaPort {
|
|
|
328
400
|
/// An interruption (another app's audio, Siri) ended: the session may
|
|
329
401
|
/// have been reconfigured; restore the call's.
|
|
330
402
|
@objc private func interrupted(_ note: Notification) {
|
|
403
|
+
pcTrace("interruption \(note.userInfo ?? [:])")
|
|
331
404
|
guard let raw = note.userInfo?[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
332
405
|
AVAudioSession.InterruptionType(rawValue: raw) == .ended else { return }
|
|
333
406
|
DispatchQueue.main.async { [weak self] in self?.reassertAudio() }
|
|
334
407
|
}
|
|
335
408
|
|
|
336
409
|
@objc private func mediaServicesReset(_ note: Notification) {
|
|
410
|
+
pcTrace("mediaServicesWereReset")
|
|
337
411
|
DispatchQueue.main.async { [weak self] in self?.reassertAudio() }
|
|
338
412
|
}
|
|
339
413
|
|
|
340
414
|
@objc private func routeChanged(_ note: Notification) {
|
|
341
415
|
let reason = (note.userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt)
|
|
342
416
|
.flatMap(AVAudioSession.RouteChangeReason.init(rawValue:))
|
|
417
|
+
pcTrace("routeChange reason=\(reason.map { String($0.rawValue) } ?? "?") \(pcSessionDescription())")
|
|
343
418
|
DispatchQueue.main.async { [weak self] in
|
|
344
419
|
guard let self = self, self.audioActive else { return }
|
|
345
420
|
// Something reconfigured the shared session mid-call: put it back.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import CallKit
|
|
2
2
|
import Foundation
|
|
3
3
|
import PushKit
|
|
4
|
+
import UIKit
|
|
4
5
|
|
|
5
6
|
/// Native PushKit intake — the iOS twin of CallMessagingService. The
|
|
6
7
|
/// registry is created at launch (PoolseCallsAppDelegateSubscriber →
|
|
@@ -59,6 +60,7 @@ final class VoipPush: NSObject, PKPushRegistryDelegate {
|
|
|
59
60
|
|
|
60
61
|
func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload,
|
|
61
62
|
for type: PKPushType, completion: @escaping () -> Void) {
|
|
63
|
+
pcTrace("VoIP push received appState=\(UIApplication.shared.applicationState.rawValue)")
|
|
62
64
|
if type == .voIP { handle(payload.dictionaryPayload) }
|
|
63
65
|
completion()
|
|
64
66
|
}
|
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.4",
|
|
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",
|