@qusaieilouti99/call-manager 0.1.80 → 0.1.82

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.
@@ -5,6 +5,8 @@ import OSLog
5
5
  protocol AudioManagerDelegate: AnyObject {
6
6
  func audioManager(_ manager: AudioManager, didChangeRoute routeInfo: AudioRoutesInfo)
7
7
  func audioManager(_ manager: AudioManager, didChangeDevices routeInfo: AudioRoutesInfo)
8
+ func audioManagerDidActivateAudioSession(_ manager: AudioManager)
9
+ func audioManagerDidDeactivateAudioSession(_ manager: AudioManager)
8
10
  }
9
11
 
10
12
  class AudioManager {
@@ -55,55 +57,33 @@ class AudioManager {
55
57
  logger.info("🔊 ✅ Audio session notifications setup completed")
56
58
  }
57
59
 
58
- func configureForIncomingCall() {
59
- logger.info("🔊 Configuring audio session for incoming call...")
60
+ // MARK: - Audio Session Configuration for CallKit
60
61
 
61
- do {
62
- try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: [.allowBluetooth, .allowBluetoothA2DP])
63
- try audioSession.setActive(true)
64
- logger.info("🔊 ✅ Audio session configured for incoming call")
65
- } catch {
66
- logger.error("🔊 ❌ Failed to configure audio session for incoming call: \(error.localizedDescription)")
67
- }
68
- }
69
-
70
- func configureForOutgoingCall(isVideo: Bool) {
71
- logger.info("🔊 Configuring audio session for outgoing call (video: \(isVideo))...")
62
+ func configureAudioSession(forCallType isVideo: Bool, isIncoming: Bool) {
63
+ logger.info("🔊 Configuring audio session: isVideo=\(isVideo), isIncoming=\(isIncoming)...")
72
64
 
73
- do {
74
- let options: AVAudioSession.CategoryOptions = isVideo ?
75
- [.allowBluetooth, .allowBluetoothA2DP, .defaultToSpeaker] :
76
- [.allowBluetooth, .allowBluetoothA2DP]
77
-
78
- try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: options)
79
- try audioSession.setActive(true)
80
- logger.info("🔊 ✅ Audio session configured for outgoing call (video: \(isVideo))")
81
- } catch {
82
- logger.error("🔊 ❌ Failed to configure audio session for outgoing call: \(error.localizedDescription)")
65
+ let options: AVAudioSession.CategoryOptions
66
+ if isVideo {
67
+ options = [.allowBluetooth, .allowBluetoothA2DP, .defaultToSpeaker]
68
+ } else {
69
+ options = [.allowBluetooth, .allowBluetoothA2DP]
83
70
  }
84
- }
85
-
86
- func configureForActiveCall(isVideo: Bool) {
87
- logger.info("🔊 Configuring audio session for active call (video: \(isVideo))...")
88
71
 
89
72
  do {
90
- let options: AVAudioSession.CategoryOptions = isVideo ?
91
- [.allowBluetooth, .allowBluetoothA2DP, .defaultToSpeaker] :
92
- [.allowBluetooth, .allowBluetoothA2DP]
93
-
94
73
  try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: options)
95
- try audioSession.setActive(true)
96
- logger.info("🔊 ✅ Audio session configured for active call (video: \(isVideo))")
74
+ logger.info("🔊 ✅ Audio session category/mode/options set for call")
97
75
  } catch {
98
- logger.error("🔊 ❌ Failed to configure audio session for active call: \(error.localizedDescription)")
76
+ logger.error("🔊 ❌ Failed to configure audio session: \(error.localizedDescription)")
99
77
  }
100
78
  }
101
79
 
