@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.
@@ -48,18 +48,48 @@ class AudioManager {
48
48
  }
49
49
 
50
50
  func configureAudioSession(forCallType isVideo: Bool,
51
- isIncoming: Bool) {
52
- logger.info("configureAudioSession: video=\(isVideo), incoming=\(isIncoming)")
53
- var opts: AVAudioSession.CategoryOptions = [.allowBluetooth, .allowBluetoothA2DP]
54
- if isVideo || isIncoming { opts.insert(.defaultToSpeaker) }
55
- do {
56
- try session.setCategory(.playAndRecord,
57
- mode: .voiceChat,
58
- options: opts)
59
- logger.info("audioSession category set")
60
- } catch {
61
- logger.error("setCategory failed: \(error.localizedDescription)")
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() {
@@ -169,63 +169,67 @@ class CallEngine {
169
169
 
170
170
  // MARK: Direct Active (Join Ongoing Call)
171
171
 
172
- func startCall(callId: String,
173
- callType: String,
174
- targetName: String,
175
- metadata: String? = nil)
176
- {
177
- logger.info("startCall (join ongoing): \(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
- // Create call directly in active state
205
- var info = CallInfo(callId: callId,
206
- callType: callType,
207
- displayName: targetName,
208
- pictureUrl: nil,
209
- state: .active)
210
- activeCalls[callId] = info
211
- currentCallId = callId
212
-
213
- audioManager.configureAudioSession(forCallType: callType == "Video",
214
- isIncoming: false)
215
-
216
- // Report to CallKit as already connected outgoing call (no ringing)
217
- callKitManager.reportConnectedOutgoingCall(callInfo: info) { [weak self] error in
218
- if let e = error {
219
- self?.logger.error("startCall reportConnected failed: \(e.localizedDescription)")
220
- self?.endCallInternal(callId: callId)
221
- return
222
- }
223
-
224
- self?.logger.info("startCall reported as connected - activating audio")
225
- self?.audioManager.activateAudioSession()
226
- self?.emitOutgoingCallAnsweredWithMetadata(callId: callId)
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
- logger.info("didSetHeld: \(callId), onHold=\(onHold)")
419
- guard var info = activeCalls[callId] else { return }
422
+ logger.info("didSetHeld: \(callId), onHold=\(onHold)")
423
+ guard var info = activeCalls[callId] else { return }
420
424
 
421
- if onHold {
422
- info.updateState(.held)
423
- info.wasHeldBySystem = true
424
- emitEvent(.callHeld, data: ["callId": callId])
425
- } else {
426
- info.updateState(.active)
427
- info.wasHeldBySystem = false
428
- currentCallId = callId
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
- // Re-configure audio when unheld
431
- audioManager.configureAudioSession(forCallType: info.callType == "Video",
432
- isIncoming: false)
434
+ // CRITICAL FIX: Force reconfigure audio session when unholding
435
+ audioManager.reconfigureAudioSessionForUnhold(forCallType: info.callType == "Video")
433
436
 
434
- emitEvent(.callUnheld, data: ["callId": callId])
435
- }
436
- activeCalls[callId] = info
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,
@@ -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
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qusaieilouti99/call-manager",
3
- "version": "0.1.129",
3
+ "version": "0.1.130",
4
4
  "description": "Call manager",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",