@qusaieilouti99/call-manager 0.1.129 → 0.1.131
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 +236 -42
- package/ios/CallEngine.swift +80 -78
- package/ios/CallKitManager.swift +0 -40
- package/package.json +1 -1
package/ios/AudioManager.swift
CHANGED
|
@@ -18,10 +18,21 @@ class AudioManager {
|
|
|
18
18
|
private let session = AVAudioSession.sharedInstance()
|
|
19
19
|
private var observers: [NSObjectProtocol] = []
|
|
20
20
|
|
|
21
|
+
// WebRTC compatibility flags
|
|
22
|
+
private var isWebRTCActive = false
|
|
23
|
+
private var isCallKitActive = false
|
|
24
|
+
private var pendingActivation = false
|
|
25
|
+
|
|
26
|
+
// Audio state management
|
|
27
|
+
private var currentCategory: AVAudioSession.Category = .playback
|
|
28
|
+
private var currentMode: AVAudioSession.Mode = .default
|
|
29
|
+
private var currentOptions: AVAudioSession.CategoryOptions = []
|
|
30
|
+
|
|
21
31
|
init(delegate: AudioManagerDelegate) {
|
|
22
32
|
self.delegate = delegate
|
|
23
33
|
logger.info("AudioManager init")
|
|
24
34
|
setupNotifications()
|
|
35
|
+
setupInitialAudioSession()
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
deinit {
|
|
@@ -44,83 +55,245 @@ class AudioManager {
|
|
|
44
55
|
queue: nil
|
|
45
56
|
) { [weak self] n in self?.handleInterruption(n) }
|
|
46
57
|
)
|
|
58
|
+
observers.append(
|
|
59
|
+
nc.addObserver(
|
|
60
|
+
forName: AVAudioSession.mediaServicesWereResetNotification,
|
|
61
|
+
object: nil,
|
|
62
|
+
queue: nil
|
|
63
|
+
) { [weak self] _ in self?.handleMediaServicesReset() }
|
|
64
|
+
)
|
|
47
65
|
logger.info("AudioManager notifications set")
|
|
48
66
|
}
|
|
49
67
|
|
|
68
|
+
private func setupInitialAudioSession() {
|
|
69
|
+
logger.info("Setting up initial audio session")
|
|
70
|
+
do {
|
|
71
|
+
// Start with playback category for non-call state
|
|
72
|
+
try session.setCategory(.playback,
|
|
73
|
+
mode: .default,
|
|
74
|
+
options: [.mixWithOthers])
|
|
75
|
+
currentCategory = .playback
|
|
76
|
+
currentMode = .default
|
|
77
|
+
currentOptions = [.mixWithOthers]
|
|
78
|
+
logger.info("Initial audio session configured")
|
|
79
|
+
} catch {
|
|
80
|
+
logger.error("Failed to setup initial audio session: \(error.localizedDescription)")
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// MARK: - WebRTC Compatible Audio Session Management
|
|
85
|
+
|
|
50
86
|
func configureAudioSession(forCallType isVideo: Bool,
|
|
51
|
-
isIncoming: Bool
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
87
|
+
isIncoming: Bool,
|
|
88
|
+
isWebRTCCall: Bool = true) {
|
|
89
|
+
logger.info("configureAudioSession: video=\(isVideo), incoming=\(isIncoming), webrtc=\(isWebRTCCall)")
|
|
90
|
+
|
|
91
|
+
isWebRTCActive = isWebRTCCall
|
|
92
|
+
|
|
93
|
+
// Define optimal configuration for VoIP calls
|
|
94
|
+
let category: AVAudioSession.Category = .playAndRecord
|
|
95
|
+
let mode: AVAudioSession.Mode = .voiceChat
|
|
96
|
+
var options: AVAudioSession.CategoryOptions = [
|
|
97
|
+
.allowBluetooth,
|
|
98
|
+
.allowBluetoothA2DP,
|
|
99
|
+
.mixWithOthers,
|
|
100
|
+
.duckOthers
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
// Video calls should default to speaker
|
|
104
|
+
if isVideo {
|
|
105
|
+
options.insert(.defaultToSpeaker)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Store configuration for later use
|
|
109
|
+
currentCategory = category
|
|
110
|
+
currentMode = mode
|
|
111
|
+
currentOptions = options
|
|
112
|
+
|
|
113
|
+
// Don't activate immediately if we're waiting for CallKit
|
|
114
|
+
if !isCallKitActive {
|
|
115
|
+
pendingActivation = true
|
|
116
|
+
logger.info("Audio session configured but not activated - waiting for CallKit")
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
applyAudioConfiguration()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private func applyAudioConfiguration() {
|
|
124
|
+
logger.info("Applying audio configuration: \(currentCategory), \(currentMode)")
|
|
125
|
+
|
|
55
126
|
do {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
127
|
+
// First, ensure we're not active to avoid conflicts
|
|
128
|
+
if session.isOtherAudioPlaying {
|
|
129
|
+
logger.info("Other audio is playing, will configure without activation")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try session.setCategory(currentCategory,
|
|
133
|
+
mode: currentMode,
|
|
134
|
+
options: currentOptions)
|
|
135
|
+
logger.info("Audio session category applied successfully")
|
|
136
|
+
|
|
60
137
|
} catch {
|
|
61
|
-
logger.error("
|
|
138
|
+
logger.error("Failed to apply audio configuration: \(error.localizedDescription)")
|
|
62
139
|
}
|
|
63
140
|
}
|
|
64
141
|
|
|
142
|
+
// MARK: - CallKit Integration Points
|
|
143
|
+
|
|
65
144
|
func activateAudioSession() {
|
|
66
|
-
logger.info("activateAudioSession")
|
|
145
|
+
logger.info("activateAudioSession - CallKit activation")
|
|
146
|
+
isCallKitActive = true
|
|
147
|
+
|
|
148
|
+
// Apply pending configuration if we have one
|
|
149
|
+
if pendingActivation {
|
|
150
|
+
applyAudioConfiguration()
|
|
151
|
+
pendingActivation = false
|
|
152
|
+
}
|
|
153
|
+
|
|
67
154
|
do {
|
|
68
155
|
try session.setActive(true)
|
|
156
|
+
logger.info("Audio session activated successfully")
|
|
69
157
|
delegate?.audioManagerDidActivateAudioSession(self)
|
|
70
158
|
} catch {
|
|
71
|
-
logger.error("activate
|
|
159
|
+
logger.error("Failed to activate audio session: \(error.localizedDescription)")
|
|
72
160
|
}
|
|
73
161
|
}
|
|
74
162
|
|
|
75
163
|
func deactivateAudioSession() {
|
|
76
|
-
logger.info("deactivateAudioSession")
|
|
164
|
+
logger.info("deactivateAudioSession - CallKit deactivation")
|
|
165
|
+
isCallKitActive = false
|
|
166
|
+
isWebRTCActive = false
|
|
167
|
+
pendingActivation = false
|
|
168
|
+
|
|
77
169
|
do {
|
|
78
170
|
try session.setActive(false, options: .notifyOthersOnDeactivation)
|
|
171
|
+
logger.info("Audio session deactivated successfully")
|
|
172
|
+
|
|
173
|
+
// Reset to default configuration for non-call state
|
|
174
|
+
try session.setCategory(.playback,
|
|
175
|
+
mode: .default,
|
|
176
|
+
options: [.mixWithOthers])
|
|
177
|
+
currentCategory = .playback
|
|
178
|
+
currentMode = .default
|
|
179
|
+
currentOptions = [.mixWithOthers]
|
|
180
|
+
logger.info("Audio session reset to default configuration")
|
|
181
|
+
|
|
79
182
|
delegate?.audioManagerDidDeactivateAudioSession(self)
|
|
80
183
|
} catch {
|
|
81
|
-
logger.error("deactivate
|
|
184
|
+
logger.error("Failed to deactivate audio session: \(error.localizedDescription)")
|
|
82
185
|
}
|
|
83
186
|
}
|
|
84
187
|
|
|
188
|
+
// MARK: - Hold/Unhold Handling (Critical for WebRTC)
|
|
189
|
+
|
|
190
|
+
func handleCallUnhold(forCallType isVideo: Bool) {
|
|
191
|
+
logger.info("handleCallUnhold: video=\(isVideo)")
|
|
192
|
+
|
|
193
|
+
// For WebRTC calls, we need to be very careful about audio session management
|
|
194
|
+
if isWebRTCActive {
|
|
195
|
+
logger.info("Handling WebRTC call unhold")
|
|
196
|
+
|
|
197
|
+
// Briefly deactivate and reactivate to ensure clean state
|
|
198
|
+
do {
|
|
199
|
+
try session.setActive(false, options: [])
|
|
200
|
+
logger.info("Audio session briefly deactivated for unhold")
|
|
201
|
+
|
|
202
|
+
// Small delay to allow system to process
|
|
203
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
|
204
|
+
self?.reconfigureForUnhold(isVideo: isVideo)
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
logger.error("Failed to deactivate for unhold: \(error.localizedDescription)")
|
|
208
|
+
// Fallback to direct reconfiguration
|
|
209
|
+
reconfigureForUnhold(isVideo: isVideo)
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
// Non-WebRTC calls can be handled normally
|
|
213
|
+
reconfigureForUnhold(isVideo: isVideo)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private func reconfigureForUnhold(isVideo: Bool) {
|
|
218
|
+
logger.info("Reconfiguring audio for unhold: video=\(isVideo)")
|
|
219
|
+
|
|
220
|
+
// Reconfigure with fresh settings
|
|
221
|
+
let category: AVAudioSession.Category = .playAndRecord
|
|
222
|
+
let mode: AVAudioSession.Mode = .voiceChat
|
|
223
|
+
var options: AVAudioSession.CategoryOptions = [
|
|
224
|
+
.allowBluetooth,
|
|
225
|
+
.allowBluetoothA2DP,
|
|
226
|
+
.mixWithOthers,
|
|
227
|
+
.duckOthers
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
if isVideo {
|
|
231
|
+
options.insert(.defaultToSpeaker)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
do {
|
|
235
|
+
try session.setCategory(category, mode: mode, options: options)
|
|
236
|
+
try session.setActive(true)
|
|
237
|
+
|
|
238
|
+
currentCategory = category
|
|
239
|
+
currentMode = mode
|
|
240
|
+
currentOptions = options
|
|
241
|
+
|
|
242
|
+
logger.info("Audio session reconfigured and activated for unhold")
|
|
243
|
+
} catch {
|
|
244
|
+
logger.error("Failed to reconfigure audio for unhold: \(error.localizedDescription)")
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// MARK: - Audio Device Management
|
|
249
|
+
|
|
85
250
|
func getAudioDevices() -> AudioRoutesInfo {
|
|
86
251
|
let current = session.currentRoute
|
|
87
252
|
var devices = ["Earpiece", "Speaker"]
|
|
253
|
+
|
|
88
254
|
if let inputs = session.availableInputs {
|
|
89
|
-
for
|
|
90
|
-
switch
|
|
255
|
+
for input in inputs {
|
|
256
|
+
switch input.portType {
|
|
91
257
|
case .bluetoothHFP, .bluetoothA2DP, .bluetoothLE:
|
|
92
|
-
if !devices.contains("Bluetooth") {
|
|
258
|
+
if !devices.contains("Bluetooth") {
|
|
259
|
+
devices.append("Bluetooth")
|
|
260
|
+
}
|
|
93
261
|
case .headphones, .headsetMic:
|
|
94
|
-
if !devices.contains("Headset") {
|
|
95
|
-
|
|
262
|
+
if !devices.contains("Headset") {
|
|
263
|
+
devices.append("Headset")
|
|
264
|
+
}
|
|
265
|
+
default:
|
|
266
|
+
break
|
|
96
267
|
}
|
|
97
268
|
}
|
|
98
269
|
}
|
|
99
|
-
let route = determineCurrentRoute()
|
|
100
270
|
|
|
101
|
-
|
|
271
|
+
let route = determineCurrentRoute()
|
|
102
272
|
let deviceHolders = devices.map { StringHolder(value: $0) }
|
|
103
|
-
|
|
104
|
-
return info
|
|
273
|
+
return AudioRoutesInfo(devices: deviceHolders, currentRoute: route)
|
|
105
274
|
}
|
|
106
275
|
|
|
107
276
|
func setAudioRoute(_ route: String) {
|
|
108
277
|
logger.info("setAudioRoute: \(route)")
|
|
278
|
+
|
|
109
279
|
do {
|
|
110
280
|
switch route {
|
|
111
281
|
case "Speaker":
|
|
112
282
|
try session.overrideOutputAudioPort(.speaker)
|
|
283
|
+
case "Earpiece", "Headset", "Bluetooth":
|
|
284
|
+
try session.overrideOutputAudioPort(.none)
|
|
113
285
|
default:
|
|
114
286
|
try session.overrideOutputAudioPort(.none)
|
|
115
287
|
}
|
|
288
|
+
logger.info("Audio route set to: \(route)")
|
|
116
289
|
} catch {
|
|
117
|
-
logger.error("
|
|
290
|
+
logger.error("Failed to set audio route to \(route): \(error.localizedDescription)")
|
|
118
291
|
}
|
|
119
292
|
}
|
|
120
293
|
|
|
121
294
|
private func determineCurrentRoute() -> String {
|
|
122
|
-
for
|
|
123
|
-
switch
|
|
295
|
+
for output in session.currentRoute.outputs {
|
|
296
|
+
switch output.portType {
|
|
124
297
|
case .bluetoothHFP, .bluetoothA2DP, .bluetoothLE:
|
|
125
298
|
return "Bluetooth"
|
|
126
299
|
case .builtInSpeaker:
|
|
@@ -129,18 +302,25 @@ class AudioManager {
|
|
|
129
302
|
return "Earpiece"
|
|
130
303
|
case .headphones, .headsetMic:
|
|
131
304
|
return "Headset"
|
|
132
|
-
default:
|
|
305
|
+
default:
|
|
306
|
+
continue
|
|
133
307
|
}
|
|
134
308
|
}
|
|
135
309
|
return "Earpiece"
|
|
136
310
|
}
|
|
137
311
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
312
|
+
// MARK: - Notification Handlers
|
|
313
|
+
|
|
314
|
+
private func handleRouteChange(_ notification: Notification) {
|
|
315
|
+
logger.info("Audio route change notification")
|
|
316
|
+
guard let reasonValue = notification.userInfo?[AVAudioSessionRouteChangeReasonKey] as? UInt,
|
|
317
|
+
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue)
|
|
318
|
+
else {
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
143
322
|
let info = getAudioDevices()
|
|
323
|
+
|
|
144
324
|
switch reason {
|
|
145
325
|
case .newDeviceAvailable, .oldDeviceUnavailable:
|
|
146
326
|
delegate?.audioManager(self, didChangeDevices: info)
|
|
@@ -149,23 +329,37 @@ class AudioManager {
|
|
|
149
329
|
}
|
|
150
330
|
}
|
|
151
331
|
|
|
152
|
-
private func handleInterruption(_
|
|
153
|
-
logger.info("interruption notification")
|
|
154
|
-
guard let
|
|
155
|
-
let type = AVAudioSession.InterruptionType(rawValue:
|
|
156
|
-
else {
|
|
332
|
+
private func handleInterruption(_ notification: Notification) {
|
|
333
|
+
logger.info("Audio interruption notification")
|
|
334
|
+
guard let typeValue = notification.userInfo?[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
335
|
+
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
|
|
336
|
+
else {
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
|
|
157
340
|
switch type {
|
|
158
341
|
case .began:
|
|
159
|
-
logger.info("interruption began")
|
|
342
|
+
logger.info("Audio interruption began")
|
|
160
343
|
case .ended:
|
|
161
|
-
logger.info("interruption ended")
|
|
162
|
-
if let
|
|
163
|
-
let
|
|
164
|
-
if
|
|
344
|
+
logger.info("Audio interruption ended")
|
|
345
|
+
if let optionsValue = notification.userInfo?[AVAudioSessionInterruptionOptionKey] as? UInt {
|
|
346
|
+
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
|
|
347
|
+
if options.contains(.shouldResume) && isCallKitActive {
|
|
348
|
+
logger.info("Resuming audio session after interruption")
|
|
165
349
|
activateAudioSession()
|
|
166
350
|
}
|
|
167
351
|
}
|
|
168
|
-
@unknown default:
|
|
352
|
+
@unknown default:
|
|
353
|
+
break
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private func handleMediaServicesReset() {
|
|
358
|
+
logger.warning("Media services were reset - reconfiguring audio")
|
|
359
|
+
if isCallKitActive {
|
|
360
|
+
// Reconfigure the audio session
|
|
361
|
+
applyAudioConfiguration()
|
|
362
|
+
activateAudioSession()
|
|
169
363
|
}
|
|
170
364
|
}
|
|
171
365
|
}
|
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,24 @@ 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: Use the new WebRTC-compatible unhold handling
|
|
435
|
+
audioManager.handleCallUnhold(forCallType: info.callType == "Video")
|
|
433
436
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
+
emitEvent(.callUnheld, data: ["callId": callId])
|
|
438
|
+
}
|
|
439
|
+
activeCalls[callId] = info
|
|
437
440
|
}
|
|
438
441
|
|
|
439
442
|
func callKitManager(_ manager: CallKitManager,
|
|
@@ -462,17 +465,16 @@ extension CallEngine: CallKitManagerDelegate {
|
|
|
462
465
|
func callKitManager(_ manager: CallKitManager,
|
|
463
466
|
didActivateAudioSession session: AVAudioSession)
|
|
464
467
|
{
|
|
465
|
-
|
|
466
|
-
|
|
468
|
+
logger.info("CallKit didActivate audioSession")
|
|
469
|
+
audioManager.activateAudioSession()
|
|
467
470
|
}
|
|
468
471
|
|
|
469
472
|
func callKitManager(_ manager: CallKitManager,
|
|
470
473
|
didDeactivateAudioSession session: AVAudioSession)
|
|
471
474
|
{
|
|
472
|
-
|
|
473
|
-
|
|
475
|
+
logger.info("CallKit didDeactivate audioSession")
|
|
476
|
+
audioManager.deactivateAudioSession()
|
|
474
477
|
}
|
|
475
|
-
}
|
|
476
478
|
|
|
477
479
|
// MARK: AudioManagerDelegate
|
|
478
480
|
|
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
|
{
|