80
+ // MARK: - Audio Route Management
81
+
102
82
  func getAudioDevices() -> AudioRoutesInfo {
103
83
  logger.debug("🔊 Getting available audio devices...")
104
84
 
105
85
  let currentRoute = audioSession.currentRoute
106
- var devices: [String] = ["Earpiece", "Speaker"]
86
+ var devices: [String] = ["Earpiece", "Speaker"] // Default options
107
87
 
108
88
  logger.debug("🔊 Current route inputs: \(currentRoute.inputs.map { $0.portType.rawValue })")
109
89
  logger.debug("🔊 Current route outputs: \(currentRoute.outputs.map { $0.portType.rawValue })")
@@ -118,7 +98,7 @@ class AudioManager {
118
98
  devices.append("Bluetooth")
119
99
  logger.debug("🔊 Added Bluetooth device")
120
100
  }
121
- case .headphones, .headsetMic, .wiredHeadphones:
101
+ case .headphones, .headsetMic:
122
102
  if !devices.contains("Headset") {
123
103
  devices.append("Headset")
124
104
  logger.debug("🔊 Added Headset device")
@@ -151,13 +131,10 @@ class AudioManager {
151
131
  logger.debug("🔊 Overriding to speaker...")
152
132
  try audioSession.overrideOutputAudioPort(.speaker)
153
133
  case "Earpiece":
154
- logger.debug("🔊 Overriding to earpiece...")
155
- try audioSession.overrideOutputAudioPort(.none)
156
- case "Bluetooth":
157
- logger.debug("🔊 Setting to Bluetooth (system managed)...")
134
+ logger.debug("🔊 Overriding to earpiece (built-in receiver)...")
158
135
  try audioSession.overrideOutputAudioPort(.none)
159
- case "Headset":
160
- logger.debug("🔊 Setting to Headset (system managed)...")
136
+ case "Bluetooth", "Headset":
137
+ logger.debug("🔊 Setting to Bluetooth/Headset (system managed via .none)...")
161
138
  try audioSession.overrideOutputAudioPort(.none)
162
139
  default:
163
140
  logger.warning("🔊 ⚠️ Unknown audio route: \(route)")
@@ -179,17 +156,32 @@ class AudioManager {
179
156
  logger.info("🔊 Mute state changed to: \(muted)")
180
157
  }
181
158
 
182
- func cleanup() {
183
- logger.info("🔊 Cleaning up audio session...")
159
+ // MARK: - Audio Session Activation/Deactivation from CallKit
184
160
 
161
+ func activateAudioSession() {
162
+ logger.info("🔊 Audio session activation requested by CallKit...")
163
+ do {
164
+ try audioSession.setActive(true)
165
+ logger.info("🔊 ✅ Audio session activated successfully")
166
+ delegate?.audioManagerDidActivateAudioSession(self)
167
+ } catch {
168
+ logger.error("🔊 ❌ Failed to activate audio session: \(error.localizedDescription)")
169
+ }
170
+ }
171
+
172
+ func deactivateAudioSession() {
173
+ logger.info("🔊 Audio session deactivation requested by CallKit...")
185
174
  do {
186
175
  try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
187
176
  logger.info("🔊 ✅ Audio session deactivated successfully")
177
+ delegate?.audioManagerDidDeactivateAudioSession(self)
188
178
  } catch {
189
179
  logger.error("🔊 ❌ Failed to deactivate audio session: \(error.localizedDescription)")
190
180
  }
191
181
  }
192
182
 
183
+ // MARK: - Internal Helper Methods
184
+
193
185
  private func getCurrentAudioRoute() -> String {
194
186
  let currentRoute = audioSession.currentRoute
195
187
 
@@ -202,11 +194,12 @@ class AudioManager {
202
194
  return "Bluetooth"
203
195
  case .builtInSpeaker:
204
196
  return "Speaker"
205
- case .headphones, .headsetMic, .wiredHeadphones:
197
+ case .headphones, .headsetMic:
206
198
  return "Headset"
207
199
  case .builtInReceiver:
208
200
  return "Earpiece"
209
201
  default:
202
+ logger.debug("🔊 Unhandled output type: \(routeType.rawValue)")
210
203
  continue
211
204
  }
212
205
  }
@@ -227,6 +220,8 @@ class AudioManager {
227
220
  self.delegate?.audioManager(self, didChangeDevices: routeInfo)
228
221
  }
229
222
 
223
+ // MARK: - Notification Handlers
224
+
230
225
  private func handleAudioRouteChanged(notification: Notification) {
231
226
  logger.info("🔊 Audio route changed notification received")
232
227
 
@@ -237,17 +232,20 @@ class AudioManager {
237
232
  return
238
233
  }
239
234
 
240
- logger.info("🔊 Route change reason: \(reason)")
235
+ logger.info("🔊 Route change reason: \(reason.rawValue)")
241
236
 
242
237
  switch reason {
243
- case .newDeviceAvailable, .oldDeviceUnavailable:
244
- logger.info("🔊 Audio device availability changed: \(reason)")
238
+ case AVAudioSession.RouteChangeReason.newDeviceAvailable, AVAudioSession.RouteChangeReason.oldDeviceUnavailable:
239
+ logger.info("🔊 Audio device availability changed: \(reason.rawValue)") // Use .rawValue
245
240
  notifyDeviceChange()
246
- case .override, .categoryChange:
247
- logger.info("🔊 Audio route override or category change: \(reason)")
241
+ case AVAudioSession.RouteChangeReason.override, AVAudioSession.RouteChangeReason.categoryChange:
242
+ logger.info("🔊 Audio route override or category change: \(reason.rawValue)") // Use .rawValue
243
+ notifyRouteChange()
244
+ case AVAudioSession.RouteChangeReason.wakeFromSleep, AVAudioSession.RouteChangeReason.noSuitableRouteForCategory:
245
+ logger.info("🔊 Session recovered or no suitable route: \(reason.rawValue)") // Use .rawValue
248
246
  notifyRouteChange()
249
247
  default:
250
- logger.info("🔊 Other audio route change reason: \(reason)")
248
+ logger.info("🔊 Other audio route change reason: \(reason.rawValue)") // Use .rawValue
251
249
  notifyRouteChange()
252
250
  }
253
251
  }
@@ -40,6 +40,7 @@ public class CallEngine {
40
40
 
41
41
  callKitManager = CallKitManager(delegate: self)
42
42
  audioManager = AudioManager(delegate: self)
43
+ VoIPTokenManager.shared.setupPushKit()
43
44
 
44
45
  isInitialized = true
45
46
  logger.info("🚀 ✅ CallEngine initialized successfully")
@@ -54,7 +55,8 @@ public class CallEngine {
54
55
  logger.debug("🚀 Getting current call state...")
55
56
  let callsArray = self.activeCalls.values.map { $0.toJSONObject() }
56
57
  do {
57
- let jsonData = try JSONSerialization.data(withJSONObject: callsArray, options: [])
58
+ // Line 466 (previously)
59
+ let jsonData = try JSONSerialization.data(withJSONObject: callsArray as Any, options: JSONSerialization.WritingOptions.prettyPrinted)
58
60
  let result = String(data: jsonData, encoding: .utf8) ?? "[]"
59
61
  logger.debug("🚀 Current call state: \(result)")
60
62
  return result
@@ -85,16 +87,17 @@ public class CallEngine {
85
87
  return
86
88
  }
87
89
 
88
- if let activeCall = self.activeCalls.values.first(where: { $0.state == .active || $0.state == .held }),
89
- !canMakeMultipleCalls {
90
- logger.warning("📞 ⚠️ Active call exists when receiving incoming call. Auto-rejecting: \(callId)")
91
- rejectIncomingCallCollision(callId: callId, reason: "Another call is already active")
92
- return
90
+ if !canMakeMultipleCalls {
91
+ if let activeCall = self.activeCalls.values.first(where: { $0.state == .active || $0.state == .held }) {
92
+ logger.warning("📞 ⚠️ Active call (\(activeCall.callId)) exists when receiving incoming call. Auto-rejecting: \(callId)")
93
+ rejectIncomingCallCollision(callId: callId, reason: "Another call is already active")
94
+ return
95
+ }
93
96
  }
94
97
 
95
- if !canMakeMultipleCalls {
98
+ if canMakeMultipleCalls {
96
99
  for call in self.activeCalls.values where call.state == .active {
97
- logger.info("📞 Holding existing active call: \(call.callId)")
100
+ logger.info("📞 Holding existing active call: \(call.callId) before new incoming call")
98
101
  holdCallInternal(callId: call.callId, heldBySystem: false)
99
102
  }
100
103
  }
@@ -111,6 +114,8 @@ public class CallEngine {
111
114
  self.currentCallId = callId
112
115
  logger.info("📞 Call added to active calls. Total: \(self.activeCalls.count)")
113
116
 
117
+ audioManager?.configureAudioSession(forCallType: callType == "Video", isIncoming: true)
118
+
114
119
  self.callKitManager?.reportIncomingCall(callInfo: callInfo) { [weak self] error in
115
120
  guard let self = self else { return }
116
121
  if let error = error {
@@ -118,7 +123,6 @@ public class CallEngine {
118
123
  self.endCallInternal(callId: callId)
119
124
  } else {
120
125
  self.logger.info("📞 ✅ Successfully reported incoming call for \(callId)")
121
- self.audioManager?.configureForIncomingCall()
122
126
  }
123
127
  }
124
128
  }
@@ -145,9 +149,9 @@ public class CallEngine {
145
149
  return
146
150
  }
147
151
 
148
- if !canMakeMultipleCalls {
152
+ if canMakeMultipleCalls {
149
153
  for call in self.activeCalls.values where call.state == .active {
150
- logger.info("📞 Holding existing active call: \(call.callId)")
154
+ logger.info("📞 Holding existing active call: \(call.callId) before new outgoing call")
151
155
  holdCallInternal(callId: call.callId, heldBySystem: false)
152
156
  }
153
157
  }
@@ -164,14 +168,15 @@ public class CallEngine {
164
168
  self.currentCallId = callId
165
169
  logger.info("📞 Call added to active calls. Total: \(self.activeCalls.count)")
166
170
 
171
+ audioManager?.configureAudioSession(forCallType: callType == "Video", isIncoming: false)
172
+
167
173
  self.callKitManager?.startOutgoingCall(callInfo: callInfo) { [weak self] error in
168
174
  guard let self = self else { return }
169
175
  if let error = error {
170
176
  self.logger.error("📞 ❌ Failed to start outgoing call: \(error.localizedDescription)")
171
177
  self.endCallInternal(callId: callId)
172
178
  } else {
173
- self.logger.info("📞 ✅ Successfully started outgoing call for \(callId)")
174
- self.audioManager?.configureForOutgoingCall(isVideo: callType == "Video")
179
+ self.logger.info("📞 ✅ Successfully initiated outgoing call for \(callId) via CallKit")
175
180
  }
176
181
  }
177
182
  }
@@ -194,9 +199,9 @@ public class CallEngine {
194
199
  return
195
200
  }
196
201
 
197
- if !canMakeMultipleCalls {
202
+ if canMakeMultipleCalls {
198
203
  for call in self.activeCalls.values where call.state == .active {
199
- logger.info("📞 Holding existing active call: \(call.callId)")
204
+ logger.info("📞 Holding existing active call: \(call.callId) before new direct active call")
200
205
  holdCallInternal(callId: call.callId, heldBySystem: false)
201
206
  }
202
207
  }
@@ -213,9 +218,10 @@ public class CallEngine {
213
218
  self.currentCallId = callId
214
219
  logger.info("📞 Call added as ACTIVE. Total: \(self.activeCalls.count)")
215
220
 
216
- self.audioManager?.configureForActiveCall(isVideo: callType == "Video")
217
- emitOutgoingCallAnsweredWithMetadata(callId: callId)
221
+ audioManager?.configureAudioSession(forCallType: callType == "Video", isIncoming: false)
222
+ audioManager?.activateAudioSession()
218
223
 
224
+ emitOutgoingCallAnsweredWithMetadata(callId: callId)
219
225
  logger.info("📞 ✅ Call \(callId) started as ACTIVE")
220
226
  }
221
227
 
@@ -226,7 +232,19 @@ public class CallEngine {
226
232
 
227
233
  public func answerCall(callId: String) {
228
234
  logger.info("📞 Local party answering: \(callId)")
229
- coreCallAnswered(callId: callId, isLocalAnswer: true)
235
+ if let uuid = UUID(uuidString: callId) {
236
+ let answerAction = CXAnswerCallAction(call: uuid)
237
+ let transaction = CXTransaction(action: answerAction)
238
+ callKitManager?.callController.request(transaction) { error in
239
+ if let error = error {
240
+ self.logger.error("📞 ❌ Failed to request CallKit answer: \(error.localizedDescription)")
241
+ } else {
242
+ self.logger.info("📞 ✅ Requested CallKit to answer call \(callId)")
243
+ }
244
+ }
245
+ } else {
246
+ logger.warning("📞 ⚠️ Invalid UUID for callId: \(callId)")
247
+ }
230
248
  }
231
249
 
232
250
  private func coreCallAnswered(callId: String, isLocalAnswer: Bool) {
@@ -244,11 +262,11 @@ public class CallEngine {
244
262
 
245
263
  logger.info("📞 Call state updated: \(previousState.stringValue) → \(CallState.active.stringValue)")
246
264
 
247
- self.audioManager?.configureForActiveCall(isVideo: callInfo.callType == "Video")
265
+ audioManager?.configureAudioSession(forCallType: callInfo.callType == "Video", isIncoming: false)
248
266
 
249
267
  if !canMakeMultipleCalls {
250
- for call in self.activeCalls.values where call.callId != callId && call.state == .active {
251
- logger.info("📞 Holding other active call: \(call.callId)")
268
+ for call in self.activeCalls.values where call.callId != callId && (call.state == .active || call.state == .incoming) {
269
+ logger.info("📞 Holding other active/incoming call: \(call.callId)")
252
270
  holdCallInternal(callId: call.callId, heldBySystem: false)
253
271
  }
254
272
  }
@@ -282,19 +300,22 @@ public class CallEngine {
282
300
  private func holdCallInternal(callId: String, heldBySystem: Bool) {
283
301
  logger.info("📞 Holding call internally: callId=\(callId), heldBySystem=\(heldBySystem)")
284
302
 
285
- guard var callInfo = self.activeCalls[callId], callInfo.state == .active else {
286
- logger.warning("📞 ⚠️ Cannot hold call \(callId) - not in active state")
303
+ guard var callInfo = self.activeCalls[callId] else {
304
+ logger.warning("📞 ⚠️ Cannot hold call \(callId) - not found")
287
305
  return
288
306
  }
289
307
 
290
- callInfo.updateState(.held)
291
- callInfo.wasHeldBySystem = heldBySystem
292
- self.activeCalls[callId] = callInfo
293
-
294
- self.callKitManager?.setCallOnHold(callId: callId, onHold: true)
295
- emitEvent(.callHeld, data: ["callId": callId])
308
+ if callInfo.state == .active {
309
+ callInfo.updateState(.held)
310
+ callInfo.wasHeldBySystem = heldBySystem
311
+ self.activeCalls[callId] = callInfo
296
312
 
297
- logger.info("📞 ✅ Call \(callId) held successfully")
313
+ self.callKitManager?.setCallOnHold(callId: callId, onHold: true)
314
+ emitEvent(.callHeld, data: ["callId": callId])
315
+ logger.info("📞 ✅ Call \(callId) held successfully")
316
+ } else {
317
+ logger.warning("📞 ⚠️ Cannot hold call \(callId) from state \(callInfo.state.stringValue). Expected .active")
318
+ }
298
319
  }
299
320
 
300
321
  public func unholdCall(callId: String) {
@@ -304,19 +325,23 @@ public class CallEngine {
304
325
  private func unholdCallInternal(callId: String, resumedBySystem: Bool) {
305
326
  logger.info("📞 Unholding call internally: callId=\(callId), resumedBySystem=\(resumedBySystem)")
306
327
 
307
- guard var callInfo = self.activeCalls[callId], callInfo.state == .held else {
308
- logger.warning("📞 ⚠️ Cannot unhold call \(callId) - not in held state")
328
+ guard var callInfo = self.activeCalls[callId] else {
329
+ logger.warning("📞 ⚠️ Cannot unhold call \(callId) - not found")
309
330
  return
310
331
  }
311
332
 
312
- callInfo.updateState(.active)
313
- callInfo.wasHeldBySystem = false
314
- self.activeCalls[callId] = callInfo
315
-
316
- self.callKitManager?.setCallOnHold(callId: callId, onHold: false)
317
- emitEvent(.callUnheld, data: ["callId": callId])
333
+ if callInfo.state == .held {
334
+ callInfo.updateState(.active)
335
+ callInfo.wasHeldBySystem = false
336
+ self.activeCalls[callId] = callInfo
318
337
 
319
- logger.info("📞 Call \(callId) unheld successfully")
338
+ self.currentCallId = callId
339
+ self.callKitManager?.setCallOnHold(callId: callId, onHold: false)
340
+ emitEvent(.callUnheld, data: ["callId": callId])
341
+ logger.info("📞 ✅ Call \(callId) unheld successfully")
342
+ } else {
343
+ logger.warning("📞 ⚠️ Cannot unhold call \(callId) from state \(callInfo.state.stringValue). Expected .held")
344
+ }
320
345
  }
321
346
 
322
347
  public func setMuted(callId: String, muted: Bool) {
@@ -327,7 +352,8 @@ public class CallEngine {
327
352
  return
328
353
  }
329
354
 
330
- self.audioManager?.setMuted(muted)
355
+ self.callKitManager?.setMuted(callId: callId, muted: muted)
356
+
331
357
  let eventType: CallEventType = muted ? .callMuted : .callUnmuted
332
358
  emitEvent(eventType, data: ["callId": callId])
333
359
  logger.info("📞 ✅ Call \(callId) mute state changed to: \(muted)")
@@ -335,7 +361,21 @@ public class CallEngine {
335
361
 
336
362
  public func endCall(callId: String) {
337
363
  logger.info("📞 Ending call: \(callId)")
338
- endCallInternal(callId: callId)
364
+ if let uuid = UUID(uuidString: callId) {
365
+ let endCallAction = CXEndCallAction(call: uuid)
366
+ let transaction = CXTransaction(action: endCallAction)
367
+ callKitManager?.callController.request(transaction) { error in
368
+ if let error = error {
369
+ self.logger.error("📞 ❌ Failed to request CallKit end call: \(error.localizedDescription)")
370
+ self.endCallInternal(callId: callId)
371
+ } else {
372
+ self.logger.info("📞 ✅ Requested CallKit to end call \(callId)")
373
+ }
374
+ }
375
+ } else {
376
+ logger.warning("📞 ⚠️ Invalid UUID for end call: \(callId)")
377
+ self.endCallInternal(callId: callId)
378
+ }
339
379
  }
340
380
 
341
381
  public func endAllCalls() {
@@ -344,22 +384,17 @@ public class CallEngine {
344
384
  let callIds = Array(self.activeCalls.keys)
345
385
  for callId in callIds {
346
386
  logger.info("📞 Ending call: \(callId)")
347
- endCallInternal(callId: callId)
387
+ endCall(callId: callId)
348
388
  }
349
389
 
350
- self.activeCalls.removeAll()
351
- self.callMetadata.removeAll()
352
- self.currentCallId = nil
353
-
354
- self.audioManager?.cleanup()
355
- logger.info("📞 ✅ All calls ended and cleanup completed")
390
+ logger.info("📞 ✅ All calls termination initiated")
356
391
  }
357
392
 
358
393
  private func endCallInternal(callId: String) {
359
394
  logger.info("📞 Ending call internally: \(callId)")
360
395
 
361
396
  guard var callInfo = self.activeCalls[callId] else {
362
- logger.warning("📞 ⚠️ Call \(callId) not found in active calls")
397
+ logger.warning("📞 ⚠️ Call \(callId) not found in active calls (already ended?)")
363
398
  return
364
399
  }
365
400
 
@@ -368,15 +403,13 @@ public class CallEngine {
368
403
  self.activeCalls.removeValue(forKey: callId)
369
404
 
370
405
  if self.currentCallId == callId {
371
- self.currentCallId = self.activeCalls.values.first { $0.state != .ended }?.callId
406
+ self.currentCallId = self.activeCalls.values.first { $0.state == .active || $0.state == .held || $0.state == .dialing || $0.state == .incoming }?.callId
372
407
  logger.info("📞 Current call ID updated to: \(self.currentCallId ?? "nil")")
373
408
  }
374
409
 
375
- self.callKitManager?.endCall(callId: callId)
376
-
377
410
  if self.activeCalls.isEmpty {
378
- logger.info("📞 No more active calls, cleaning up audio")
379
- self.audioManager?.cleanup()
411
+ logger.info("📞 No more active calls, deactivating audio session")
412
+ audioManager?.deactivateAudioSession()
380
413
  }
381
414
 
382
415
  logger.info("📞 Notifying \(self.callEndListeners.count) internal call end listeners")
@@ -430,7 +463,7 @@ public class CallEngine {
430
463
  if let handler = handler, !self.cachedEvents.isEmpty {
431
464
  logger.info("📡 Emitting \(self.cachedEvents.count) cached events")
432
465
  for (type, data) in self.cachedEvents {
433
- logger.debug("📡 Emitting cached event: \(type)")
466
+ logger.debug("📡 Emitting cached event: \(type.rawValue)")
434
467
  handler(type, data)
435
468
  }
436
469
  self.cachedEvents.removeAll()
@@ -439,18 +472,19 @@ public class CallEngine {
439
472
  }
440
473
 
441
474
  private func emitEvent(_ type: CallEventType, data: [String: Any]) {
442
- logger.info("📡 Emitting event: \(type)")
475
+ logger.info("📡 Emitting event: \(type.rawValue)")
443
476
  logger.debug("📡 Event data: \(data)")
444
477
 
445
478
  do {
446
- let jsonData = try JSONSerialization.data(withJSONObject: data, options: [])
479
+ // Line 486 (previously)
480
+ let jsonData = try JSONSerialization.data(withJSONObject: data as Any, options: JSONSerialization.WritingOptions.prettyPrinted)
447
481
  let dataString = String(data: jsonData, encoding: .utf8) ?? "{}"
448
482
 
449
483
  if let eventHandler = self.eventHandler {
450
484
  logger.debug("📡 Calling event handler with data: \(dataString)")
451
485
  eventHandler(type, dataString)
452
486
  } else {
453
- logger.info("📡 No event handler, caching event: \(type)")
487
+ logger.info("📡 No event handler, caching event: \(type.rawValue)")
454
488
  self.cachedEvents.append((type, dataString))
455
489
  }
456
490
  } catch {
@@ -565,7 +599,7 @@ public class CallEngine {
565
599
  extension CallEngine: CallKitManagerDelegate {
566
600
  func callKitManager(_ manager: CallKitManager, didAnswerCall callId: String) {
567
601
  logger.info("📲 CallKit delegate: answer call \(callId)")
568
- answerCall(callId: callId)
602
+ coreCallAnswered(callId: callId, isLocalAnswer: true)
569
603
  }
570
604
 
571
605
  func callKitManager(_ manager: CallKitManager, didEndCall callId: String) {
@@ -586,6 +620,25 @@ extension CallEngine: CallKitManagerDelegate {
586
620
  logger.info("📲 CallKit delegate: set muted \(callId), muted: \(muted)")
587
621
  setMuted(callId: callId, muted: muted)
588
622
  }
623
+
624
+ func callKitManager(_ manager: CallKitManager, didStartOutgoingCall callId: String) {
625
+ logger.info("📲 CallKit delegate: did start outgoing call \(callId)")
626
+ if var callInfo = activeCalls[callId], callInfo.state == .dialing {
627
+ callInfo.updateState(.active)
628
+ activeCalls[callId] = callInfo
629
+ logger.info("📞 Call \(callId) transitioned from DIALING to ACTIVE as reported by CallKit")
630
+ }
631
+ }
632
+
633
+ func callKitManager(_ manager: CallKitManager, didActivateAudioSession session: AVAudioSession) {
634
+ logger.info("📲 CallKit delegate: did activate audio session")
635
+ audioManager?.activateAudioSession()
636
+ }
637
+
638
+ func callKitManager(_ manager: CallKitManager, didDeactivateAudioSession session: AVAudioSession) {
639
+ logger.info("📲 CallKit delegate: did deactivate audio session")
640
+ audioManager?.deactivateAudioSession()
641
+ }
589
642
  }
590
643
 
591
644
  extension CallEngine: AudioManagerDelegate {
@@ -606,4 +659,12 @@ extension CallEngine: AudioManagerDelegate {
606
659
  ]
607
660
  emitEvent(.audioDevicesChanged, data: eventData)
608
661
  }
662
+
663
+ func audioManagerDidActivateAudioSession(_ manager: AudioManager) {
664
+ logger.info("🔊 Audio manager delegate: audio session activated")
665
+ }
666
+
667
+ func audioManagerDidDeactivateAudioSession(_ manager: AudioManager) {
668
+ logger.info("🔊 Audio manager delegate: audio session deactivated")
669
+ }
609
670
  }
@@ -29,13 +29,13 @@ public struct CallInfo {
29
29
  mutating func updateState(_ newState: CallState) {
30
30
  let oldState = self.state
31
31
  self.state = newState
32
- Self.logger.info("📱 CallInfo state changed: callId=\(callId), \(oldState.stringValue) → \(newState.stringValue)")
32
+ Self.logger.info("📱 CallInfo state changed: c\\ \(oldState.stringValue) → \(newState.stringValue)")
33
33
  }
34
34
 
35
35
  mutating func updateDisplayName(_ newName: String) {
36
36
  let oldName = self.displayName
37
37
  self.displayName = newName
38
- Self.logger.info("📱 CallInfo display name updated: callId=\(callId), '\(oldName)' → '\(newName)'")
38
+ Self.logger.info("📱 CallInfo display name updated: ca, '\(oldName)' → '\(newName)'")
39
39
  }
40
40
 
41
41
  func toJSONObject() -> [String: Any] {