@capgo/capacitor-stream-call 7.1.31 → 7.7.7

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.
@@ -12,6 +12,7 @@ import WebKit
12
12
  */
13
13
  @objc(StreamCallPlugin)
14
14
  public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
15
+ private let pluginVersion: String = "7.7.7"
15
16
  public let identifier = "StreamCallPlugin"
16
17
  public let jsName = "StreamCall"
17
18
  public let pluginMethods: [CAPPluginMethod] = [
@@ -19,6 +20,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
19
20
  CAPPluginMethod(name: "login", returnType: CAPPluginReturnPromise),
20
21
  CAPPluginMethod(name: "logout", returnType: CAPPluginReturnPromise),
21
22
  CAPPluginMethod(name: "call", returnType: CAPPluginReturnPromise),
23
+ CAPPluginMethod(name: "joinCall", returnType: CAPPluginReturnPromise),
24
+ CAPPluginMethod(name: "toggleViews", returnType: CAPPluginReturnPromise),
22
25
  CAPPluginMethod(name: "endCall", returnType: CAPPluginReturnPromise),
23
26
  CAPPluginMethod(name: "setMicrophoneEnabled", returnType: CAPPluginReturnPromise),
24
27
  CAPPluginMethod(name: "setCameraEnabled", returnType: CAPPluginReturnPromise),
@@ -30,7 +33,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
30
33
  CAPPluginMethod(name: "getCallInfo", returnType: CAPPluginReturnPromise),
31
34
  CAPPluginMethod(name: "setDynamicStreamVideoApikey", returnType: CAPPluginReturnPromise),
32
35
  CAPPluginMethod(name: "getDynamicStreamVideoApikey", returnType: CAPPluginReturnPromise),
33
- CAPPluginMethod(name: "getCurrentUser", returnType: CAPPluginReturnPromise)
36
+ CAPPluginMethod(name: "getCurrentUser", returnType: CAPPluginReturnPromise),
37
+ CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise)
34
38
  ]
35
39
 
