@telnyx/react-voice-commons-sdk 1.1.0 → 1.1.1
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/ios/CallKitBridge.m +7 -0
- package/ios/CallKitBridge.swift +113 -47
- package/package.json +2 -2
package/ios/CallKitBridge.m
CHANGED
|
@@ -60,4 +60,11 @@ RCT_EXTERN_METHOD(updateCall:(NSString *)callUUID
|
|
|
60
60
|
RCT_EXTERN_METHOD(getActiveCalls:(RCTPromiseResolveBlock)resolve
|
|
61
61
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
62
62
|
|
|
63
|
+
#if DEBUG
|
|
64
|
+
RCT_EXTERN_METHOD(simulateAudioSetupRace:(NSString *)callUUID
|
|
65
|
+
delayMilliseconds:(nonnull NSNumber *)delayMilliseconds
|
|
66
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
67
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
68
|
+
#endif
|
|
69
|
+
|
|
63
70
|
@end
|
package/ios/CallKitBridge.swift
CHANGED
|
@@ -440,29 +440,31 @@ import React
|
|
|
440
440
|
NSLog("TelnyxVoice: reportCallConnected - fulfilling deferred CXAnswerCallAction")
|
|
441
441
|
pendingAction.fulfill()
|
|
442
442
|
} else {
|
|
443
|
-
|
|
444
|
-
let rtcAudioSession = RTCAudioSession.sharedInstance()
|
|
445
|
-
rtcAudioSession.lockForConfiguration()
|
|
446
|
-
let webRTCConfig = RTCAudioSessionConfiguration.webRTC()
|
|
447
|
-
webRTCConfig.categoryOptions = [.duckOthers, .allowBluetooth]
|
|
448
|
-
do {
|
|
449
|
-
try rtcAudioSession.setConfiguration(webRTCConfig)
|
|
450
|
-
} catch {
|
|
451
|
-
NSLog("TelnyxVoice: reportCallConnected - setConfiguration error: \(error)")
|
|
452
|
-
}
|
|
453
|
-
do {
|
|
454
|
-
try rtcAudioSession.setActive(true)
|
|
455
|
-
} catch {
|
|
456
|
-
NSLog("TelnyxVoice: reportCallConnected - setActive error: \(error)")
|
|
457
|
-
}
|
|
458
|
-
rtcAudioSession.isAudioEnabled = true
|
|
459
|
-
rtcAudioSession.unlockForConfiguration()
|
|
460
|
-
rtcAudioSession.audioSessionDidActivate(AVAudioSession.sharedInstance())
|
|
443
|
+
manager.verifyAudioAfterConnection(for: uuid)
|
|
461
444
|
}
|
|
462
445
|
|
|
463
446
|
resolve(["success": true])
|
|
464
447
|
}
|
|
465
448
|
|
|
449
|
+
#if DEBUG
|
|
450
|
+
@objc func simulateAudioSetupRace(
|
|
451
|
+
_ callUUID: String,
|
|
452
|
+
delayMilliseconds: NSNumber,
|
|
453
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
454
|
+
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
455
|
+
) {
|
|
456
|
+
guard let uuid = UUID(uuidString: callUUID) else {
|
|
457
|
+
reject("INVALID_UUID", "Invalid UUID format", nil)
|
|
458
|
+
return
|
|
459
|
+
}
|
|
460
|
+
getCallKitManager().simulateAudioSetupRace(
|
|
461
|
+
for: uuid,
|
|
462
|
+
delay: delayMilliseconds.doubleValue / 1_000
|
|
463
|
+
)
|
|
464
|
+
resolve(["scheduled": true, "delayMilliseconds": delayMilliseconds])
|
|
465
|
+
}
|
|
466
|
+
#endif
|
|
467
|
+
|
|
466
468
|
@objc func reportCallEnded(
|
|
467
469
|
_ callUUID: String, reason: NSNumber,
|
|
468
470
|
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
@@ -744,6 +746,20 @@ import React
|
|
|
744
746
|
lock: stateLock)
|
|
745
747
|
public lazy var pendingHeldActions = LockedDictionary<UUID, CXSetHeldCallAction>(
|
|
746
748
|
lock: stateLock)
|
|
749
|
+
private var audioLifecycleGeneration = 0
|
|
750
|
+
|
|
751
|
+
private func advanceAudioLifecycleGeneration() -> Int {
|
|
752
|
+
stateLock.lock()
|
|
753
|
+
defer { stateLock.unlock() }
|
|
754
|
+
audioLifecycleGeneration += 1
|
|
755
|
+
return audioLifecycleGeneration
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
private func currentAudioLifecycleGeneration() -> Int {
|
|
759
|
+
stateLock.lock()
|
|
760
|
+
defer { stateLock.unlock() }
|
|
761
|
+
return audioLifecycleGeneration
|
|
762
|
+
}
|
|
747
763
|
|
|
748
764
|
fileprivate func markCallKitRegistered(_ callUUID: UUID) {
|
|
749
765
|
guard var callData = activeCalls[callUUID] else {
|
|
@@ -825,10 +841,6 @@ import React
|
|
|
825
841
|
}
|
|
826
842
|
|
|
827
843
|
private func setupCallKit() {
|
|
828
|
-
// ALWAYS configure WebRTC audio, even if provider already exists
|
|
829
|
-
RTCAudioSession.sharedInstance().useManualAudio = true
|
|
830
|
-
RTCAudioSession.sharedInstance().isAudioEnabled = false
|
|
831
|
-
|
|
832
844
|
// Guard against duplicate provider creation (async setupAutomatically can overwrite
|
|
833
845
|
// the provider created by setupSynchronously for VoIP push)
|
|
834
846
|
guard callKitProvider == nil else {
|
|
@@ -836,9 +848,84 @@ import React
|
|
|
836
848
|
return
|
|
837
849
|
}
|
|
838
850
|
|
|
851
|
+
// Set the initial state only when installing the provider. Re-running setup after
|
|
852
|
+
// CallKit activation must never disable a live WebRTC audio device.
|
|
853
|
+
RTCAudioSession.sharedInstance().useManualAudio = true
|
|
854
|
+
RTCAudioSession.sharedInstance().isAudioEnabled = false
|
|
855
|
+
|
|
839
856
|
_ = installCallKitProvider()
|
|
840
857
|
}
|
|
841
858
|
|
|
859
|
+
fileprivate func activateWebRTCAudio(
|
|
860
|
+
_ audioSession: AVAudioSession = AVAudioSession.sharedInstance(),
|
|
861
|
+
reason: String
|
|
862
|
+
) {
|
|
863
|
+
let rtcAudioSession = RTCAudioSession.sharedInstance()
|
|
864
|
+
rtcAudioSession.lockForConfiguration()
|
|
865
|
+
let wasAudioEnabled = rtcAudioSession.isAudioEnabled
|
|
866
|
+
NSLog("TelnyxVoice: activating WebRTC audio (\(reason)); enabled=\(wasAudioEnabled)")
|
|
867
|
+
let webRTCConfig = RTCAudioSessionConfiguration.webRTC()
|
|
868
|
+
webRTCConfig.categoryOptions = [.duckOthers, .allowBluetooth]
|
|
869
|
+
do {
|
|
870
|
+
try rtcAudioSession.setConfiguration(webRTCConfig)
|
|
871
|
+
} catch {
|
|
872
|
+
NSLog("TelnyxVoice: activateWebRTCAudio configuration (\(reason)) error: \(error)")
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
var activationSucceeded = false
|
|
876
|
+
do {
|
|
877
|
+
try rtcAudioSession.setActive(true)
|
|
878
|
+
activationSucceeded = true
|
|
879
|
+
} catch {
|
|
880
|
+
NSLog("TelnyxVoice: activateWebRTCAudio activation (\(reason)) error: \(error)")
|
|
881
|
+
}
|
|
882
|
+
if activationSucceeded {
|
|
883
|
+
rtcAudioSession.isAudioEnabled = true
|
|
884
|
+
} else if !wasAudioEnabled {
|
|
885
|
+
rtcAudioSession.isAudioEnabled = false
|
|
886
|
+
}
|
|
887
|
+
rtcAudioSession.unlockForConfiguration()
|
|
888
|
+
if activationSucceeded && !wasAudioEnabled {
|
|
889
|
+
rtcAudioSession.audioSessionDidActivate(audioSession)
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
fileprivate func verifyAudioAfterConnection(for callUUID: UUID) {
|
|
894
|
+
// didActivate is asynchronous after CXAnswerCallAction.fulfill(). Verify shortly
|
|
895
|
+
// afterwards so either callback ordering converges on the same idempotent state.
|
|
896
|
+
let verificationGeneration = currentAudioLifecycleGeneration()
|
|
897
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { [weak self] in
|
|
898
|
+
guard let self,
|
|
899
|
+
self.currentAudioLifecycleGeneration() == verificationGeneration,
|
|
900
|
+
self.activeCalls[callUUID] != nil
|
|
901
|
+
else { return }
|
|
902
|
+
let rtcAudioSession = RTCAudioSession.sharedInstance()
|
|
903
|
+
rtcAudioSession.lockForConfiguration()
|
|
904
|
+
let isAudioEnabled = rtcAudioSession.isAudioEnabled
|
|
905
|
+
rtcAudioSession.unlockForConfiguration()
|
|
906
|
+
if isAudioEnabled {
|
|
907
|
+
NSLog("TelnyxVoice: audio verification passed for \(callUUID)")
|
|
908
|
+
} else if AVAudioSession.sharedInstance().category != .playAndRecord {
|
|
909
|
+
NSLog("TelnyxVoice: audio verification skipped for inactive CallKit session \(callUUID)")
|
|
910
|
+
} else {
|
|
911
|
+
NSLog("TelnyxVoice: audio verification recovering disabled audio for \(callUUID)")
|
|
912
|
+
self.activateWebRTCAudio(reason: "connected-call verification")
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
#if DEBUG
|
|
918
|
+
fileprivate func simulateAudioSetupRace(for callUUID: UUID, delay: TimeInterval) {
|
|
919
|
+
NSLog("TelnyxVoice: [VSUP-226] scheduling late audio reset in \(delay)s")
|
|
920
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
921
|
+
let rtcAudioSession = RTCAudioSession.sharedInstance()
|
|
922
|
+
rtcAudioSession.isAudioEnabled = false
|
|
923
|
+
NSLog("TelnyxVoice: [VSUP-226] injected late setup reset; enabled=false")
|
|
924
|
+
self?.verifyAudioAfterConnection(for: callUUID)
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
#endif
|
|
928
|
+
|
|
842
929
|
private func installCallKitProvider() -> CXProvider {
|
|
843
930
|
// Use the localizedName from the app's bundle display name or fallback
|
|
844
931
|
let appName =
|
|
@@ -1404,6 +1491,7 @@ import React
|
|
|
1404
1491
|
"CallKitDidPerformEndCallAction", callUUID: action.callUUID,
|
|
1405
1492
|
callData: callData)
|
|
1406
1493
|
|
|
1494
|
+
_ = advanceAudioLifecycleGeneration()
|
|
1407
1495
|
activeCalls.removeValue(forKey: action.callUUID)
|
|
1408
1496
|
pendingAnswerActions.removeValue(forKey: action.callUUID)?.fail()
|
|
1409
1497
|
pendingHeldActions.removeValue(forKey: action.callUUID)?.fail()
|
|
@@ -1442,31 +1530,8 @@ import React
|
|
|
1442
1530
|
|
|
1443
1531
|
public func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
|
|
1444
1532
|
NSLog("TelnyxVoice: Audio session activated by CallKit")
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
// Step 1: Configure (matches native iOS SDK setupCorrectAudioConfiguration)
|
|
1449
|
-
rtcAudioSession.lockForConfiguration()
|
|
1450
|
-
let webRTCConfig = RTCAudioSessionConfiguration.webRTC()
|
|
1451
|
-
webRTCConfig.categoryOptions = [.duckOthers, .allowBluetooth]
|
|
1452
|
-
do {
|
|
1453
|
-
try rtcAudioSession.setConfiguration(webRTCConfig)
|
|
1454
|
-
} catch {
|
|
1455
|
-
NSLog("TelnyxVoice: didActivateAudioSession - setConfiguration error: \(error)")
|
|
1456
|
-
}
|
|
1457
|
-
rtcAudioSession.unlockForConfiguration()
|
|
1458
|
-
|
|
1459
|
-
// Step 2: Activate (matches native iOS SDK setAudioSessionActive)
|
|
1460
|
-
rtcAudioSession.lockForConfiguration()
|
|
1461
|
-
do {
|
|
1462
|
-
try rtcAudioSession.setActive(true)
|
|
1463
|
-
} catch {
|
|
1464
|
-
NSLog("TelnyxVoice: didActivateAudioSession - setActive error: \(error)")
|
|
1465
|
-
}
|
|
1466
|
-
rtcAudioSession.isAudioEnabled = true
|
|
1467
|
-
rtcAudioSession.unlockForConfiguration()
|
|
1468
|
-
|
|
1469
|
-
rtcAudioSession.audioSessionDidActivate(audioSession)
|
|
1533
|
+
_ = advanceAudioLifecycleGeneration()
|
|
1534
|
+
activateWebRTCAudio(audioSession, reason: "CallKit didActivate")
|
|
1470
1535
|
|
|
1471
1536
|
// Emit audio session activated event to React Native
|
|
1472
1537
|
CallKitBridge.shared?.emitAudioSessionEvent(
|
|
@@ -1480,6 +1545,7 @@ import React
|
|
|
1480
1545
|
|
|
1481
1546
|
public func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
|
|
1482
1547
|
NSLog("TelnyxVoice: Audio session deactivated by CallKit")
|
|
1548
|
+
_ = advanceAudioLifecycleGeneration()
|
|
1483
1549
|
|
|
1484
1550
|
let rtcAudioSession = RTCAudioSession.sharedInstance()
|
|
1485
1551
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telnyx/react-voice-commons-sdk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "A high-level, state-agnostic, drop-in module for the Telnyx React Native SDK that simplifies WebRTC voice calling integration",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/index.js",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
88
|
"@react-native-community/eslint-config": "^3.2.0",
|
|
89
|
-
"@telnyx/react-native-voice-sdk": ">=1.
|
|
89
|
+
"@telnyx/react-native-voice-sdk": ">=1.1.0",
|
|
90
90
|
"eventemitter3": "^5.0.1",
|
|
91
91
|
"expo": "~53.0.22",
|
|
92
92
|
"react-native-url-polyfill": "^3.0.0",
|