@capgo/capacitor-stream-call 0.0.43 → 0.0.51

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.
@@ -26,7 +26,8 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
26
26
  CAPPluginMethod(name: "isCameraEnabled", returnType: CAPPluginReturnPromise),
27
27
  CAPPluginMethod(name: "getCallStatus", returnType: CAPPluginReturnPromise),
28
28
  CAPPluginMethod(name: "setSpeaker", returnType: CAPPluginReturnPromise),
29
- CAPPluginMethod(name: "switchCamera", returnType: CAPPluginReturnPromise)
29
+ CAPPluginMethod(name: "switchCamera", returnType: CAPPluginReturnPromise),
30
+ CAPPluginMethod(name: "getCallInfo", returnType: CAPPluginReturnPromise)
30
31
  ]
31
32
 
32
33
  private enum State {
@@ -60,7 +61,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
60
61
  private var callViewModel: CallViewModel?
61
62
 
62
63
  // Helper method to update call status and notify listeners
63
- private func updateCallStatusAndNotify(callId: String, state: String, userId: String? = nil, reason: String? = nil) {
64
+ private func updateCallStatusAndNotify(callId: String, state: String, userId: String? = nil, reason: String? = nil, caller: [String: Any]? = nil, members: [[String: Any]]? = nil) {
64
65
  // Update stored call info
65
66
  currentCallId = callId
66
67
  currentCallState = state
@@ -78,6 +79,14 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
78
79
  if let reason = reason {
79
80
  data["reason"] = reason
80
81
  }
82
+
83
+ if let caller = caller {
84
+ data["caller"] = caller
85
+ }
86
+
87
+ if let members = members {
88
+ data["members"] = members
89
+ }
81
90
 
82
91
  // Notify listeners
83
92
  notifyListeners("callEvent", data: data)
@@ -231,20 +240,69 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
231
240
  self.hasNotifiedCallJoined = true
232
241
  }
233
242
  } else if case .incoming(let incomingCall) = newState {
234
- self.updateCallStatusAndNotify(callId: incomingCall.id, state: "ringing")
235
- } else if newState == .idle && self.streamVideo?.state.activeCall == nil {
243
+ // Extract caller information
244
+ Task {
245
+ var caller: [String: Any]? = nil
246
+ var members: [[String: Any]]? = nil
247
+
248
+ do {
249
+ // Get the call from StreamVideo to access detailed information
250
+ if let streamVideo = self.streamVideo {
251
+ let call = streamVideo.call(callType: incomingCall.type, callId: incomingCall.id)
252
+ let callInfo = try await call.get()
253
+
254
+ // Extract caller information
255
+ let createdBy = callInfo.call.createdBy
256
+ var callerData: [String: Any] = [:]
257
+ callerData["userId"] = createdBy.id
258
+ callerData["name"] = createdBy.name
259
+ callerData["imageURL"] = createdBy.image
260
+ callerData["role"] = createdBy.role
261
+ caller = callerData
262
+
263
+ // Extract members information from current participants if available
264
+ var membersArray: [[String: Any]] = []
265
+ if let activeCall = streamVideo.state.activeCall {
266
+ let participants = activeCall.state.participants
267
+ for participant in participants {
268
+ var memberData: [String: Any] = [:]
269
+ memberData["userId"] = participant.userId
270
+ memberData["name"] = participant.name
271
+ memberData["imageURL"] = participant.profileImageURL?.absoluteString ?? ""
272
+ memberData["role"] = participant.roles.first ?? ""
273
+ membersArray.append(memberData)
274
+ }
275
+ }
276
+ members = membersArray.isEmpty ? nil : membersArray
277
+ }
278
+ } catch {
279
+ print("Failed to get call info for caller details: \(error)")
280
+ }
281
+
282
+ // Notify with caller information
283
+ self.updateCallStatusAndNotify(callId: incomingCall.id, state: "ringing", caller: caller, members: members)
284
+ }
285
+ } else if newState == .idle {
236
286
  // Get the call ID that was active before the state changed
237
287
  let endingCallId = viewModel.call?.cId
238
- print("Call ending: \(String(describing: endingCallId))")
239
-
240
- // Notify that call has ended - use the properly tracked call ID
241
- self.updateCallStatusAndNotify(callId: endingCallId ?? "", state: "left")
288
+ print("Call state changed to idle. EndingCallId: \(String(describing: endingCallId)), ActiveCall: \(String(describing: self.streamVideo?.state.activeCall?.cId))")
242
289
 
243
- // Reset notification flag when call ends
244
- self.hasNotifiedCallJoined = false
245
-
246
- // Remove the call overlay view when not in a call
247
- self.ensureViewRemoved()
290
+ // Only notify about call ending if we have a valid call ID and there's truly no active call
291
+ // This prevents false "left" events during normal state transitions
292
+ if let callId = endingCallId, !callId.isEmpty, self.streamVideo?.state.activeCall == nil {
293
+ print("Call actually ending: \(callId)")
294
+
295
+ // Notify that call has ended - use the properly tracked call ID
296
+ self.updateCallStatusAndNotify(callId: callId, state: "left")
297
+
298
+ // Reset notification flag when call ends
299
+ self.hasNotifiedCallJoined = false
300
+
301
+ // Remove the call overlay view when not in a call
302
+ self.ensureViewRemoved()
303
+ } else {
304
+ print("Not sending left event - CallId: \(String(describing: endingCallId)), ActiveCall exists: \(self.streamVideo?.state.activeCall != nil)")
305
+ }
248
306
  }
249
307
  } catch {
250
308
  log.error("Error handling call state update: \(String(describing: error))")
@@ -409,9 +467,6 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
409
467
  print("- Should Ring: \(shouldRing)")
410
468
  print("- Team: \(team)")
411
469
 
412
-
413
-
414
- // Update the CallOverlayView with the active call
415
470
  // Create the call object
416
471
  await self.callViewModel?.startCall(
417
472
  callType: callType,
@@ -420,6 +475,46 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
420
475
  ring: shouldRing
421
476
  )
422
477
 
478
+ // Wait for call state to be populated by WebSocket events
479
+ let callStream = streamVideo!.call(callType: callType, callId: callId)
480
+
481
+ // Wait until we have member data - with timeout to prevent infinite loop
482
+ var allMembers: [[String: Any]] = []
483
+ var attempts = 0
484
+ let maxAttempts = 50 // 5 seconds max
485
+
486
+ while allMembers.isEmpty && attempts < maxAttempts {
487
+ let membersList = await callStream.state.members
488
+ if !membersList.isEmpty {
489
+ allMembers = membersList.map { member in
490
+ [
491
+ "userId": member.user.id,
492
+ "name": member.user.name,
493
+ "imageURL": member.user.imageURL?.absoluteString ?? "",
494
+ "role": member.user.role
495
+ ]
496
+ }
497
+ } else {
498
+ attempts += 1
499
+ // Wait a bit and try again
500
+ try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds
501
+ }
502
+ }
503
+
504
+ // If we still don't have members after timeout, use basic data
505
+ if allMembers.isEmpty {
506
+ allMembers = members.map { userId in
507
+ [
508
+ "userId": userId,
509
+ "name": "",
510
+ "imageURL": "",
511
+ "role": ""
512
+ ]
513
+ }
514
+ }
515
+
516
+ // Now send the created event with complete member data
517
+ self.updateCallStatusAndNotify(callId: callId, state: "created", members: allMembers)
423
518
 
424
519
  // Update UI on main thread
425
520
  await MainActor.run {
@@ -430,7 +525,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
430
525
 
431
526
  call.resolve([
432
527
  "success": true
433
- ])
528
+ ])
434
529
 
