@@ -43,6 +43,7 @@ class CallManager: NSObject, ObservableObject {
4343 // State
4444 @Published var callState : CallState = . idle
4545 @Published var voipToken : String ?
46+ @Published var activeCallUUID : UUID ?
4647
4748 @AppStorage ( " url " ) var url : String = " "
4849 @AppStorage ( " token " ) var token : String = " "
@@ -68,8 +69,6 @@ class CallManager: NSObject, ObservableObject {
6869
6970 let room = Room ( )
7071
71- private var activeCallUUID : UUID ?
72-
7372 var hasActiveCall : Bool {
7473 switch callState {
7574 case . activeIncoming, . activeOutgoing, . connected:
@@ -86,9 +85,10 @@ class CallManager: NSObject, ObservableObject {
8685 pushRegistry. delegate = self
8786 _ = provider
8887
89- // Set auto-config off
88+ // Set audio session auto-config off
9089 AudioManager . shared. audioSession. isAutomaticConfigurationEnabled = false
9190
91+ // Set audio engine off
9292 do {
9393 try AudioManager . shared. setEngineAvailability ( . none)
9494 } catch {
@@ -98,9 +98,6 @@ class CallManager: NSObject, ObservableObject {
9898
9999 func startCall( handle: String ) async {
100100 let callUUID = UUID ( )
101- Task { @MainActor in
102- activeCallUUID = callUUID
103- }
104101
105102 let handle = CXHandle ( type: . generic, value: handle)
106103 let startCallAction = CXStartCallAction ( call: callUUID, handle: handle)
@@ -109,22 +106,31 @@ class CallManager: NSObject, ObservableObject {
109106 do {
110107 try await callController. request ( transaction)
111108 print ( " Started call " )
109+
110+ Task { @MainActor in
111+ activeCallUUID = callUUID
112+ }
112113 } catch {
113114 print ( " Failed to start call: \( error) " )
114115 }
115116 }
116117
117118 func endCall( ) async {
118- guard let callUUID = activeCallUUID else { return }
119-
120- let endCallAction = CXEndCallAction ( call: callUUID)
121- let transaction = CXTransaction ( action: endCallAction)
122-
123- do {
124- try await callController. request ( transaction)
125- print ( " Ended call " )
126- } catch {
127- print ( " Failed to end call: \( error) " )
119+ Task { @MainActor in
120+ // Read `activeCallUUID` on main thread
121+ guard let callUUID = activeCallUUID else { return }
122+
123+ Task {
124+ let endCallAction = CXEndCallAction ( call: callUUID)
125+ let transaction = CXTransaction ( action: endCallAction)
126+
127+ do {
128+ try await callController. request ( transaction)
129+ print ( " Ended call " )
130+ } catch {
131+ print ( " Failed to end call: \( error) " )
132+ }
133+ }
128134 }
129135 }
130136
@@ -143,9 +149,9 @@ class CallManager: NSObject, ObservableObject {
143149
144150 // NOTE: Using sync version for background mode compatibility
145151 func reportIncomingCallSync( from callerId: String , callerName: String , completion: @escaping @Sendable ( ( any Error ) ? ) -> Void ) {
146- let callUUID = UUID ( )
147- activeCallUUID = callUUID
152+ logger. debug ( " Incoming call " )
148153
154+ let callUUID = UUID ( )
149155 let callUpdate = CXCallUpdate ( )
150156 callUpdate. remoteHandle = CXHandle ( type: . generic, value: callerId)
151157 callUpdate. hasVideo = false
@@ -155,6 +161,7 @@ class CallManager: NSObject, ObservableObject {
155161
156162 Task { @MainActor in
157163 self . callState = . activeIncoming
164+ self . activeCallUUID = callUUID
158165 }
159166 }
160167}
@@ -178,16 +185,16 @@ extension CallManager {
178185
179186extension CallManager : CXProviderDelegate {
180187 func providerDidReset( _: CXProvider ) {
181- logger. debug ( " CXProvider did reset" )
188+ logger. debug ( " Did reset" )
182189
183190 Task { @MainActor in
191+ self . activeCallUUID = nil
184192 self . callState = . idle
185193 }
186- activeCallUUID = nil
187194 }
188195
189196 func provider( _ provider: CXProvider , perform action: CXStartCallAction ) {
190- logger. debug ( " CXProvider call start " )
197+ logger. debug ( " Call starting... " )
191198
192199 Task { @MainActor in
193200 self . callState = . activeOutgoing
@@ -206,74 +213,94 @@ extension CallManager: CXProviderDelegate {
206213 action. fulfill ( )
207214 } catch {
208215 logger. critical ( " Failed to connect to room with error: \( error) " )
216+ action. fail ( )
209217
210218 Task { @MainActor in
211219 self . callState = . errored( error)
220+ self . activeCallUUID = nil
212221 }
213-
214- action. fulfill ( )
215222 }
216223 }
217224 }
218225
219226 func provider( _: CXProvider , perform action: CXAnswerCallAction ) {
220- logger. debug ( " CXProvider answer call " )
221-
222- Task { @MainActor in
223- self . callState = . connected
224- }
227+ logger. debug ( " Answer call " )
225228
226229 Task {
227230 do {
231+ try await connectToRoom ( )
228232 logger. debug ( " Connected to room " )
233+
234+ Task { @MainActor in
235+ self . callState = . connected
236+ }
237+
229238 action. fulfill ( )
230- try await connectToRoom ( )
231239 } catch {
232240 logger. critical ( " Failed to connect to room with error: \( error) " )
233- action. fulfill ( )
241+
242+ Task { @MainActor in
243+ self . callState = . errored( error)
244+ }
245+
246+ action. fail ( )
234247 }
235248 }
236249 }
237250
238251 func provider( _: CXProvider , perform action: CXEndCallAction ) {
239- logger. debug ( " CXProvider end call " )
240-
241- Task { @MainActor in
242- self . activeCallUUID = nil
243- self . callState = . idle
244- }
252+ logger. debug ( " End call " )
245253
246254 Task {
247255 await disconnectFromRoom ( )
248256 action. fulfill ( )
257+
258+ Task { @MainActor in
259+ self . activeCallUUID = nil
260+
261+ if case . errored = self . callState {
262+ // Keep the errored state, for failed incoming cases.
263+ } else {
264+ self . callState = . idle
265+ }
266+ }
249267 }
250268 }
251269
252270 func provider( _: CXProvider , perform action: CXSetMutedCallAction ) {
253- logger. debug ( " CXProvider muted call " )
254-
255- // TODO: Handle mute
256- action. fulfill ( )
271+ Task {
272+ do {
273+ try await room. localParticipant. setMicrophone ( enabled: !action. isMuted)
274+ logger. debug ( " Muted call: \( action. isMuted) " )
275+ action. fulfill ( )
276+ } catch {
277+ logger. critical ( " Failed to set microphone enabled: \( !action. isMuted) with error: \( error. localizedDescription) " )
278+ action. fail ( )
279+ }
280+ }
257281 }
258282
259- func provider( _: CXProvider , didActivate _ : AVAudioSession ) {
283+ func provider( _: CXProvider , didActivate session : AVAudioSession ) {
260284 // Audio session can now be activated.
261- logger. debug ( " CXProvider did activate session" )
285+ logger. debug ( " Did activate session" )
262286
263287 do {
264- let session = AVAudioSession . sharedInstance ( )
265288 try session. setCategory ( . playAndRecord, mode: . voiceChat, options: [ . mixWithOthers] )
266-
267289 try AudioManager . shared. setEngineAvailability ( . default)
268-
269290 } catch {
270- print ( " Failed to set engine permissions : \( error. localizedDescription) " )
291+ logger . critical ( " Failed to set engine availability : \( error. localizedDescription) " )
271292 }
272293 }
273294
274295 func provider( _: CXProvider , didDeactivate _: AVAudioSession ) {
275296 // Audio session will need to be deactivated.
276- logger. debug ( " CXProvider did deactivate session " )
297+ logger. debug ( " Did deactivate session " )
298+
299+ do {
300+ try AudioManager . shared. setEngineAvailability ( . none)
301+ } catch {
302+ logger. critical ( " Failed to set engine availability: \( error. localizedDescription) " )
303+ }
277304 }
278305}
279306
@@ -283,7 +310,8 @@ extension CallManager: PKPushRegistryDelegate {
283310 func pushRegistry( _: PKPushRegistry , didUpdate pushCredentials: PKPushCredentials , for type: PKPushType ) {
284311 guard type == . voIP else { return }
285312 let token = pushCredentials. token. map { String ( format: " %02x " , $0) } . joined ( )
286- logger. info ( " token: \( token) " )
313+
314+ logger. info ( " Push token updated: \( token) " )
287315
288316 Task { @MainActor in
289317 self . voipToken = token
@@ -292,7 +320,7 @@ extension CallManager: PKPushRegistryDelegate {
292320
293321 func pushRegistry( _: PKPushRegistry , didInvalidatePushTokenFor type: PKPushType ) {
294322 guard type == . voIP else { return }
295- logger. info ( " VoIP Push Token invalidated" )
323+ logger. info ( " Push Token invalidated " )
296324
297325 Task { @MainActor in
298326 voipToken = nil
@@ -305,7 +333,17 @@ extension CallManager: PKPushRegistryDelegate {
305333 return
306334 }
307335
308- logger. info ( " Received VoIP push with payload: \( payload. dictionaryPayload) " )
336+ logger. info ( " Received push with payload: \( payload. dictionaryPayload) " )
337+
338+ /// NOTE: I've observed that the mic cannot initialized if we don't set .playAndRecord here,
339+ /// which appears like a bug in my opinion, since ``CallManager/provider(_:didActivate:)``
340+ /// should be invoked at the right time instead of having to set the session category here.
341+ do {
342+ let session = AVAudioSession . sharedInstance ( )
343+ try session. setCategory ( . playAndRecord, mode: . voiceChat, options: [ . mixWithOthers] )
344+ } catch {
345+ print ( " Failed to configure AVAudioSession: \( error. localizedDescription) " )
346+ }
309347
310348 // Extract caller information from payload
311349 let callerId = payload. dictionaryPayload [ " callerId " ] as? String ?? UUID ( ) . uuidString
0 commit comments