@stream-io/react-native-callingx 0.4.0 → 0.5.0
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.
- package/CHANGELOG.md +6 -0
- package/dist/module/spec/NativeCallingx.js.map +1 -1
- package/dist/typescript/src/spec/NativeCallingx.d.ts +9 -2
- package/dist/typescript/src/spec/NativeCallingx.d.ts.map +1 -1
- package/dist/typescript/src/types.d.ts +17 -6
- package/dist/typescript/src/types.d.ts.map +1 -1
- package/ios/AudioSessionManager.swift +49 -17
- package/ios/Callingx.mm +5 -2
- package/ios/CallingxCall.swift +3 -5
- package/ios/CallingxImpl.swift +266 -210
- package/ios/CallingxLog.swift +67 -0
- package/ios/CallingxSessionOwnership.swift +37 -0
- package/ios/Settings.swift +2 -6
- package/ios/UUIDStorage.swift +10 -30
- package/ios/VoipNotificationsManager.swift +9 -23
- package/ios/VoipPushHandler.m +6 -14
- package/package.json +3 -3
- package/src/spec/NativeCallingx.ts +9 -2
- package/src/types.ts +13 -6
package/ios/CallingxImpl.swift
CHANGED
|
@@ -2,6 +2,7 @@ import Foundation
|
|
|
2
2
|
import CallKit
|
|
3
3
|
import AVFoundation
|
|
4
4
|
import UIKit
|
|
5
|
+
import Combine
|
|
5
6
|
import stream_react_native_webrtc
|
|
6
7
|
|
|
7
8
|
// MARK: - Event Names
|
|
@@ -10,6 +11,7 @@ import stream_react_native_webrtc
|
|
|
10
11
|
public static let didToggleHoldAction = "didToggleHoldCallAction"
|
|
11
12
|
public static let didPerformSetMutedCallAction = "didPerformSetMutedCallAction"
|
|
12
13
|
public static let didChangeAudioRoute = "didChangeAudioRoute"
|
|
14
|
+
public static let didAudioInterruption = "didAudioInterruption"
|
|
13
15
|
public static let didDisplayIncomingCall = "didDisplayIncomingCall"
|
|
14
16
|
public static let didActivateAudioSession = "didActivateAudioSession"
|
|
15
17
|
public static let didDeactivateAudioSession = "didDeactivateAudioSession"
|
|
@@ -25,23 +27,30 @@ import stream_react_native_webrtc
|
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
// MARK: - Callingx Implementation
|
|
28
|
-
@objc public class CallingxImpl: NSObject, CXProviderDelegate {
|
|
29
|
-
|
|
30
|
+
@objc public class CallingxImpl: NSObject, CXProviderDelegate, RTCAudioSessionDelegate {
|
|
31
|
+
|
|
30
32
|
// MARK: - Shared State
|
|
31
33
|
@objc public static var sharedProvider: CXProvider?
|
|
32
34
|
@objc public static var uuidStorage: UUIDStorage?
|
|
33
35
|
@objc public static var sharedInstance: CallingxImpl?
|
|
34
36
|
/// Events stored before the module instance exists (e.g. VoIP from killed state). Drained in getInitialEvents().
|
|
35
37
|
private static var delayedEvents: [[String: Any]] = []
|
|
36
|
-
|
|
38
|
+
|
|
37
39
|
// MARK: - Instance Properties
|
|
38
40
|
@objc public var callKeepCallController: CXCallController?
|
|
39
41
|
@objc public var callKeepProvider: CXProvider?
|
|
40
42
|
@objc public weak var eventEmitter: CallingxEventEmitter?
|
|
41
43
|
@objc public weak var webRTCModule: WebRTCModule?
|
|
42
|
-
|
|
44
|
+
|
|
43
45
|
private var canSendEvents: Bool = false
|
|
44
46
|
private var isSetup: Bool = false
|
|
47
|
+
/// Combine subscription to the AudioDeviceModule's engine-lifecycle publisher.
|
|
48
|
+
/// Wired lazily in `setup()` because `webRTCModule` (the ADM source) is injected
|
|
49
|
+
/// from the TurboModule host after `init`.
|
|
50
|
+
private var engineSubscription: AnyCancellable?
|
|
51
|
+
/// The ADM `engineSubscription` is bound to. Tracked so we can detect a new ADM
|
|
52
|
+
/// (a JS reload recreates WebRTCModule) and re-wire instead of staying on a dead publisher.
|
|
53
|
+
private weak var subscribedADM: AudioDeviceModule?
|
|
45
54
|
|
|
46
55
|
// Pending CXActions awaiting JS fulfillment
|
|
47
56
|
private var pendingAnswerActions: [String: (action: CXAnswerCallAction, enqueuedAt: DispatchTime)] = [:]
|
|
@@ -50,6 +59,21 @@ import stream_react_native_webrtc
|
|
|
50
59
|
// a large timeout to accomodate for cold start + metro server load time
|
|
51
60
|
private let pendingActionTimeoutSeconds = 30
|
|
52
61
|
|
|
62
|
+
/// UUIDs of mute actions the app requested via `setMutedCall`. Lets the perform delegate skip
|
|
63
|
+
/// app-initiated mutes (vs system ones from the native CallKit UI). A set so concurrent toggles
|
|
64
|
+
/// each match their own UUID. Guarded by `pendingActionsQueue`.
|
|
65
|
+
private var appInitiatedMuteActionIds: Set<UUID> = []
|
|
66
|
+
|
|
67
|
+
/// `true` while the audio engine is starting. Startup toggles `voiceProcessingInputMuted`, which
|
|
68
|
+
/// iOS 17+ surfaces as system-initiated mutes — artifacts, not user intent, so we skip them.
|
|
69
|
+
/// Set on `willEnableAudioEngine`, cleared on `willStartAudioEngine`. Guarded by `pendingActionsQueue`.
|
|
70
|
+
private var isAudioEngineStarting = false
|
|
71
|
+
|
|
72
|
+
/// Mute value of the last app-requested `CXSetMutedCallAction`. iOS 17+ round-trips it back as a
|
|
73
|
+
/// system-initiated action of the same value; we skip that echo (a real UI toggle flips the value).
|
|
74
|
+
/// Reset when the call ends. Guarded by `pendingActionsQueue`.
|
|
75
|
+
private var lastAppRequestedMute: Bool?
|
|
76
|
+
|
|
53
77
|
@objc public static func getSharedInstance() -> CallingxImpl {
|
|
54
78
|
if sharedInstance == nil {
|
|
55
79
|
sharedInstance = CallingxImpl()
|
|
@@ -64,14 +88,22 @@ import stream_react_native_webrtc
|
|
|
64
88
|
|
|
65
89
|
isSetup = false
|
|
66
90
|
canSendEvents = false
|
|
67
|
-
|
|
91
|
+
|
|
92
|
+
// Route changes go through RTCAudioSessionDelegate (fires after WebRTC's
|
|
93
|
+
// internal bookkeeping, so we don't need to defensively re-read currentRoute).
|
|
94
|
+
RTCAudioSession.sharedInstance().add(self)
|
|
95
|
+
|
|
96
|
+
// Interruptions stay on NSNotificationCenter: the delegate's
|
|
97
|
+
// `audioSessionDidBeginInterruption:` callback doesn't carry userInfo, and
|
|
98
|
+
// `AVAudioSessionInterruptionReasonKey` (which we branch on for hardware
|
|
99
|
+
// mic-mute / route-disconnect) lives there.
|
|
68
100
|
NotificationCenter.default.addObserver(
|
|
69
101
|
self,
|
|
70
|
-
selector: #selector(
|
|
71
|
-
name: AVAudioSession.
|
|
102
|
+
selector: #selector(onAudioInterruption(_:)),
|
|
103
|
+
name: AVAudioSession.interruptionNotification,
|
|
72
104
|
object: nil
|
|
73
105
|
)
|
|
74
|
-
|
|
106
|
+
|
|
75
107
|
CallingxImpl.sharedInstance = self
|
|
76
108
|
|
|
77
109
|
if CallingxImpl.uuidStorage == nil {
|
|
@@ -88,8 +120,11 @@ import stream_react_native_webrtc
|
|
|
88
120
|
}
|
|
89
121
|
|
|
90
122
|
deinit {
|
|
123
|
+
RTCAudioSession.sharedInstance().remove(self)
|
|
91
124
|
NotificationCenter.default.removeObserver(self)
|
|
92
|
-
|
|
125
|
+
engineSubscription?.cancel()
|
|
126
|
+
engineSubscription = nil
|
|
127
|
+
|
|
93
128
|
callKeepProvider?.setDelegate(nil, queue: nil)
|
|
94
129
|
callKeepProvider?.invalidate()
|
|
95
130
|
CallingxImpl.sharedProvider = nil
|
|
@@ -122,9 +157,7 @@ import stream_react_native_webrtc
|
|
|
122
157
|
guard let storage = uuidStorage else { return }
|
|
123
158
|
|
|
124
159
|
if storage.containsCid(callId) {
|
|
125
|
-
|
|
126
|
-
NSLog("%@","[Callingx][reportNewIncomingCall] callId already exists")
|
|
127
|
-
#endif
|
|
160
|
+
CallingxLog.core.debugPublic("[reportNewIncomingCall] callId already exists")
|
|
128
161
|
completion?()
|
|
129
162
|
resolve?(true)
|
|
130
163
|
return
|
|
@@ -142,10 +175,7 @@ import stream_react_native_webrtc
|
|
|
142
175
|
callUpdate.localizedCallerName = localizedCallerName
|
|
143
176
|
|
|
144
177
|
sharedProvider?.reportNewIncomingCall(with: uuid, update: callUpdate) { error in
|
|
145
|
-
|
|
146
|
-
NSLog("%@","[Callingx][reportNewIncomingCall] callId = \(callId), error = \(String(describing: error))")
|
|
147
|
-
#endif
|
|
148
|
-
|
|
178
|
+
CallingxLog.core.debugPublic("[reportNewIncomingCall] callId = \(callId), error = \(String(describing: error))")
|
|
149
179
|
let errorCode = error != nil ? CallingxImpl.getIncomingCallErrorCode(error!) : ""
|
|
150
180
|
|
|
151
181
|
let body = [
|
|
@@ -167,9 +197,7 @@ import stream_react_native_webrtc
|
|
|
167
197
|
}
|
|
168
198
|
|
|
169
199
|
if error == nil {
|
|
170
|
-
|
|
171
|
-
NSLog("%@","[Callingx][reportNewIncomingCall] success callId = \(callId)")
|
|
172
|
-
#endif
|
|
200
|
+
CallingxLog.core.debugPublic("[reportNewIncomingCall] success callId = \(callId)")
|
|
173
201
|
resolve?(true)
|
|
174
202
|
} else {
|
|
175
203
|
reject?("DISPLAY_INCOMING_CALL_ERROR", error?.localizedDescription, error)
|
|
@@ -211,14 +239,10 @@ import stream_react_native_webrtc
|
|
|
211
239
|
}
|
|
212
240
|
|
|
213
241
|
@objc public static func endCall(_ callId: String, reason: Int) {
|
|
214
|
-
|
|
215
|
-
NSLog("%@","[Callingx][endCall] callId = \(callId) reason = \(reason)")
|
|
216
|
-
#endif
|
|
242
|
+
CallingxLog.core.debugPublic("[endCall] callId = \(callId) reason = \(reason)")
|
|
217
243
|
|
|
218
244
|
guard let call = uuidStorage?.getCall(forCid: callId) else {
|
|
219
|
-
|
|
220
|
-
NSLog("%@","[Callingx][endCall] callId not found")
|
|
221
|
-
#endif
|
|
245
|
+
CallingxLog.core.debugPublic("[endCall] callId not found")
|
|
222
246
|
return
|
|
223
247
|
}
|
|
224
248
|
|
|
@@ -249,9 +273,7 @@ import stream_react_native_webrtc
|
|
|
249
273
|
|
|
250
274
|
// MARK: - Instance Methods
|
|
251
275
|
@objc public func requestTransaction(_ transaction: CXTransaction) {
|
|
252
|
-
|
|
253
|
-
NSLog("%@","[Callingx][requestTransaction] transaction = \(transaction)")
|
|
254
|
-
#endif
|
|
276
|
+
CallingxLog.core.debugPublic("[requestTransaction] transaction = \(transaction)")
|
|
255
277
|
|
|
256
278
|
if callKeepCallController == nil {
|
|
257
279
|
callKeepCallController = CXCallController()
|
|
@@ -259,21 +281,22 @@ import stream_react_native_webrtc
|
|
|
259
281
|
|
|
260
282
|
callKeepCallController?.request(transaction) { [weak self] error in
|
|
261
283
|
if let error = error {
|
|
262
|
-
|
|
263
|
-
NSLog("%@","[Callingx][requestTransaction] Error requesting transaction (\(transaction.actions)): (\(error))")
|
|
264
|
-
#endif
|
|
284
|
+
CallingxLog.core.errorPublic("[requestTransaction] Error requesting transaction (\(transaction.actions)): (\(error))")
|
|
265
285
|
|
|
266
286
|
// Reset per-call action-source flags for all actions in the failed transaction
|
|
267
287
|
for action in transaction.actions {
|
|
288
|
+
if let mutedAction = action as? CXSetMutedCallAction {
|
|
289
|
+
self?.pendingActionsQueue.sync {
|
|
290
|
+
_ = self?.appInitiatedMuteActionIds.remove(mutedAction.uuid)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
268
293
|
if let callAction = action as? CXCallAction,
|
|
269
294
|
let call = CallingxImpl.uuidStorage?.getCallByUUID(callAction.callUUID) {
|
|
270
295
|
call.resetAllSelfFlags()
|
|
271
296
|
}
|
|
272
297
|
}
|
|
273
298
|
} else {
|
|
274
|
-
|
|
275
|
-
NSLog("%@","[Callingx][requestTransaction] Requested transaction successfully")
|
|
276
|
-
#endif
|
|
299
|
+
CallingxLog.core.debugPublic("[requestTransaction] Requested transaction successfully")
|
|
277
300
|
|
|
278
301
|
if let startCallAction = transaction.actions.first as? CXStartCallAction {
|
|
279
302
|
let callUpdate = CXCallUpdate()
|
|
@@ -292,9 +315,7 @@ import stream_react_native_webrtc
|
|
|
292
315
|
}
|
|
293
316
|
|
|
294
317
|
@objc public func sendEvent(_ name: String, body: [String: Any]?) {
|
|
295
|
-
|
|
296
|
-
NSLog("%@","[Callingx] sendEventWithNameWrapper: \(name)")
|
|
297
|
-
#endif
|
|
318
|
+
CallingxLog.core.debugPublic("sendEventWithNameWrapper: \(name)")
|
|
298
319
|
|
|
299
320
|
let sendEventAction = {
|
|
300
321
|
var dictionary: [String: Any] = ["eventName": name]
|
|
@@ -306,9 +327,7 @@ import stream_react_native_webrtc
|
|
|
306
327
|
self.eventEmitter?.emitEvent(dictionary)
|
|
307
328
|
} else {
|
|
308
329
|
CallingxImpl.delayedEvents.append(dictionary)
|
|
309
|
-
|
|
310
|
-
NSLog("%@","[Callingx] delayedEvents: \(CallingxImpl.delayedEvents)")
|
|
311
|
-
#endif
|
|
330
|
+
CallingxLog.core.debugPublic("delayedEvents: \(CallingxImpl.delayedEvents)")
|
|
312
331
|
}
|
|
313
332
|
}
|
|
314
333
|
|
|
@@ -321,27 +340,91 @@ import stream_react_native_webrtc
|
|
|
321
340
|
}
|
|
322
341
|
}
|
|
323
342
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
343
|
+
// MARK: - RTCAudioSessionDelegate
|
|
344
|
+
|
|
345
|
+
public func audioSessionDidChangeRoute(_ session: RTCAudioSession,
|
|
346
|
+
reason: AVAudioSession.RouteChangeReason,
|
|
347
|
+
previousRoute: AVAudioSessionRouteDescription) {
|
|
348
|
+
guard let output = CallingxImpl.getAudioOutput() else {
|
|
328
349
|
return
|
|
329
350
|
}
|
|
330
|
-
|
|
351
|
+
|
|
331
352
|
let params: [String: Any] = [
|
|
332
353
|
"output": output,
|
|
333
|
-
"reason":
|
|
354
|
+
"reason": reason.rawValue
|
|
334
355
|
]
|
|
335
|
-
|
|
356
|
+
|
|
336
357
|
sendEvent(CallingxEvents.didChangeAudioRoute, body: params)
|
|
337
358
|
}
|
|
338
|
-
|
|
359
|
+
|
|
360
|
+
// MARK: - Audio Session Interruption
|
|
361
|
+
|
|
362
|
+
// Observability + JS-event only; audio recovery is WebRTC's: AudioEngineDevice
|
|
363
|
+
// restarts the engine on interruption-end. We do not touch the session here.
|
|
364
|
+
@objc private func onAudioInterruption(_ notification: Notification) {
|
|
365
|
+
guard CallingxSessionOwnership.callingxOwnsSession else {
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
guard let info = notification.userInfo,
|
|
370
|
+
let typeRaw = info[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
371
|
+
let type = AVAudioSession.InterruptionType(rawValue: typeRaw) else {
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let reason = interruptionReason(info)
|
|
376
|
+
var payload: [String: Any] = ["source": "callingx"]
|
|
377
|
+
if let reason {
|
|
378
|
+
payload["reason"] = reason
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
switch type {
|
|
382
|
+
case .began:
|
|
383
|
+
payload["phase"] = "began"
|
|
384
|
+
sendEvent(CallingxEvents.didAudioInterruption, body: payload)
|
|
385
|
+
CallingxLog.core.debugPublic("Audio interruption began (reason=\(reason ?? "n/a")). Recovery owned by WebRTC AudioEngineDevice.")
|
|
386
|
+
case .ended:
|
|
387
|
+
var shouldResume = false
|
|
388
|
+
if let optsRaw = info[AVAudioSessionInterruptionOptionKey] as? UInt {
|
|
389
|
+
shouldResume = AVAudioSession.InterruptionOptions(rawValue: optsRaw).contains(.shouldResume)
|
|
390
|
+
}
|
|
391
|
+
payload["phase"] = "ended"
|
|
392
|
+
payload["shouldResume"] = shouldResume
|
|
393
|
+
sendEvent(CallingxEvents.didAudioInterruption, body: payload)
|
|
394
|
+
CallingxLog.core.debugPublic("Audio interruption ended (shouldResume=\(shouldResume)). WebRTC restarts the engine.")
|
|
395
|
+
@unknown default:
|
|
396
|
+
break
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private func interruptionReason(_ info: [AnyHashable: Any]) -> String? {
|
|
401
|
+
guard #available(iOS 14.5, *),
|
|
402
|
+
let reasonRaw = info[AVAudioSessionInterruptionReasonKey] as? UInt,
|
|
403
|
+
let reason = AVAudioSession.InterruptionReason(rawValue: reasonRaw) else {
|
|
404
|
+
return nil
|
|
405
|
+
}
|
|
406
|
+
if #available(iOS 17.0, *) {
|
|
407
|
+
switch reason {
|
|
408
|
+
case .builtInMicMuted:
|
|
409
|
+
return "builtInMicMuted"
|
|
410
|
+
case .routeDisconnected:
|
|
411
|
+
return "routeDisconnected"
|
|
412
|
+
default:
|
|
413
|
+
break
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if reason == .default {
|
|
417
|
+
return "default"
|
|
418
|
+
}
|
|
419
|
+
return "raw(\(reason.rawValue))"
|
|
420
|
+
}
|
|
421
|
+
|
|
339
422
|
// MARK: - Setup Methods
|
|
340
423
|
@objc public func setup(options: [String: Any]) {
|
|
341
424
|
callKeepCallController = CXCallController()
|
|
342
|
-
|
|
425
|
+
|
|
343
426
|
Settings.setSettings(options)
|
|
344
|
-
|
|
427
|
+
|
|
345
428
|
// This is mostly needed for very first setup, as we need to override the default
|
|
346
429
|
// provider configuration which is set in the constructor.
|
|
347
430
|
// IMPORTANT: We override CXProvider instance only if there is no registered call, otherwise we may lose corrsponding call state/events from CallKit
|
|
@@ -349,23 +432,48 @@ import stream_react_native_webrtc
|
|
|
349
432
|
let oldProvider = CallingxImpl.sharedProvider
|
|
350
433
|
let newProvider = CXProvider(configuration: Settings.getProviderConfiguration())
|
|
351
434
|
newProvider.setDelegate(self, queue: nil)
|
|
352
|
-
|
|
435
|
+
|
|
353
436
|
CallingxImpl.sharedProvider = newProvider
|
|
354
437
|
callKeepProvider = newProvider
|
|
355
|
-
|
|
438
|
+
|
|
356
439
|
oldProvider?.setDelegate(nil, queue: nil)
|
|
357
440
|
oldProvider?.invalidate()
|
|
358
441
|
}
|
|
359
442
|
|
|
360
443
|
isSetup = true
|
|
361
444
|
}
|
|
445
|
+
|
|
446
|
+
/// Wires the ADM engine-lifecycle subscription. Call after `webRTCModule` is injected
|
|
447
|
+
/// (it's nil during `setup()` on the callingx path). Re-wires when the ADM changes — a JS
|
|
448
|
+
/// reload recreates WebRTCModule while this singleton persists; a no-op for the same ADM.
|
|
449
|
+
@objc public func wireEngineSubscription() {
|
|
450
|
+
guard let adm = getAudioDeviceModule() else { return }
|
|
451
|
+
guard subscribedADM !== adm else { return } // already wired to this ADM
|
|
452
|
+
engineSubscription?.cancel() // ADM changed (e.g. JS reload) — rewire
|
|
453
|
+
subscribedADM = adm
|
|
454
|
+
CallingxLog.core.debugPublic("[wireEngineSubscription]")
|
|
455
|
+
|
|
456
|
+
engineSubscription = adm.publisher.sink { [weak self] event in
|
|
457
|
+
guard CallingxSessionOwnership.callingxOwnsSession else { return }
|
|
458
|
+
switch event {
|
|
459
|
+
case .willEnableAudioEngine:
|
|
460
|
+
self?.pendingActionsQueue.sync { self?.isAudioEngineStarting = true }
|
|
461
|
+
AudioSessionManager.shared.engineWillEnable()
|
|
462
|
+
case .willStartAudioEngine:
|
|
463
|
+
// Engine is now rendering; voice-processing mute reflects real intent again.
|
|
464
|
+
self?.pendingActionsQueue.sync { self?.isAudioEngineStarting = false }
|
|
465
|
+
case .didDisableAudioEngine:
|
|
466
|
+
AudioSessionManager.shared.engineDidDisable()
|
|
467
|
+
default:
|
|
468
|
+
break
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
362
472
|
|
|
363
473
|
@objc public func getInitialEvents() -> [[String: Any]] {
|
|
364
474
|
var events: [[String: Any]] = []
|
|
365
475
|
let action = {
|
|
366
|
-
|
|
367
|
-
NSLog("%@","[Callingx][getInitialEvents] delayedEvents = \(CallingxImpl.delayedEvents)")
|
|
368
|
-
#endif
|
|
476
|
+
CallingxLog.core.debugPublic("[getInitialEvents] delayedEvents = \(CallingxImpl.delayedEvents)")
|
|
369
477
|
|
|
370
478
|
events = CallingxImpl.delayedEvents
|
|
371
479
|
CallingxImpl.delayedEvents = []
|
|
@@ -385,22 +493,16 @@ import stream_react_native_webrtc
|
|
|
385
493
|
|
|
386
494
|
// MARK: - Call Management
|
|
387
495
|
@objc public func answerIncomingCall(_ callId: String) -> Bool {
|
|
388
|
-
|
|
389
|
-
NSLog("%@","[Callingx][answerIncomingCall] callId = \(callId)")
|
|
390
|
-
#endif
|
|
496
|
+
CallingxLog.core.debugPublic("[answerIncomingCall] callId = \(callId)")
|
|
391
497
|
|
|
392
498
|
guard let call = CallingxImpl.uuidStorage?.getCall(forCid: callId) else {
|
|
393
|
-
|
|
394
|
-
NSLog("%@","[Callingx][answerIncomingCall] callId not found")
|
|
395
|
-
#endif
|
|
499
|
+
CallingxLog.core.debugPublic("[answerIncomingCall] callId not found")
|
|
396
500
|
return false
|
|
397
501
|
}
|
|
398
502
|
|
|
399
503
|
// Guard: already answered or ended — prevent duplicate CXAnswerCallAction transactions
|
|
400
504
|
if call.isAnswered || call.hasEnded {
|
|
401
|
-
|
|
402
|
-
NSLog("%@","[Callingx][answerIncomingCall] callId already answered/ended, skipping")
|
|
403
|
-
#endif
|
|
505
|
+
CallingxLog.core.debugPublic("[answerIncomingCall] callId already answered/ended, skipping")
|
|
404
506
|
return true
|
|
405
507
|
}
|
|
406
508
|
|
|
@@ -447,9 +549,7 @@ import stream_react_native_webrtc
|
|
|
447
549
|
let popTime = DispatchTime.now() + .milliseconds(timeout)
|
|
448
550
|
DispatchQueue.main.asyncAfter(deadline: popTime) { [weak self] in
|
|
449
551
|
guard let self = self, !self.isSetup else { return }
|
|
450
|
-
|
|
451
|
-
NSLog("%@","[Callingx] Displayed a call without a reachable app, ending the call: \(callId)")
|
|
452
|
-
#endif
|
|
552
|
+
CallingxLog.core.debugPublic("Displayed a call without a reachable app, ending the call: \(callId)")
|
|
453
553
|
CallingxImpl.endCall(callId, reason: CXCallEndedReason.failed.rawValue)
|
|
454
554
|
}
|
|
455
555
|
}
|
|
@@ -457,22 +557,16 @@ import stream_react_native_webrtc
|
|
|
457
557
|
}
|
|
458
558
|
|
|
459
559
|
@objc public func endCall(_ callId: String) -> Bool {
|
|
460
|
-
|
|
461
|
-
NSLog("%@","[Callingx][endCall] callId = \(callId)")
|
|
462
|
-
#endif
|
|
560
|
+
CallingxLog.core.debugPublic("[endCall] callId = \(callId)")
|
|
463
561
|
|
|
464
562
|
guard let call = CallingxImpl.uuidStorage?.getCall(forCid: callId) else {
|
|
465
|
-
|
|
466
|
-
NSLog("%@","[Callingx][endCall] callId not found")
|
|
467
|
-
#endif
|
|
563
|
+
CallingxLog.core.debugPublic("[endCall] callId not found")
|
|
468
564
|
return false
|
|
469
565
|
}
|
|
470
566
|
|
|
471
567
|
// Guard: already ended — prevent duplicate CXEndCallAction transactions
|
|
472
568
|
if call.hasEnded {
|
|
473
|
-
|
|
474
|
-
NSLog("%@","[Callingx][endCall] callId already ended, skipping")
|
|
475
|
-
#endif
|
|
569
|
+
CallingxLog.core.debugPublic("[endCall] callId already ended, skipping")
|
|
476
570
|
return true
|
|
477
571
|
}
|
|
478
572
|
|
|
@@ -488,9 +582,7 @@ import stream_react_native_webrtc
|
|
|
488
582
|
|
|
489
583
|
@objc public func isCallTracked(_ callId: String) -> Bool {
|
|
490
584
|
guard let uuid = CallingxImpl.uuidStorage?.getUUID(forCid: callId) else {
|
|
491
|
-
|
|
492
|
-
NSLog("%@","[Callingx][isCallTracked] callId not found")
|
|
493
|
-
#endif
|
|
585
|
+
CallingxLog.core.debugPublic("[isCallTracked] callId not found")
|
|
494
586
|
return false
|
|
495
587
|
}
|
|
496
588
|
|
|
@@ -504,14 +596,10 @@ import stream_react_native_webrtc
|
|
|
504
596
|
}
|
|
505
597
|
|
|
506
598
|
@objc public func setCurrentCallActive(_ callId: String) -> Bool {
|
|
507
|
-
|
|
508
|
-
NSLog("%@","[Callingx][setCurrentCallActive] callId = \(callId)")
|
|
509
|
-
#endif
|
|
599
|
+
CallingxLog.core.debugPublic("[setCurrentCallActive] callId = \(callId)")
|
|
510
600
|
|
|
511
601
|
guard let call = CallingxImpl.uuidStorage?.getCall(forCid: callId) else {
|
|
512
|
-
|
|
513
|
-
NSLog("%@","[Callingx][setCurrentCallActive] callId not found")
|
|
514
|
-
#endif
|
|
602
|
+
CallingxLog.core.debugPublic("[setCurrentCallActive] callId not found")
|
|
515
603
|
return false
|
|
516
604
|
}
|
|
517
605
|
|
|
@@ -524,35 +612,30 @@ import stream_react_native_webrtc
|
|
|
524
612
|
}
|
|
525
613
|
|
|
526
614
|
@objc public func setMutedCall(_ callId: String, isMuted: Bool) -> Bool {
|
|
527
|
-
|
|
528
|
-
NSLog("%@","[Callingx][setMutedCall] muted = \(isMuted)")
|
|
529
|
-
#endif
|
|
530
|
-
|
|
615
|
+
CallingxLog.core.debugPublic("[setMutedCall] muted = \(isMuted)")
|
|
531
616
|
guard let call = CallingxImpl.uuidStorage?.getCall(forCid: callId) else {
|
|
532
|
-
|
|
533
|
-
NSLog("%@","[Callingx][setMutedCall] callId not found")
|
|
534
|
-
#endif
|
|
617
|
+
CallingxLog.core.debugPublic("[setMutedCall] callId not found")
|
|
535
618
|
return false
|
|
536
619
|
}
|
|
537
620
|
|
|
538
|
-
call.markSelfMuted()
|
|
539
621
|
let setMutedAction = CXSetMutedCallAction(call: call.uuid, muted: isMuted)
|
|
622
|
+
// Record the action UUID so the perform delegate can recognize this as app-initiated
|
|
623
|
+
// (and skip echoing it back to JS) without racing on a shared per-call flag.
|
|
624
|
+
pendingActionsQueue.sync {
|
|
625
|
+
_ = appInitiatedMuteActionIds.insert(setMutedAction.uuid)
|
|
626
|
+
}
|
|
540
627
|
let transaction = CXTransaction()
|
|
541
628
|
transaction.addAction(setMutedAction)
|
|
542
|
-
|
|
629
|
+
|
|
543
630
|
requestTransaction(transaction)
|
|
544
631
|
return true
|
|
545
632
|
}
|
|
546
633
|
|
|
547
634
|
@objc public func setOnHoldCall(_ callId: String, isOnHold: Bool) -> Bool {
|
|
548
|
-
|
|
549
|
-
NSLog("%@","[Callingx][setOnHold] uuidString = \(callId), shouldHold = \(isOnHold)")
|
|
550
|
-
#endif
|
|
635
|
+
CallingxLog.core.debugPublic("[setOnHold] uuidString = \(callId), shouldHold = \(isOnHold)")
|
|
551
636
|
|
|
552
637
|
guard let uuid = CallingxImpl.uuidStorage?.getUUID(forCid: callId) else {
|
|
553
|
-
|
|
554
|
-
NSLog("%@","[Callingx][setOnHoldCall] callId not found")
|
|
555
|
-
#endif
|
|
638
|
+
CallingxLog.core.debugPublic("[setOnHoldCall] callId not found")
|
|
556
639
|
return false
|
|
557
640
|
}
|
|
558
641
|
|
|
@@ -570,16 +653,12 @@ import stream_react_native_webrtc
|
|
|
570
653
|
callerName: String,
|
|
571
654
|
hasVideo: Bool
|
|
572
655
|
) {
|
|
573
|
-
|
|
574
|
-
NSLog("%@","[Callingx][startCall] uuidString = \(callId), phoneNumber = \(phoneNumber)")
|
|
575
|
-
#endif
|
|
656
|
+
CallingxLog.core.debugPublic("[startCall] uuidString = \(callId), phoneNumber = \(phoneNumber)")
|
|
576
657
|
|
|
577
658
|
guard let storage = CallingxImpl.uuidStorage else { return }
|
|
578
659
|
|
|
579
660
|
if (storage.containsCid(callId)) {
|
|
580
|
-
|
|
581
|
-
NSLog("%@","[Callingx][startCall] Call \(callId) is already registered")
|
|
582
|
-
#endif
|
|
661
|
+
CallingxLog.core.debugPublic("[startCall] Call \(callId) is already registered")
|
|
583
662
|
return
|
|
584
663
|
}
|
|
585
664
|
|
|
@@ -601,14 +680,10 @@ import stream_react_native_webrtc
|
|
|
601
680
|
phoneNumber: String,
|
|
602
681
|
callerName: String
|
|
603
682
|
) -> Bool {
|
|
604
|
-
|
|
605
|
-
NSLog("%@","[Callingx][updateDisplay] uuidString = \(callId) displayName = \(callerName) uri = \(phoneNumber)")
|
|
606
|
-
#endif
|
|
683
|
+
CallingxLog.core.debugPublic("[updateDisplay] uuidString = \(callId) displayName = \(callerName) uri = \(phoneNumber)")
|
|
607
684
|
|
|
608
685
|
guard let uuid = CallingxImpl.uuidStorage?.getUUID(forCid: callId) else {
|
|
609
|
-
|
|
610
|
-
NSLog("%@","[Callingx][updateDisplay] callId not found")
|
|
611
|
-
#endif
|
|
686
|
+
CallingxLog.core.debugPublic("[updateDisplay] callId not found")
|
|
612
687
|
return false
|
|
613
688
|
}
|
|
614
689
|
|
|
@@ -625,20 +700,19 @@ import stream_react_native_webrtc
|
|
|
625
700
|
|
|
626
701
|
// MARK: - CXProviderDelegate
|
|
627
702
|
public func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
#endif
|
|
631
|
-
|
|
703
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:performStartCallAction]")
|
|
704
|
+
|
|
632
705
|
guard let call = CallingxImpl.uuidStorage?.getCallByUUID(action.callUUID) else {
|
|
633
|
-
|
|
634
|
-
NSLog("%@","[Callingx][CXProviderDelegate][provider:performStartCallAction] callId not found")
|
|
635
|
-
#endif
|
|
706
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:performStartCallAction] callId not found")
|
|
636
707
|
action.fail()
|
|
637
708
|
return
|
|
638
709
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
710
|
+
|
|
711
|
+
// Claim audio-session ownership BEFORE createAudioSessionIfNeeded:
|
|
712
|
+
// both can synchronously fire .didDisableAudioEngine / .willEnableAudioEngine
|
|
713
|
+
// through the ADM publisher. The engine sink gates on this flag.
|
|
714
|
+
CallingxSessionOwnership.callingxOwnsSession = true
|
|
715
|
+
AudioSessionManager.shared.createAudioSessionIfNeeded()
|
|
642
716
|
|
|
643
717
|
sendEvent(CallingxEvents.didReceiveStartCallAction, body: [
|
|
644
718
|
"callId": call.cid,
|
|
@@ -654,19 +728,17 @@ import stream_react_native_webrtc
|
|
|
654
728
|
|
|
655
729
|
public func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
|
656
730
|
guard let call = CallingxImpl.uuidStorage?.getCallByUUID(action.callUUID) else {
|
|
657
|
-
|
|
658
|
-
NSLog("%@","[Callingx][CXProviderDelegate][provider:performAnswerCallAction] callId not found")
|
|
659
|
-
#endif
|
|
731
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:performAnswerCallAction] callId not found")
|
|
660
732
|
action.fail()
|
|
661
733
|
return
|
|
662
734
|
}
|
|
663
735
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
AudioSessionManager.createAudioSessionIfNeeded()
|
|
736
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:performAnswerCallAction] isSelfAnswered: \(call.isSelfAnswered)")
|
|
737
|
+
// Claim audio-session ownership BEFORE adm.reset() and createAudioSessionIfNeeded:
|
|
738
|
+
// both can synchronously fire .didDisableAudioEngine / .willEnableAudioEngine
|
|
739
|
+
// through the ADM publisher. The engine sink gates on this flag.
|
|
740
|
+
CallingxSessionOwnership.callingxOwnsSession = true
|
|
741
|
+
AudioSessionManager.shared.createAudioSessionIfNeeded()
|
|
670
742
|
|
|
671
743
|
let source = call.isSelfAnswered ? "app" : "sys"
|
|
672
744
|
sendEvent(CallingxEvents.performAnswerCallAction, body: [
|
|
@@ -691,9 +763,7 @@ import stream_react_native_webrtc
|
|
|
691
763
|
let timeout = DispatchTime.now() + DispatchTimeInterval.seconds(pendingActionTimeoutSeconds)
|
|
692
764
|
pendingActionsQueue.asyncAfter(deadline: timeout) { [weak self] in
|
|
693
765
|
if let pending = self?.pendingAnswerActions.removeValue(forKey: cid) {
|
|
694
|
-
|
|
695
|
-
NSLog("%@","[Callingx][CXProviderDelegate][provider:performAnswerCallAction] answer timeout for callId: \(cid)")
|
|
696
|
-
#endif
|
|
766
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:performAnswerCallAction] answer timeout for callId: \(cid)")
|
|
697
767
|
pending.action.fail()
|
|
698
768
|
}
|
|
699
769
|
}
|
|
@@ -702,18 +772,14 @@ import stream_react_native_webrtc
|
|
|
702
772
|
|
|
703
773
|
public func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
|
704
774
|
guard let call = CallingxImpl.uuidStorage?.getCallByUUID(action.callUUID) else {
|
|
705
|
-
|
|
706
|
-
NSLog("%@","[Callingx][CXProviderDelegate][provider:performEndCallAction] callId not found")
|
|
707
|
-
#endif
|
|
775
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:performEndCallAction] callId not found")
|
|
708
776
|
// End actions represent explicit user intent to close call UI.
|
|
709
777
|
// Fulfill stale/duplicate end actions to avoid "Call Failed" UX.
|
|
710
778
|
action.fulfill()
|
|
711
779
|
return
|
|
712
780
|
}
|
|
713
781
|
|
|
714
|
-
|
|
715
|
-
NSLog("%@","[Callingx][CXProviderDelegate][provider:performEndCallAction] isSelfEnded: \(call.isSelfEnded)")
|
|
716
|
-
#endif
|
|
782
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:performEndCallAction] isSelfEnded: \(call.isSelfEnded)")
|
|
717
783
|
|
|
718
784
|
let source = call.isSelfEnded ? "app" : "sys"
|
|
719
785
|
sendEvent(CallingxEvents.performEndCallAction, body: [
|
|
@@ -724,6 +790,8 @@ import stream_react_native_webrtc
|
|
|
724
790
|
call.resetSelfEnded()
|
|
725
791
|
call.markEnded()
|
|
726
792
|
CallingxImpl.uuidStorage?.removeCid(call.cid)
|
|
793
|
+
// Forget this call's mute intent so its stale value can't be read as an echo next call.
|
|
794
|
+
pendingActionsQueue.sync { lastAppRequestedMute = nil }
|
|
727
795
|
|
|
728
796
|
if source == "app" {
|
|
729
797
|
// App initiated this end — no need to wait for JS, fulfill immediately
|
|
@@ -738,9 +806,7 @@ import stream_react_native_webrtc
|
|
|
738
806
|
let timeout = DispatchTime.now() + DispatchTimeInterval.seconds(pendingActionTimeoutSeconds)
|
|
739
807
|
pendingActionsQueue.asyncAfter(deadline: timeout) { [weak self] in
|
|
740
808
|
if let pending = self?.pendingEndActions.removeValue(forKey: cid) {
|
|
741
|
-
|
|
742
|
-
NSLog("%@","[Callingx][CXProviderDelegate][provider:performEndCallAction] end timeout for callId: \(cid)")
|
|
743
|
-
#endif
|
|
809
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:performEndCallAction] end timeout for callId: \(cid)")
|
|
744
810
|
pending.action.fulfill()
|
|
745
811
|
}
|
|
746
812
|
}
|
|
@@ -748,14 +814,10 @@ import stream_react_native_webrtc
|
|
|
748
814
|
}
|
|
749
815
|
|
|
750
816
|
public func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
|
|
751
|
-
|
|
752
|
-
NSLog("%@","[Callingx][CXProviderDelegate][provider:performSetHeldCallAction]")
|
|
753
|
-
#endif
|
|
817
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:performSetHeldCallAction]")
|
|
754
818
|
|
|
755
819
|
guard let callId = CallingxImpl.uuidStorage?.getCid(forUUID: action.callUUID) else {
|
|
756
|
-
|
|
757
|
-
NSLog("%@","[Callingx][CXProviderDelegate][provider:performSetHeldCallAction] callId not found")
|
|
758
|
-
#endif
|
|
820
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:performSetHeldCallAction] callId not found")
|
|
759
821
|
action.fail()
|
|
760
822
|
return
|
|
761
823
|
}
|
|
@@ -770,25 +832,25 @@ import stream_react_native_webrtc
|
|
|
770
832
|
|
|
771
833
|
public func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
|
|
772
834
|
guard let call = CallingxImpl.uuidStorage?.getCallByUUID(action.callUUID) else {
|
|
773
|
-
|
|
774
|
-
NSLog("%@","[Callingx][CXProviderDelegate][provider:performSetMutedCallAction] callId not found")
|
|
775
|
-
#endif
|
|
835
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:performSetMutedCallAction] callId not found")
|
|
776
836
|
action.fail()
|
|
777
837
|
return
|
|
778
838
|
}
|
|
779
839
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
//
|
|
791
|
-
|
|
840
|
+
// Resolve all three suppression flags in one queue hop (serialized state).
|
|
841
|
+
let (isAppInitiated, suppressDuringStartup, isMuteEcho) = pendingActionsQueue.sync { () -> (Bool, Bool, Bool) in
|
|
842
|
+
let appInitiated = appInitiatedMuteActionIds.remove(action.uuid) != nil
|
|
843
|
+
// Remember the value so its iOS 17+ system echo can be skipped below.
|
|
844
|
+
if appInitiated { lastAppRequestedMute = action.isMuted }
|
|
845
|
+
let echo = !appInitiated && lastAppRequestedMute == action.isMuted
|
|
846
|
+
return (appInitiated, isAudioEngineStarting, echo)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:performSetMutedCallAction] \(action.isMuted) isAppInitiated: \(isAppInitiated) suppressDuringStartup: \(suppressDuringStartup) isMuteEcho: \(isMuteEcho)")
|
|
850
|
+
// Forward to JS only genuine system mutes (user tapped native CallKit UI). Skip app-initiated
|
|
851
|
+
// actions (feedback loop), their iOS 17+ system echoes, and engine-startup artifacts —
|
|
852
|
+
// see each flag's field docs.
|
|
853
|
+
if !isAppInitiated && !suppressDuringStartup && !isMuteEcho {
|
|
792
854
|
sendEvent(CallingxEvents.didPerformSetMutedCallAction, body: [
|
|
793
855
|
"muted": action.isMuted,
|
|
794
856
|
"callId": call.cid
|
|
@@ -799,14 +861,10 @@ import stream_react_native_webrtc
|
|
|
799
861
|
}
|
|
800
862
|
|
|
801
863
|
public func provider(_ provider: CXProvider, perform action: CXPlayDTMFCallAction) {
|
|
802
|
-
|
|
803
|
-
NSLog("%@","[Callingx][CXProviderDelegate][provider:performPlayDTMFCallAction]")
|
|
804
|
-
#endif
|
|
864
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:performPlayDTMFCallAction]")
|
|
805
865
|
|
|
806
866
|
guard let callId = CallingxImpl.uuidStorage?.getCid(forUUID: action.callUUID) else {
|
|
807
|
-
|
|
808
|
-
NSLog("%@","[Callingx][CXProviderDelegate][provider:performPlayDTMFCallAction] callId not found")
|
|
809
|
-
#endif
|
|
867
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:performPlayDTMFCallAction] callId not found")
|
|
810
868
|
action.fail()
|
|
811
869
|
return
|
|
812
870
|
}
|
|
@@ -820,17 +878,17 @@ import stream_react_native_webrtc
|
|
|
820
878
|
}
|
|
821
879
|
|
|
822
880
|
public func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
881
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:didActivateAudioSession] category=\(audioSession.category) mode=\(audioSession.mode)")
|
|
882
|
+
// Re-claim ownership BEFORE notifying WebRTC. Handles the PSTN/Siri
|
|
883
|
+
// interruption-resume case: didDeactivate cleared the flag if the call
|
|
884
|
+
// had ended, but for an interruption the call is still tracked and
|
|
885
|
+
// ownership was preserved — re-asserting here is a no-op then, and
|
|
886
|
+
// closes any edge case where it had been cleared.
|
|
887
|
+
CallingxSessionOwnership.callingxOwnsSession = true
|
|
826
888
|
|
|
827
889
|
// When CallKit activates the AVAudioSession, inform WebRTC as well.
|
|
828
890
|
RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession)
|
|
829
891
|
|
|
830
|
-
// No-op on the first didActivate per call (CXAction.perform already configured);
|
|
831
|
-
// only fires for interruption recovery / unhold cycles. See Apple Forums 749202.
|
|
832
|
-
AudioSessionManager.reapplyForDidActivateIfNeeded()
|
|
833
|
-
|
|
834
892
|
// Enable wake lock to keep the device awake during the call
|
|
835
893
|
DispatchQueue.main.async {
|
|
836
894
|
UIApplication.shared.isIdleTimerDisabled = true
|
|
@@ -840,14 +898,22 @@ import stream_react_native_webrtc
|
|
|
840
898
|
}
|
|
841
899
|
|
|
842
900
|
public func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
|
|
843
|
-
|
|
844
|
-
NSLog("%@","[Callingx][CXProviderDelegate][provider:didDeactivateAudioSession] category=\(audioSession.category) mode=\(audioSession.mode)")
|
|
845
|
-
#endif
|
|
901
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:didDeactivateAudioSession] category=\(audioSession.category) mode=\(audioSession.mode)")
|
|
846
902
|
|
|
847
903
|
// When CallKit deactivates the AVAudioSession, inform WebRTC as well.
|
|
848
904
|
RTCAudioSession.sharedInstance().audioSessionDidDeactivate(audioSession)
|
|
849
905
|
getAudioDeviceModule()?.reset()
|
|
850
|
-
|
|
906
|
+
|
|
907
|
+
// Invariant: callingx ships with maximumCallsPerCallGroup = maximumCallGroups = 1
|
|
908
|
+
// (see packages/react-native-callingx/src/utils/constants.ts defaultiOSOptions).
|
|
909
|
+
// So `UUIDStorage.count() == 0` reliably distinguishes:
|
|
910
|
+
// - true end-of-call (call removed in CXEndCallAction.perform before didDeactivate)
|
|
911
|
+
// - PSTN/Siri interruption (call still tracked, will resume via didActivate)
|
|
912
|
+
// Do NOT "fix" this to handle multi-call semantics — the product does not support
|
|
913
|
+
// concurrent CallKit calls. See plan: critically-review-the-implementation-zesty-spindle.
|
|
914
|
+
if let storage = CallingxImpl.uuidStorage, storage.count() == 0 {
|
|
915
|
+
CallingxSessionOwnership.callingxOwnsSession = false
|
|
916
|
+
}
|
|
851
917
|
|
|
852
918
|
// Disable wake lock when the call ends
|
|
853
919
|
DispatchQueue.main.async {
|
|
@@ -860,9 +926,7 @@ import stream_react_native_webrtc
|
|
|
860
926
|
public func provider(_ provider: CXProvider, timedOutPerforming action: CXAction) {
|
|
861
927
|
// note: in practice we should never be getting this callback as we already have a pending timeout set.
|
|
862
928
|
// in our tests callkit timesout and exectutes this method in approximately 60 seconds.
|
|
863
|
-
|
|
864
|
-
NSLog("%@","[Callingx][CXProviderDelegate][provider:timedOutPerformingAction]")
|
|
865
|
-
#endif
|
|
929
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:timedOutPerformingAction]")
|
|
866
930
|
|
|
867
931
|
guard let callAction = action as? CXCallAction else {
|
|
868
932
|
return
|
|
@@ -873,33 +937,36 @@ import stream_react_native_webrtc
|
|
|
873
937
|
if let answerEntry = pendingAnswerActions.first(where: { $0.value.action.callUUID == callAction.callUUID }) {
|
|
874
938
|
pendingAnswerActions.removeValue(forKey: answerEntry.key)
|
|
875
939
|
let elapsedMs = elapsedMilliseconds(since: answerEntry.value.enqueuedAt)
|
|
876
|
-
|
|
877
|
-
NSLog("%@","[Callingx][CXProviderDelegate][provider:timedOutPerformingAction] removed pending answer action for callId: \(answerEntry.key), elapsedMs=\(elapsedMs)")
|
|
878
|
-
#endif
|
|
940
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:timedOutPerformingAction] removed pending answer action for callId: \(answerEntry.key), elapsedMs=\(elapsedMs)")
|
|
879
941
|
}
|
|
880
942
|
|
|
881
943
|
if let endEntry = pendingEndActions.first(where: { $0.value.action.callUUID == callAction.callUUID }) {
|
|
882
944
|
pendingEndActions.removeValue(forKey: endEntry.key)
|
|
883
945
|
let elapsedMs = elapsedMilliseconds(since: endEntry.value.enqueuedAt)
|
|
884
|
-
|
|
885
|
-
NSLog("%@","[Callingx][CXProviderDelegate][provider:timedOutPerformingAction] removed pending end action for callId: \(endEntry.key), elapsedMs=\(elapsedMs)")
|
|
886
|
-
#endif
|
|
946
|
+
CallingxLog.core.debugPublic("[CXProviderDelegate][provider:timedOutPerformingAction] removed pending end action for callId: \(endEntry.key), elapsedMs=\(elapsedMs)")
|
|
887
947
|
}
|
|
888
948
|
}
|
|
889
949
|
}
|
|
890
950
|
|
|
891
951
|
public func providerDidReset(_ provider: CXProvider) {
|
|
892
|
-
|
|
893
|
-
NSLog("%@","[Callingx][providerDidReset]")
|
|
894
|
-
#endif
|
|
952
|
+
CallingxLog.core.debugPublic("[providerDidReset]")
|
|
895
953
|
|
|
896
954
|
// Clear any pending actions to prevent memory leaks.
|
|
897
955
|
// After a provider reset, all pending CXActions are invalid.
|
|
898
956
|
pendingActionsQueue.sync {
|
|
899
957
|
pendingAnswerActions.removeAll()
|
|
900
958
|
pendingEndActions.removeAll()
|
|
959
|
+
lastAppRequestedMute = nil
|
|
901
960
|
}
|
|
902
961
|
|
|
962
|
+
// A provider reset invalidates all CallKit calls. didDeactivate is not
|
|
963
|
+
// guaranteed to fire in its usual shape afterwards, so release ownership
|
|
964
|
+
// here and wipe UUIDStorage to keep the `count() == 0` discriminator in
|
|
965
|
+
// didDeactivate honest (stale entries would otherwise refuse to release
|
|
966
|
+
// ownership on the next end-of-call).
|
|
967
|
+
CallingxImpl.uuidStorage?.removeAllObjects()
|
|
968
|
+
CallingxSessionOwnership.callingxOwnsSession = false
|
|
969
|
+
|
|
903
970
|
sendEvent(CallingxEvents.providerReset, body: nil)
|
|
904
971
|
}
|
|
905
972
|
|
|
@@ -908,15 +975,11 @@ import stream_react_native_webrtc
|
|
|
908
975
|
@objc public func fulfillAnswerCallAction(_ callId: String, didFail: Bool) {
|
|
909
976
|
pendingActionsQueue.sync { [weak self] in
|
|
910
977
|
guard let pending = self?.pendingAnswerActions.removeValue(forKey: callId) else {
|
|
911
|
-
|
|
912
|
-
NSLog("%@","[Callingx][fulfillAnswerCallAction] action not found for callId: \(callId)")
|
|
913
|
-
#endif
|
|
978
|
+
CallingxLog.core.debugPublic("[fulfillAnswerCallAction] action not found for callId: \(callId)")
|
|
914
979
|
return
|
|
915
980
|
}
|
|
916
981
|
let elapsedMs = elapsedMilliseconds(since: pending.enqueuedAt)
|
|
917
|
-
|
|
918
|
-
NSLog("%@","[Callingx][fulfillAnswerCallAction] callId: \(callId), didFail: \(didFail), elapsedMs=\(elapsedMs)")
|
|
919
|
-
#endif
|
|
982
|
+
CallingxLog.core.debugPublic("[fulfillAnswerCallAction] callId: \(callId), didFail: \(didFail), elapsedMs=\(elapsedMs)")
|
|
920
983
|
if didFail { pending.action.fail() } else { pending.action.fulfill() }
|
|
921
984
|
}
|
|
922
985
|
}
|
|
@@ -924,15 +987,11 @@ import stream_react_native_webrtc
|
|
|
924
987
|
@objc public func fulfillEndCallAction(_ callId: String, didFail: Bool) {
|
|
925
988
|
pendingActionsQueue.sync { [weak self] in
|
|
926
989
|
guard let pending = self?.pendingEndActions.removeValue(forKey: callId) else {
|
|
927
|
-
|
|
928
|
-
NSLog("%@","[Callingx][fulfillEndCallAction] action not found for callId: \(callId)")
|
|
929
|
-
#endif
|
|
990
|
+
CallingxLog.core.debugPublic("[fulfillEndCallAction] action not found for callId: \(callId)")
|
|
930
991
|
return
|
|
931
992
|
}
|
|
932
993
|
let elapsedMs = elapsedMilliseconds(since: pending.enqueuedAt)
|
|
933
|
-
|
|
934
|
-
NSLog("%@","[Callingx][fulfillEndCallAction] callId: \(callId), didFail: \(didFail), elapsedMs=\(elapsedMs)")
|
|
935
|
-
#endif
|
|
994
|
+
CallingxLog.core.debugPublic("[fulfillEndCallAction] callId: \(callId), didFail: \(didFail), elapsedMs=\(elapsedMs)")
|
|
936
995
|
if didFail { pending.action.fail() } else { pending.action.fulfill() }
|
|
937
996
|
}
|
|
938
997
|
}
|
|
@@ -940,7 +999,7 @@ import stream_react_native_webrtc
|
|
|
940
999
|
// MARK: - Audio Configuration
|
|
941
1000
|
|
|
942
1001
|
@objc public func setDefaultAudioDeviceEndpointType(_ endpointType: String) {
|
|
943
|
-
AudioSessionManager.setDefaultAudioDeviceEndpointType(endpointType)
|
|
1002
|
+
AudioSessionManager.shared.setDefaultAudioDeviceEndpointType(endpointType)
|
|
944
1003
|
}
|
|
945
1004
|
|
|
946
1005
|
// MARK: - Helper Methods
|
|
@@ -953,12 +1012,9 @@ import stream_react_native_webrtc
|
|
|
953
1012
|
|
|
954
1013
|
private func getAudioDeviceModule() -> AudioDeviceModule? {
|
|
955
1014
|
guard let adm = webRTCModule?.audioDeviceModule else {
|
|
956
|
-
|
|
957
|
-
NSLog("%@","[Callingx] WebRTCModule is not available. Ensure it was injected from the TurboModule host.")
|
|
958
|
-
#endif
|
|
1015
|
+
CallingxLog.core.errorPublic("WebRTCModule is not available. Ensure it was injected from the TurboModule host.")
|
|
959
1016
|
return nil
|
|
960
1017
|
}
|
|
961
1018
|
return adm
|
|
962
1019
|
}
|
|
963
1020
|
}
|
|
964
|
-
|