435
530
  } catch {
436
531
  log.error("Error making call: \(String(describing: error))")
@@ -447,16 +542,81 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
447
542
  try requireInitialized()
448
543
 
449
544
  Task {
450
- if let activeCall = streamVideo?.state.activeCall {
451
- activeCall.leave()
452
-
453
- // Update view state instead of cleaning up
545
+ // Check both active call and callViewModel's call state to handle outgoing calls
546
+ let activeCall = streamVideo?.state.activeCall
547
+ let viewModelCall = await callViewModel?.call
548
+
549
+ // Helper function to determine if we should end or leave the call
550
+ func shouldEndCall(for streamCall: Call) async throws -> Bool {
551
+ do {
552
+ let callInfo = try await streamCall.get()
553
+ let currentUserId = streamVideo?.user.id
554
+ let createdBy = callInfo.call.createdBy.id
555
+ let isCreator = createdBy == currentUserId
556
+
557
+ // Use call.state.participants.count to get participant count (as per StreamVideo iOS SDK docs)
558
+ let totalParticipants = await streamCall.state.participants.count
559
+ let shouldEnd = isCreator || totalParticipants <= 2
560
+
561
+ print("Call \(streamCall.cId) - Creator: \(createdBy), CurrentUser: \(currentUserId ?? "nil"), IsCreator: \(isCreator), TotalParticipants: \(totalParticipants), ShouldEnd: \(shouldEnd)")
562
+
563
+ return shouldEnd
564
+ } catch {
565
+ print("Error getting call info for \(streamCall.cId), defaulting to leave: \(error)")
566
+ return false // Fallback to leave if we can't determine
567
+ }
568
+ }
569
+
570
+ if let activeCall = activeCall {
571
+ // There's an active call, check if we should end or leave
572
+ do {
573
+ let shouldEnd = try await shouldEndCall(for: activeCall)
574
+
575
+ if shouldEnd {
576
+ print("Ending active call \(activeCall.cId) for all participants")
577
+ try await activeCall.end()
578
+ } else {
579
+ print("Leaving active call \(activeCall.cId)")
580
+ try await activeCall.leave()
581
+ }
582
+ } catch {
583
+ print("Error ending/leaving active call: \(error)")
584
+ try await activeCall.leave() // Fallback to leave
585
+ }
586
+
454
587
  await MainActor.run {
455
- // self.overlayViewModel?.updateCall(nil)
456
588
  self.overlayView?.isHidden = true
457
589
  self.webView?.isOpaque = true
458
590
  }
459
-
591
+
592
+ call.resolve([
593
+ "success": true
594
+ ])
595
+ } else if let viewModelCall = viewModelCall {
596
+ // There's a call in the viewModel (likely outgoing/ringing), check if we should end or leave
597
+ do {
598
+ let shouldEnd = try await shouldEndCall(for: viewModelCall)
599
+
600
+ if shouldEnd {
601
+ print("Ending viewModel call \(viewModelCall.cId) for all participants")
602
+ try await viewModelCall.end()
603
+ } else {
604
+ print("Leaving viewModel call \(viewModelCall.cId)")
605
+ try await viewModelCall.leave()
606
+ }
607
+ } catch {
608
+ print("Error ending/leaving viewModel call: \(error)")
609
+ try await viewModelCall.leave() // Fallback to leave
610
+ }
611
+
612
+ // Also hang up to reset the calling state
613
+ await callViewModel?.hangUp()
614
+
615
+ await MainActor.run {
616
+ self.overlayView?.isHidden = true
617
+ self.webView?.isOpaque = true
618
+ }
619
+
460
620
  call.resolve([
461
621
  "success": true
462
622
  ])
@@ -783,7 +943,7 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
783
943
  }
784
944
 
785
945
  @objc func getCallStatus(_ call: CAPPluginCall) {
786
- // Use stored state rather than accessing SDK state directly
946
+ // If not in a call, reject
787
947
  if currentCallId.isEmpty || currentCallState == "left" {
788
948
  call.reject("Not in a call")
789
949
  return
@@ -794,6 +954,90 @@ public class StreamCallPlugin: CAPPlugin, CAPBridgedPlugin {
794
954
  "state": currentCallState
795
955
  ])
796
956
  }
957
+
958
+ @objc func getCallInfo(_ call: CAPPluginCall) {
959
+ guard let callId = call.getString("callId") else {
960
+ call.reject("Missing required parameter: callId")
961
+ return
962
+ }
963
+
964
+ do {
965
+ try requireInitialized()
966
+
967
+ guard let activeCall = streamVideo?.state.activeCall, activeCall.cId == callId else {
968
+ call.reject("Call ID does not match active call")
969
+ return
970
+ }
971
+
972
+ Task {
973
+ do {
974
+ // Get detailed call information
975
+ let callInfo = try await activeCall.get()
976
+
977
+ // Extract caller information
978
+ var caller: [String: Any]? = nil
979
+ let createdBy = callInfo.call.createdBy
980
+ var callerData: [String: Any] = [:]
981
+ callerData["userId"] = createdBy.id
982
+ callerData["name"] = createdBy.name
983
+ callerData["imageURL"] = createdBy.image
984
+ callerData["role"] = createdBy.role
985
+ caller = callerData
986
+
987
+ // Extract members information
988
+ var membersArray: [[String: Any]] = []
989
+ let participants = await activeCall.state.participants
990
+ for participant in participants {
991
+ var memberData: [String: Any] = [:]
992
+ memberData["userId"] = participant.userId
993
+ memberData["name"] = participant.name
994
+ memberData["imageURL"] = participant.profileImageURL
995
+ memberData["role"] = participant.roles.first ?? ""
996
+ membersArray.append(memberData)
997
+ }
998
+ let members = membersArray
999
+
1000
+ // Determine call state based on current calling state
1001
+ let state: String
1002
+ let callingState = await self.callViewModel?.callingState
1003
+ switch callingState {
1004
+ case .idle:
1005
+ state = "idle"
1006
+ case .incoming:
1007
+ state = "ringing"
1008
+ case .outgoing:
1009
+ state = "ringing"
1010
+ case .inCall:
1011
+ state = "joined"
1012
+ case .lobby:
1013
+ state = "lobby"
1014
+ case .joining:
1015
+ state = "joining"
1016
+ case .reconnecting:
1017
+ state = "reconnecting"
1018
+ case .none:
1019
+ state = "unknown"
1020
+ }
1021
+
1022
+ var result: [String: Any] = [:]
1023
+ result["callId"] = callId
1024
+ result["state"] = state
1025
+
1026
+ if let caller = caller {
1027
+ result["caller"] = caller
1028
+ }
1029
+
1030
+ result["members"] = members
1031
+
1032
+ call.resolve(result)
1033
+ } catch {
1034
+ call.reject("Failed to get call info: \(error.localizedDescription)")
1035
+ }
1036
+ }
1037
+ } catch {
1038
+ call.reject("StreamVideo not initialized")
1039
+ }
1040
+ }
797
1041
 
798
1042
  @objc func setSpeaker(_ call: CAPPluginCall) {
799
1043
  guard let name = call.getString("name") else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-stream-call",
3
- "version": "0.0.43",
3
+ "version": "0.0.51",
4
4
  "description": "Uses the https://getstream.io/ SDK to implement calling in Capacitor",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",