36
40
  private enum State {
@@ -47,6 +51,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
47
51
  private var hostingController: UIHostingController<CallOverlayView>?
48
52
  private var tokenSubscription: AnyCancellable?
49
53
  private var activeCallSubscription: AnyCancellable?
54
+ private var speakerSubscription: AnyCancellable?
50
55
  private var lastVoIPToken: String?
51
56
  private var touchInterceptView: TouchInterceptView?
52
57
  private var needsTouchInterceptorSetup: Bool = false
@@ -59,9 +64,12 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
59
64
  private var currentCallId: String = ""
60
65
  private var currentCallState: String = ""
61
66
  private var hasNotifiedCallJoined: Bool = false
67
+ private var previousCallingState: CallingState?
62
68
 
63
69
  @Injected(\.callKitAdapter) var callKitAdapter
64
70
  @Injected(\.callKitPushNotificationAdapter) var callKitPushNotificationAdapter
71
+ @Injected(\.utils) var utils
72
+
65
73
  private var webviewDelegate: WebviewNavigationDelegate?
66
74
 
67
75
  // Declare as optional and initialize in load() method
@@ -188,9 +196,11 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
188
196
  DispatchQueue.main.async { [weak self] in
189
197
  guard let self = self else { return }
190
198
 
191
- // Cancel existing subscription if any
199
+ // Cancel existing subscriptions if any
192
200
  self.activeCallSubscription?.cancel()
193
201
  self.activeCallSubscription = nil
202
+ self.speakerSubscription?.cancel()
203
+ self.speakerSubscription = nil
194
204
 
195
205
  // Verify callViewModel exists
196
206
  guard let callViewModel = self.callViewModel, let streamVideo = self.streamVideo else {
@@ -208,6 +218,52 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
208
218
  // while the subscription is active
209
219
  let viewModel = callViewModel
210
220
 
221
+ let callEvents = streamVideo
222
+ .eventPublisher()
223
+ .sink { [weak self] event in
224
+ guard let self else { return }
225
+ switch event {
226
+ case let .typeCallRejectedEvent(response):
227
+ let data: [String: Any] = [
228
+ "callId": response.callCid,
229
+ "state": "rejected"
230
+ ]
231
+ notifyListeners("callEvent", data: data)
232
+ case let .typeCallEndedEvent(response):
233
+ let data: [String: Any] = [
234
+ "callId": response.callCid,
235
+ "state": "left"
236
+ ]
237
+ notifyListeners("callEvent", data: data)
238
+
239
+ case let .typeCallSessionParticipantCountsUpdatedEvent(response):
240
+ let activeCall = streamVideo.state.activeCall
241
+ let callDropped = self.currentCallId == response.callCid && activeCall == nil
242
+ let onlyOneParticipant = activeCall?.cId == response.callCid && activeCall?.state.participantCount == 1
243
+ if onlyOneParticipant || callDropped {
244
+ self.endCallInternal()
245
+ } else {
246
+ print("""
247
+ onlyOneParticipant check:
248
+ - activeCall?.cId: \(String(describing: activeCall?.cId))
249
+ - response.callCid: \(response.callCid)
250
+ - activeCall?.state.participantCount: \(String(describing: activeCall?.state.participantCount))
251
+ - Result (onlyOneParticipant): \(onlyOneParticipant)
252
+ """)
253
+ }
254
+ if let count = activeCall?.state.participantCount {
255
+ let data: [String: Any] = [
256
+ "callId": response.callCid,
257
+ "state": "participant_counts",
258
+ "count": count
259
+ ]
260
+ notifyListeners("callEvent", data: data)
261
+ }
262
+ default:
263
+ break
264
+ }
265
+ }
266
+
211
267
  // Subscribe to streamVideo.state.$activeCall to handle CallKit integration
212
268
  let callPublisher = streamVideo.state.$activeCall
213
269
  .receive(on: DispatchQueue.main)
@@ -220,6 +276,58 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
220
276
  // Sync callViewModel with activeCall from streamVideo state
221
277
  // This ensures CallKit integration works properly
222
278
  viewModel.setActiveCall(activeCall)
279
+ viewModel.update(participantsLayout: .grid)
280
+
281
+ if let callId = viewModel.call?.cId, !self.hasNotifiedCallJoined || callId != self.currentCallId {
282
+ print("Notifying call joined: \(callId)")
283
+ self.updateCallStatusAndNotify(callId: callId, state: "joined")
284
+ self.hasNotifiedCallJoined = true
285
+ }
286
+
287
+ // Subscribe to speaker status for this active call
288
+ self.speakerSubscription = activeCall.speaker.$status
289
+ .receive(on: DispatchQueue.main)
290
+ .sink { [weak self] speakerStatus in
291
+ guard let self = self else { return }
292
+
293
+ // Only emit if the current active call matches our current call ID
294
+ guard activeCall.cId == self.currentCallId else {
295
+ return
296
+ }
297
+
298
+ print("Speaker status update: \(speakerStatus)")
299
+
300
+ let state: String
301
+ switch speakerStatus {
302
+ case .enabled:
303
+ state = "speaker_enabled"
304
+ case .disabled:
305
+ state = "speaker_disabled"
306
+ }
307
+
308
+ let data: [String: Any] = [
309
+ "callId": self.currentCallId,
310
+ "state": state
311
+ ]
312
+
313
+ self.notifyListeners("callEvent", data: data)
314
+ }
315
+ } else {
316
+ // Clean up speaker subscription when activeCall becomes nil
317
+ print("Active call became nil, cleaning up speaker subscription")
318
+ self.speakerSubscription?.cancel()
319
+ self.speakerSubscription = nil
320
+
321
+ print("Call actually ending: \(self.currentCallId)")
322
+
323
+ // Notify that call has ended - use the stored call ID
324
+ self.updateCallStatusAndNotify(callId: self.currentCallId, state: "left")
325
+
326
+ // Reset notification flag when call ends
327
+ self.hasNotifiedCallJoined = false
328
+
329
+ // Remove the call overlay view and touch intercept view when not in a call
330
+ self.ensureViewRemoved()
223
331
  }
224
332
  }
225
333
 
@@ -238,6 +346,53 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
238
346
  do {
239
347
  try self.requireInitialized()
240
348
  print("Call State Update: \(newState)")
349
+ // Check if transitioning from outgoing to idle
350
+ if newState == .idle,
351
+ let previousState = self.previousCallingState,
352
+ case .outgoing = previousState {
353
+ print("Call state changed from outgoing to idle. Ending call with ID: \(self.currentCallId)")
354
+
355
+ // Stop ongoing sound when call is cancelled
356
+ self.utils.callSoundsPlayer.stopOngoingSound()
357
+
358
+ // End the call using the stored call ID and type
359
+ if !self.currentCallId.isEmpty {
360
+ Task {
361
+ do {
362
+ // Parse call type and id from currentCallId format "type:id"
363
+ let components = self.currentCallId.components(separatedBy: ":")
364
+ if components.count >= 2 {
365
+ let callType = components[0]
366
+ let callId = components[1]
367
+
368
+ // Try to get the call and end it using the parsed call type
369
+ if let streamVideo = self.streamVideo {
370
+ let call = streamVideo.call(callType: callType, callId: callId)
371
+ try await call.end()
372
+ print("Successfully ended outgoing call: \(callId) with type: \(callType)")
373
+
374
+ let data: [String: Any] = [
375
+ "callId": call.cId,
376
+ "state": "outgoing_call_ended"
377
+ ]
378
+
379
+ self.notifyListeners("callEvent", data: data)
380
+ }
381
+ } else {
382
+ print("Invalid call ID format: \(self.currentCallId)")
383
+ }
384
+ } catch {
385
+ print("Error ending outgoing call \(self.currentCallId): \(error)")
386
+ }
387
+
388
+ // Clean up stored call information
389
+ DispatchQueue.main.async {
390
+ self.currentCallId = ""
391
+ print("Cleaned up call information after ending outgoing call")
392
+ }
393
+ }
394
+ }
395
+ }
241
396
 
242
397
  if newState == .inCall {
243
398
  print("- In call state detected")
@@ -249,85 +404,32 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
249
404
  // Create/update overlay and make visible when there's an active call
250
405
  self.createCallOverlayView()
251
406
 
407
+ // Stop ongoing sound when call is joined
408
+ self.utils.callSoundsPlayer.stopOngoingSound()
409
+
252
410
  // Notify that a call has started - but only if we haven't notified for this call yet
253
411
  if let callId = viewModel.call?.cId, !self.hasNotifiedCallJoined || callId != self.currentCallId {
254
412
  print("Notifying call joined: \(callId)")
255
413
  self.updateCallStatusAndNotify(callId: callId, state: "joined")
256
414
  self.hasNotifiedCallJoined = true
257
415
  }
258
- } else if case .incoming(let incomingCall) = newState {
259
- // Extract caller information
260
- Task {
261
- var caller: [String: Any]?
262
- var members: [[String: Any]]?
263
-
264
- do {
265
- // Get the call from StreamVideo to access detailed information
266
- if let streamVideo = self.streamVideo {
267
- let call = streamVideo.call(callType: incomingCall.type, callId: incomingCall.id)
268
- let callInfo = try await call.get()
269
-
270
- // Extract caller information
271
- let createdBy = callInfo.call.createdBy
272
- var callerData: [String: Any] = [:]
273
- callerData["userId"] = createdBy.id
274
- callerData["name"] = createdBy.name
275
- callerData["imageURL"] = createdBy.image
276
- callerData["role"] = createdBy.role
277
- caller = callerData
278
-
279
- // Extract members information from current participants if available
280
- var membersArray: [[String: Any]] = []
281
- if let activeCall = streamVideo.state.activeCall {
282
- let participants = activeCall.state.participants
283
- for participant in participants {
284
- var memberData: [String: Any] = [:]
285
- memberData["userId"] = participant.userId
286
- memberData["name"] = participant.name
287
- memberData["imageURL"] = participant.profileImageURL?.absoluteString ?? ""
288
- memberData["role"] = participant.roles.first ?? ""
289
- membersArray.append(memberData)
290
- }
291
- }
292
- members = membersArray.isEmpty ? nil : membersArray
293
- }
294
- } catch {
295
- print("Failed to get call info for caller details: \(error)")
296
- }
297
-
298
- // Notify with caller information
299
- let fullCallId = "\(incomingCall.type):\(incomingCall.id)"
300
- self.updateCallStatusAndNotify(callId: fullCallId, state: "ringing", caller: caller, members: members)
301
- }
302
- } else if newState == .idle {
303
- print("Call state changed to idle. CurrentCallId: \(self.currentCallId), ActiveCall: \(String(describing: self.streamVideo?.state.activeCall?.cId))")
304
-
305
- // Only notify about call ending if we have a valid stored call ID and there's truly no active call
306
- // This prevents false "left" events during normal state transitions
307
- if !self.currentCallId.isEmpty && self.streamVideo?.state.activeCall == nil {
308
- print("Call actually ending: \(self.currentCallId)")
309
-
310
- // Notify that call has ended - use the stored call ID
311
- self.updateCallStatusAndNotify(callId: self.currentCallId, state: "left")
312
416
 
313
- // Reset notification flag when call ends
314
- self.hasNotifiedCallJoined = false
315
-
316
- // Remove the call overlay view when not in a call
317
- self.ensureViewRemoved()
318
- } else {
319
- print("Not sending left event - CurrentCallId: \(self.currentCallId), ActiveCall exists: \(self.streamVideo?.state.activeCall != nil)")
320
- }
417
+ } else if case .incoming(let incomingCall) = newState {
418
+ self.updateCallStatusAndNotify(callId: incomingCall.id, state: "ringing")
419
+ // }
321
420
  }
421
+ // Update the previous state for next comparison
422
+ self.previousCallingState = newState
322
423
  } catch {
323
424
  log.error("Error handling call state update: \(String(describing: error))")
324
425
  }
325
426
  }
