@qusaieilouti99/call-manager 0.1.129 → 0.1.130
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/AudioManager.swift +42 -12
- package/ios/CallEngine.swift +81 -73
- package/ios/CallKitManager.swift +0 -40
- package/package.json +1 -1
package/ios/AudioManager.swift
CHANGED
|
@@ -48,18 +48,48 @@ class AudioManager {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
func configureAudioSession(forCallType isVideo: Bool,
|
|
51
|
-
isIncoming: Bool
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
51
|
+
isIncoming: Bool,
|
|
52
|
+
forceReconfigure: Bool = false) {
|
|
53
|
+
logger.info("configureAudioSession: video=\(isVideo), incoming=\(isIncoming), force=\(forceReconfigure)")
|
|
54
|
+
|
|
55
|
+
// For hold/unhold scenarios, we need to fully reconfigure
|
|
56
|
+
if forceReconfigure {
|
|
57
|
+
do {
|
|
58
|
+
try session.setActive(false, options: .notifyOthersOnDeactivation)
|
|
59
|
+
logger.info("deactivated audio session for reconfigure")
|
|
60
|
+
} catch {
|
|
61
|
+
logger.error("deactivate for reconfigure failed: \(error.localizedDescription)")
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
var opts: AVAudioSession.CategoryOptions = [.allowBluetooth, .allowBluetoothA2DP, .mixWithOthers]
|
|
66
|
+
|
|
67
|
+
// Always use defaultToSpeaker for video calls
|
|
68
|
+
// For audio calls, maintain consistent routing
|
|
69
|
+
if isVideo {
|
|
70
|
+
opts.insert(.defaultToSpeaker)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
do {
|
|
74
|
+
try session.setCategory(.playAndRecord,
|
|
75
|
+
mode: .voiceChat,
|
|
76
|
+
options: opts)
|
|
77
|
+
logger.info("audioSession category set")
|
|
78
|
+
|
|
79
|
+
// Immediately reactivate after configuration
|
|
80
|
+
if forceReconfigure || !session.isOtherAudioPlaying {
|
|
81
|
+
try session.setActive(true)
|
|
82
|
+
logger.info("audioSession reactivated after configuration")
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
logger.error("setCategory/activate failed: \(error.localizedDescription)")
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Add a method specifically for hold/unhold scenarios
|
|
90
|
+
func reconfigureAudioSessionForUnhold(forCallType isVideo: Bool) {
|
|
91
|
+
logger.info("reconfigureAudioSessionForUnhold: video=\(isVideo)")
|
|
92
|
+
configureAudioSession(forCallType: isVideo, isIncoming: false, forceReconfigure: true)
|
|
63
93
|
}
|
|
64
94
|
|
|
65
95
|
func activateAudioSession() {
|
package/ios/CallEngine.swift
CHANGED
|
@@ -169,63 +169,67 @@ class CallEngine {
|
|
|
169
169
|
|
|
170
170
|
// MARK: Direct Active (Join Ongoing Call)
|
|
171
171
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
172
|
+
func startCall(callId: String,
|
|
173
|
+
callType: String,
|
|
174
|
+
targetName: String,
|
|
175
|
+
metadata: String? = nil)
|
|
176
|
+
{
|
|
177
|
+
logger.info("startCall (modified for proper CallKit integration): \(callId)")
|
|
178
|
+
if let m = metadata {
|
|
179
|
+
callMetadata[callId] = m
|
|
180
|
+
logger.info("metadata cached for \(callId)")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
guard activeCalls[callId] == nil else {
|
|
184
|
+
logger.warning("call \(callId) already exists")
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
guard validateOutgoingCallRequest() else {
|
|
189
|
+
logger.warning("rejecting join call \(callId) - conflicts")
|
|
190
|
+
emitEvent(.callRejected, data: ["callId": callId,
|
|
191
|
+
"reason": "Conflict with existing call"])
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if canMakeMultipleCalls {
|
|
196
|
+
activeCalls.values
|
|
197
|
+
.filter { $0.state == .active }
|
|
198
|
+
.forEach { call in
|
|
199
|
+
logger.info("holding existing \(call.callId)")
|
|
200
|
+
callKitManager.setCallOnHold(callId: call.callId, onHold: true)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Use standard outgoing call flow instead of trying to bypass it
|
|
205
|
+
var info = CallInfo(callId: callId,
|
|
206
|
+
callType: callType,
|
|
207
|
+
displayName: targetName,
|
|
208
|
+
pictureUrl: nil,
|
|
209
|
+
state: .dialing)
|
|
210
|
+
activeCalls[callId] = info
|
|
211
|
+
currentCallId = callId
|
|
212
|
+
|
|
213
|
+
audioManager.configureAudioSession(forCallType: callType == "Video",
|
|
214
|
+
isIncoming: false)
|
|
215
|
+
|
|
216
|
+
// Use standard outgoing call method
|
|
217
|
+
callKitManager.startOutgoingCall(callInfo: info) { [weak self] error in
|
|
218
|
+
if let e = error {
|
|
219
|
+
self?.logger.error("startCall failed: \(e.localizedDescription)")
|
|
220
|
+
self?.endCallInternal(callId: callId)
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Immediately report as connected since this is for joining ongoing calls
|
|
225
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
|
226
|
+
if let uuid = UUID(uuidString: callId) {
|
|
227
|
+
self?.callKitManager.provider.reportOutgoingCall(with: uuid, connectedAt: Date())
|
|
228
|
+
self?.logger.info("startCall: reported as connected")
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
229
233
|
// MARK: JS Actions
|
|
230
234
|
|
|
231
235
|
func answerCall(callId: String) {
|
|
@@ -415,25 +419,29 @@ extension CallEngine: CallKitManagerDelegate {
|
|
|
415
419
|
didSetHeld callId: String,
|
|
416
420
|
onHold: Bool)
|
|
417
421
|
{
|
|
418
|
-
|
|
419
|
-
|
|
422
|
+
logger.info("didSetHeld: \(callId), onHold=\(onHold)")
|
|
423
|
+
guard var info = activeCalls[callId] else { return }
|
|
420
424
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
425
|
+
if onHold {
|
|
426
|
+
info.updateState(.held)
|
|
427
|
+
info.wasHeldBySystem = true
|
|
428
|
+
emitEvent(.callHeld, data: ["callId": callId])
|
|
429
|
+
} else {
|
|
430
|
+
info.updateState(.active)
|
|
431
|
+
info.wasHeldBySystem = false
|
|
432
|
+
currentCallId = callId
|
|
429
433
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
isIncoming: false)
|
|
434
|
+
// CRITICAL FIX: Force reconfigure audio session when unholding
|
|
435
|
+
audioManager.reconfigureAudioSessionForUnhold(forCallType: info.callType == "Video")
|
|
433
436
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
+
// Give the audio session a moment to stabilize
|
|
438
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
439
|
+
self.audioManager.activateAudioSession()
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
emitEvent(.callUnheld, data: ["callId": callId])
|
|
443
|
+
}
|
|
444
|
+
activeCalls[callId] = info
|
|
437
445
|
}
|
|
438
446
|
|
|
439
447
|
func callKitManager(_ manager: CallKitManager,
|
package/ios/CallKitManager.swift
CHANGED
|
@@ -74,46 +74,6 @@ class CallKitManager: NSObject {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
func reportConnectedOutgoingCall(callInfo: CallInfo,
|
|
78
|
-
completion: @escaping (Error?) -> Void)
|
|
79
|
-
{
|
|
80
|
-
logger.info("reportConnectedOutgoingCall: \(callInfo.callId)")
|
|
81
|
-
guard let uuid = UUID(uuidString: callInfo.callId) else {
|
|
82
|
-
let err = NSError(domain: "CallKitManager",
|
|
83
|
-
code: -1,
|
|
84
|
-
userInfo: [NSLocalizedDescriptionKey: "Invalid UUID"])
|
|
85
|
-
completion(err)
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
let handle = CXHandle(type: .generic, value: callInfo.displayName)
|
|
90
|
-
let action = CXStartCallAction(call: uuid, handle: handle)
|
|
91
|
-
action.isVideo = callInfo.callType == "Video"
|
|
92
|
-
|
|
93
|
-
let tx = CXTransaction(action: action)
|
|
94
|
-
activeCallIds.insert(callInfo.callId)
|
|
95
|
-
|
|
96
|
-
callController.request(tx) { [weak self] error in
|
|
97
|
-
if let e = error {
|
|
98
|
-
self?.logger.error("reportConnectedOutgoingCall error: \(e.localizedDescription)")
|
|
99
|
-
self?.activeCallIds.remove(callInfo.callId)
|
|
100
|
-
completion(e)
|
|
101
|
-
return
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Immediately report the call as connected (no ringing phase)
|
|
105
|
-
let update = CXCallUpdate()
|
|
106
|
-
update.remoteHandle = handle
|
|
107
|
-
update.localizedCallerName = callInfo.displayName
|
|
108
|
-
update.hasVideo = callInfo.callType == "Video"
|
|
109
|
-
|
|
110
|
-
// Report as connected at current time - this skips ringing
|
|
111
|
-
self?.provider.reportOutgoingCall(with: uuid, connectedAt: Date())
|
|
112
|
-
self?.logger.info("Call reported as immediately connected")
|
|
113
|
-
completion(nil)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
77
|
func startOutgoingCall(callInfo: CallInfo,
|
|
118
78
|
completion: @escaping (Error?) -> Void)
|
|
119
79
|
{
|