326
427
 
327
- // Combine both publishers
428
+ // Combine all publishers
328
429
  self.activeCallSubscription = AnyCancellable {
329
430
  callPublisher.cancel()
330
431
  statePublisher.cancel()
432
+ callEvents.cancel()
331
433
  }
332
434
 
333
435
  print("Active call subscription setup completed")
@@ -421,9 +523,20 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
421
523
  tokenSubscription = nil
422
524
  activeCallSubscription?.cancel()
423
525
  activeCallSubscription = nil
526
+ speakerSubscription?.cancel()
527
+ speakerSubscription = nil
424
528
  lastVoIPToken = nil
529
+ state = .notInitialized
425
530
 
426
531
  SecureUserRepository.shared.removeCurrentUser()
532
+ self.callViewModel = nil
533
+ self.overlayView = nil
534
+
535
+ // Clean up touch intercept view
536
+ if let touchInterceptView = self.touchInterceptView {
537
+ touchInterceptView.removeFromSuperview()
538
+ self.touchInterceptView = nil
539
+ }
427
540
 
428
541
  // Update the CallOverlayView with nil StreamVideo instance
429
542
  Task { @MainActor in
@@ -457,6 +570,85 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
457
570
  }
458
571
  }
459
572
 
573
+ @objc func toggleViews(_ call: CAPPluginCall) {
574
+ Task { @MainActor in
575
+ guard let viewModel = self.callViewModel else {
576
+ call.reject("ViewModel is not initialized.")
577
+ return
578
+ }
579
+
580
+ let layouts: [ParticipantsLayout] = [.spotlight, .fullScreen, .grid]
581
+ let currentLayout = viewModel.participantsLayout
582
+
583
+ // If currentLayout is not in layouts, default to .grid index
584
+ let currentIndex = layouts.firstIndex(of: currentLayout) ?? layouts.firstIndex(of: .grid) ?? 0
585
+ let nextIndex = (currentIndex + 1) % layouts.count
586
+ let nextLayout = layouts[nextIndex]
587
+
588
+ viewModel.update(participantsLayout: nextLayout)
589
+
590
+ call.resolve([
591
+ "newLayout": "\(nextLayout)"
592
+ ])
593
+ }
594
+ }
595
+
596
+ @objc func joinCall(_ call: CAPPluginCall) {
597
+ guard let callId = call.getString("callId") else {
598
+ call.reject("Missing required parameter: callId")
599
+ return
600
+ }
601
+
602
+ guard let callType = call.getString("callType") else {
603
+ call.reject("Missing required parameter: callType")
604
+ return
605
+ }
606
+
607
+ // Initialize if needed
608
+ if state == .notInitialized {
609
+ initializeStreamVideo()
610
+ if state != .initialized {
611
+ call.reject("Failed to initialize StreamVideo")
612
+ return
613
+ }
614
+ }
615
+
616
+ do {
617
+ try requireInitialized()
618
+
619
+ Task {
620
+ do {
621
+ print("Joining call:")
622
+ print("- Call ID: \(callId)")
623
+ print("- Call Type: \(callType)")
624
+
625
+ // Create the call object
626
+ await self.callViewModel?.joinCall(
627
+ callType: callType,
628
+ callId: callId
629
+ )
630
+
631
+ // Now send the created event with complete member data
632
+ self.updateCallStatusAndNotify(callId: callId, state: "joined")
633
+
634
+ // Update UI on main thread
635
+ await MainActor.run {
636
+ // self.overlayViewModel?.updateCall(streamCall)
637
+ self.overlayView?.isHidden = false
638
+ self.webView?.isOpaque = false
639
+ }
640
+
641
+ call.resolve([
642
+ "success": true
643
+ ])
644
+
645
+ }
646
+ }
647
+ } catch {
648
+ call.reject("StreamVideo not initialized")
649
+ }
650
+ }
651
+
460
652
  @objc func call(_ call: CAPPluginCall) {
461
653
  guard let members = call.getArray("userIds", String.self) else {
462
654
  call.reject("Missing required parameter: userIds (array of user IDs)")
@@ -497,6 +689,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
497
689
  print("- Users: \(members)")
498
690
  print("- Should Ring: \(shouldRing)")
499
691
  print("- Team: \(String(describing: team))")
692
+ // Play outgoing call sound
693
+ self.utils.callSoundsPlayer.playOutgoingCallSound()
500
694
 
501
695
  // Create the call object
502
696
  await self.callViewModel?.startCall(
@@ -625,7 +819,97 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
625
819
  }
626
820
  }
627
821
 
822
+ private func endCallInternal() {
823
+ // Stop ongoing sound when call ends
824
+ self.utils.callSoundsPlayer.stopOngoingSound()
825
+
826
+ do {
827
+ try requireInitialized()
828
+
829
+ Task {
830
+ // Check both active call and callViewModel's call state to handle outgoing calls
831
+ let activeCall = streamVideo?.state.activeCall
832
+ let viewModelCall = await callViewModel?.call
833
+
834
+ // Helper function to determine if we should end or leave the call
835
+ func shouldEndCall(for streamCall: Call) async throws -> Bool {
836
+ do {
837
+ let callInfo = try await streamCall.get()
838
+ let currentUserId = streamVideo?.user.id
839
+ let createdBy = callInfo.call.createdBy.id
840
+ let isCreator = createdBy == currentUserId
841
+
842
+ // Use call.state.participants.count to get participant count (as per StreamVideo iOS SDK docs)
843
+ // let totalParticipants = await streamCall.state.participants.count
844
+ let forceEnd = await (activeCall?.state.custom["type"]?.stringValue == "direct")
845
+ let shouldEnd = isCreator || forceEnd
846
+
847
+ print("Call \(streamCall.cId) - Creator: \(createdBy), CurrentUser: \(currentUserId ?? "nil"), IsCreator: \(isCreator), ShouldEnd: \(shouldEnd)")
848
+
849
+ return shouldEnd
850
+ } catch {
851
+ print("Error getting call info for \(streamCall.cId), defaulting to leave: \(error)")
852
+ return false // Fallback to leave if we can't determine
853
+ }
854
+ }
855
+
856
+ if let activeCall = activeCall {
857
+ // There's an active call, check if we should end or leave
858
+ do {
859
+ let shouldEnd = try await shouldEndCall(for: activeCall)
860
+
861
+ if shouldEnd {
862
+ print("Ending active call \(activeCall.cId) for all participants")
863
+ try await activeCall.end()
864
+ } else {
865
+ print("Leaving active call \(activeCall.cId)")
866
+ try await activeCall.leave()
867
+ }
868
+ } catch {
869
+ print("Error ending/leaving active call: \(error)")
870
+ try await activeCall.leave() // Fallback to leave
871
+ }
872
+
873
+ await MainActor.run {
874
+ self.overlayView?.isHidden = true
875
+ self.webView?.isOpaque = true
876
+ }
877
+
878
+ } else if let viewModelCall = viewModelCall {
879
+ // There's a call in the viewModel (likely outgoing/ringing), check if we should end or leave
880
+ do {
881
+ let shouldEnd = try await shouldEndCall(for: viewModelCall)
882
+
883
+ if shouldEnd {
884
+ print("Ending viewModel call \(viewModelCall.cId) for all participants")
885
+ try await viewModelCall.end()
886
+ } else {
887
+ print("Leaving viewModel call \(viewModelCall.cId)")
888
+ try await viewModelCall.leave()
889
+ }
890
+ } catch {
891
+ print("Error ending/leaving viewModel call: \(error)")
892
+ try await viewModelCall.leave() // Fallback to leave
893
+ }
894
+
895
+ // Also hang up to reset the calling state
896
+ await callViewModel?.hangUp()
897
+
898
+ await MainActor.run {
899
+ self.overlayView?.isHidden = true
900
+ self.webView?.isOpaque = true
901
+ }
902
+ }
903
+ }
904
+ } catch {
905
+ log.error("StreamVideo not initialized")
906
+ }
907
+ }
908
+
628
909
  @objc func endCall(_ call: CAPPluginCall) {
910
+ // Stop ongoing sound when call ends
911
+ self.utils.callSoundsPlayer.stopOngoingSound()
912
+
629
913
  do {
630
914
  try requireInitialized()
631
915
 
@@ -641,10 +925,11 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
641
925
  let currentUserId = streamVideo?.user.id
642
926
  let createdBy = callInfo.call.createdBy.id
643
927
  let isCreator = createdBy == currentUserId
928
+ let forceEnd = await (activeCall?.state.custom["type"]?.stringValue == "direct")
644
929
 
645
930
  // Use call.state.participants.count to get participant count (as per StreamVideo iOS SDK docs)
646
931
  let totalParticipants = await streamCall.state.participants.count
647
- let shouldEnd = isCreator || totalParticipants <= 2
932
+ let shouldEnd = isCreator || forceEnd
648
933
 
649
934
  print("Call \(streamCall.cId) - Creator: \(createdBy), CurrentUser: \(currentUserId ?? "nil"), IsCreator: \(isCreator), TotalParticipants: \(totalParticipants), ShouldEnd: \(shouldEnd)")
650
935
 
@@ -735,10 +1020,10 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
735
1020
  Task {
736
1021
  do {
737
1022
  if let activeCall = streamVideo?.state.activeCall {
738
- if enabled {
739
- try await activeCall.microphone.enable()
740
- } else {
741
- try await activeCall.microphone.disable()
1023
+ if enabled && activeCall.microphone.status == .disabled {
1024
+ try await activeCall.microphone.toggle()
1025
+ } else if !enabled && activeCall.microphone.status == .enabled {
1026
+ try await activeCall.microphone.toggle()
742
1027
  }
743
1028
  call.resolve([
744
1029
  "success": true
@@ -768,10 +1053,10 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
768
1053
  Task {
769
1054
  do {
770
1055
  if let activeCall = streamVideo?.state.activeCall {
771
- if enabled {
772
- try await activeCall.camera.enable()
773
- } else {
774
- try await activeCall.camera.disable()
1056
+ if enabled && activeCall.camera.status == .disabled {
1057
+ try await activeCall.camera.toggle()
1058
+ } else if !enabled && activeCall.camera.status == .enabled {
1059
+ try await activeCall.camera.toggle()
775
1060
  }
776
1061
  call.resolve([
777
1062
  "success": true
@@ -896,7 +1181,6 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
896
1181
  Task { @MainActor in
897
1182
  self.callViewModel = CallViewModel(participantsLayout: .grid)
898
1183
  // self.callViewModel?.participantAutoLeavePolicy = LastParticipantAutoLeavePolicy()
899
-
900
1184
  // Setup subscriptions for new StreamVideo instance
901
1185
  self.setupActiveCallSubscription()
902
1186
  }
@@ -905,7 +1189,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
905
1189
 
906
1190
  state = .initialized
907
1191
  callKitAdapter.streamVideo = self.streamVideo
908
- callKitAdapter.availabilityPolicy = .always
1192
+ callKitAdapter.availabilityPolicy = .regionBased
909
1193
 
910
1194
  setupTokenSubscription()
911
1195
 
@@ -1035,6 +1319,9 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1035
1319
  webView.isOpaque = false
1036
1320
  webView.backgroundColor = .clear
1037
1321
  webView.scrollView.backgroundColor = .clear
1322
+
1323
+ // Add touch intercept view when making existing overlay visible
1324
+ addTouchInterceptView(webView: webView, parent: parent, overlayView: existingOverlayView)
1038
1325
  return
1039
1326
  }
1040
1327
 
@@ -1067,8 +1354,38 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1067
1354
  self.hostingController = overlayView
1068
1355
  self.overlayView = overlayView.view
1069
1356
 
1070
- // Add touch interceptor if not already present
1071
- self.addTouchInterceptor()
1357
+ // Add touch intercept view for the new overlay
1358
+ addTouchInterceptView(webView: webView, parent: parent, overlayView: overlayView.view)
1359
+ }
1360
+
1361
+ private func addTouchInterceptView(webView: WKWebView, parent: UIView, overlayView: UIView) {
1362
+ // Create touch intercept view
1363
+ let touchInterceptView = TouchInterceptView(frame: parent.bounds)
1364
+ touchInterceptView.translatesAutoresizingMaskIntoConstraints = false
1365
+ touchInterceptView.backgroundColor = .clear
1366
+ touchInterceptView.isOpaque = false
1367
+ touchInterceptView.setupWithWebView(webView, overlayView: overlayView)
1368
+
1369
+ // Add to parent view
1370
+ parent.addSubview(touchInterceptView)
1371
+
1372
+ // Set up active call check function
1373
+ touchInterceptView.setActiveCallCheck { [weak self] in
1374
+ return self?.streamVideo?.state.activeCall != nil
1375
+ }
1376
+
1377
+ // Store reference and set call active
1378
+ self.touchInterceptView = touchInterceptView
1379
+
1380
+ // Setup constraints for touchInterceptView to cover the entire parent
1381
+ NSLayoutConstraint.activate([
1382
+ touchInterceptView.topAnchor.constraint(equalTo: parent.topAnchor),
1383
+ touchInterceptView.bottomAnchor.constraint(equalTo: parent.bottomAnchor),
1384
+ touchInterceptView.leadingAnchor.constraint(equalTo: parent.leadingAnchor),
1385
+ touchInterceptView.trailingAnchor.constraint(equalTo: parent.trailingAnchor)
1386
+ ])
1387
+
1388
+ print("Touch intercept view added and activated for call")
1072
1389
  }
1073
1390
 
1074
1391
  // MARK: - Dynamic API Key Management
@@ -1094,6 +1411,15 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1094
1411
  }
1095
1412
 
1096
1413
  func ensureViewRemoved() {
1414
+ print("Removing call views and touch intercept")
1415
+
1416
+ // Remove touch intercept view
1417
+ if let touchInterceptView = self.touchInterceptView {
1418
+ print("Removing touch intercept view")
1419
+ touchInterceptView.removeFromSuperview()
1420
+ self.touchInterceptView = nil
1421
+ }
1422
+
1097
1423
  // Check if we have an overlay view
1098
1424
  if let existingOverlayView = self.overlayView {
1099
1425
  print("Hiding call overlay view")
@@ -1120,93 +1446,52 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1120
1446
  return
1121
1447
  }
1122
1448
 
1123
- call.resolve([
1449
+ let result: [String: Any] = [
1124
1450
  "callId": currentCallId,
1125
1451
  "state": currentCallState
1126
- ])
1127
- }
1452
+ ]
1128
1453
 
1129
- @objc func getCallInfo(_ call: CAPPluginCall) {
1130
- guard let callId = call.getString("callId") else {
1131
- call.reject("Missing required parameter: callId")
1132
- return
1133
- }
1454
+ call.resolve(result)
1455
+ }
1134
1456
 
1457
+ func getCallInfo(callId: String, activeCall: Call) async -> [String: Any] {
1135
1458
  do {
1136
- try requireInitialized()
1137
-
1138
- guard let activeCall = streamVideo?.state.activeCall, activeCall.cId == callId else {
1139
- call.reject("Call ID does not match active call")
1140
- return
1141
- }
1459
+ let customRaw = await MainActor.run { activeCall.state.custom }
1142
1460
 
1143
- Task {
1144
- do {
1145
- // Get detailed call information
1146
- let callInfo = try await activeCall.get()
1147
-
1148
- // Extract caller information
1149
- var caller: [String: Any]?
1150
- let createdBy = callInfo.call.createdBy
1151
- var callerData: [String: Any] = [:]
1152
- callerData["userId"] = createdBy.id
1153
- callerData["name"] = createdBy.name
1154
- callerData["imageURL"] = createdBy.image
1155
- callerData["role"] = createdBy.role
1156
- caller = callerData
1157
-
1158
- // Extract members information
1159
- var membersArray: [[String: Any]] = []
1160
- let participants = await activeCall.state.participants
1161
- for participant in participants {
1162
- var memberData: [String: Any] = [:]
1163
- memberData["userId"] = participant.userId
1164
- memberData["name"] = participant.name
1165
- memberData["imageURL"] = participant.profileImageURL
1166
- memberData["role"] = participant.roles.first ?? ""
1167
- membersArray.append(memberData)
1168
- }
1169
- let members = membersArray
1170
-
1171
- // Determine call state based on current calling state
1172
- let state: String
1173
- let callingState = await self.callViewModel?.callingState
1174
- switch callingState {
1175
- case .idle:
1176
- state = "idle"
1177
- case .incoming:
1178
- state = "ringing"
1179
- case .outgoing:
1180
- state = "ringing"
1181
- case .inCall:
1182
- state = "joined"
1183
- case .lobby:
1184
- state = "lobby"
1185
- case .joining:
1186
- state = "joining"
1187
- case .reconnecting:
1188
- state = "reconnecting"
1189
- case .none:
1190
- state = "unknown"
1191
- }
1461
+ // Convert RawJSON dictionary to [String: Any] via JSONSerialization
1462
+ let custom: [String: Any]
1463
+ let jsonData = try JSONEncoder().encode(customRaw)
1464
+ custom = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] ?? [:]
1192
1465
 
1193
- var result: [String: Any] = [:]
1194
- result["callId"] = callId
1195
- result["state"] = state
1466
+ let result: [String: Any] = [
1467
+ "callId": callId,
1468
+ "custom": custom
1469
+ ]
1196
1470
 
1197
- if let caller = caller {
1198
- result["caller"] = caller
1199
- }
1471
+ print("Debug: Resolving getCallInfo with custom =", custom)
1472
+ return result
1473
+ } catch {
1474
+ print("Error resolving getCallInfo:", error)
1475
+ return [
1476
+ "callId": callId,
1477
+ "custom": [:]
1478
+ ]
1479
+ }
1480
+ }
1200
1481
 
1201
- result["members"] = members
1482
+ @objc func enableBluetooth(call: CAPPluginCall) {
1483
+ Task {
1484
+ do {
1202
1485
 
1203
- call.resolve(result)
1204
- } catch {
1205
- call.reject("Failed to get call info: \(error.localizedDescription)")
1206
- }
1486
+ let policy = DefaultAudioSessionPolicy()
1487
+ try await self.callViewModel?.call?.updateAudioSessionPolicy(policy)
1488
+ call.resolve([
1489
+ "success": true
1490
+ ])
1491
+ } catch {
1492
+ print("Failed to update policy: \(error)")
1493
+ call.reject("Unable to set bluetooth policy")
1207
1494
  }
1208
- } catch {
1209
- call.reject("StreamVideo not initialized")
1210
1495
  }
1211
1496
  }
1212
1497
 
@@ -1350,4 +1635,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
1350
1635
  }
1351
1636
  }
1352
1637
 
1638
+ @objc func getPluginVersion(_ call: CAPPluginCall) {
1639
+ call.resolve(["version": self.pluginVersion])
1640
+ }
1641
+
1353
1642
  }