@capgo/capacitor-stream-call 0.0.5 → 0.0.6

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.
@@ -63,6 +63,7 @@ public class StreamCallPlugin : Plugin() {
63
63
  private var savedActivity: Activity? = null
64
64
  private var savedActivityPaused = false
65
65
  private var savedCallsToEndOnResume = mutableListOf<Call>()
66
+ private val callStates: MutableMap<String, CallState> = mutableMapOf()
66
67
 
67
68
  private enum class State {
68
69
  NOT_INITIALIZED,
@@ -342,7 +343,7 @@ public class StreamCallPlugin : Plugin() {
342
343
  SecureUserRepository.getInstance(context).save(credentials)
343
344
 
344
345
  // Initialize Stream Video with new credentials
345
- if (!hadSavedCredentials || (savedCredentials!!.user.id !== userId)) {
346
+ if (!hadSavedCredentials || (savedCredentials!!.user.id != userId)) {
346
347
  initializeStreamVideo()
347
348
  }
348
349
 
@@ -560,89 +561,152 @@ public class StreamCallPlugin : Plugin() {
560
561
  client.subscribe { event: VideoEvent ->
561
562
  android.util.Log.v("StreamCallPlugin", "Received an event ${event.getEventType()} $event")
562
563
  when (event) {
563
- is CallEndedEvent -> {
564
- runOnMainThread {
565
- android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallEndedEvent for call ${event.call.cid}")
566
- overlayView?.setContent {
567
- CallOverlayView(
568
- context = context,
569
- streamVideo = streamVideoClient,
570
- call = null
571
- )
572
- }
573
- overlayView?.isVisible = false
574
- // Stop ringtone if it's still playing
575
- ringtonePlayer?.stopRinging()
576
- // Hide incoming call view if visible
577
- incomingCallView?.isVisible = false
578
- }
564
+ // Handle CallCreatedEvent differently - only log it but don't try to access members yet
565
+ is org.openapitools.client.models.CallCreatedEvent -> {
566
+ val callCid = event.call.cid
567
+ android.util.Log.d("StreamCallPlugin", "Call created: $callCid")
568
+
569
+ // let's get the members
570
+ val callParticipants = event.members.filter{ it.user.id != this@StreamCallPlugin.streamVideoClient?.userId } .map { it.user.id }
571
+ android.util.Log.d("StreamCallPlugin", "Call created for $callCid with ${callParticipants.size} participants")
572
+
573
+ // Start tracking this call now that we have the member list
574
+ startCallTimeoutMonitor(callCid, callParticipants)
575
+
579
576
  val data = JSObject().apply {
580
- put("callId", event.call.cid)
581
- put("state", "left")
577
+ put("callId", callCid)
578
+ put("state", "created")
582
579
  }
583
580
  notifyListeners("callEvent", data)
584
581
  }
585
- is CallSessionEndedEvent -> {
586
- runOnMainThread {
587
- android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallSessionEndedEvent for call ${event.call.cid}")
588
- overlayView?.setContent {
589
- CallOverlayView(
590
- context = context,
591
- streamVideo = streamVideoClient,
592
- call = null
593
- )
594
- }
595
- overlayView?.isVisible = false
596
- // Stop ringtone if it's still playing
597
- ringtonePlayer?.stopRinging()
598
- // Hide incoming call view if visible
599
- incomingCallView?.isVisible = false
600
- }
582
+ // Add handler for CallSessionStartedEvent which contains participant information
583
+ is org.openapitools.client.models.CallSessionStartedEvent -> {
584
+ val callCid = event.call.cid
585
+
601
586
  val data = JSObject().apply {
602
- put("callId", event.call.cid)
603
- put("state", "left")
587
+ put("callId", callCid)
588
+ put("state", "session_started")
604
589
  }
605
590
  notifyListeners("callEvent", data)
606
591
  }
592
+
607
593
  is CallRejectedEvent -> {
608
- runOnMainThread {
609
- android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallRejectedEvent for call ${event.call.cid}")
610
- overlayView?.setContent {
611
- CallOverlayView(
612
- context = context,
613
- streamVideo = streamVideoClient,
614
- call = null
615
- )
616
- }
617
- overlayView?.isVisible = false
618
-
619
- val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
620
- if (keyguardManager.isKeyguardLocked) {
621
- android.util.Log.d("StreamCallPlugin", "Stop ringing and move to background")
622
- this@StreamCallPlugin.ringtonePlayer?.stopRinging()
623
- moveAllActivitiesToBackgroundOrKill(context)
624
- }
594
+ val userId = event.user.id
595
+ val callCid = event.call.cid
596
+
597
+ // Update call state
598
+ callStates[callCid]?.let { callState ->
599
+ callState.participantResponses[userId] = "rejected"
625
600
  }
601
+
602
+ // runOnMainThread {
603
+ // android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallRejectedEvent for call ${event.call.cid}")
604
+ // overlayView?.setContent {
605
+ // CallOverlayView(
606
+ // context = context,
607
+ // streamVideo = streamVideoClient,
608
+ // call = null
609
+ // )
610
+ // }
611
+ // overlayView?.isVisible = false
612
+ // }
613
+
626
614
  val data = JSObject().apply {
627
615
  put("callId", event.call.cid)
628
616
  put("state", "rejected")
617
+ put("userId", userId)
629
618
  }
630
619
  notifyListeners("callEvent", data)
620
+
621
+ // Check if all participants have responded
622
+ checkAllParticipantsResponded(callCid)
631
623
  }
624
+
632
625
  is CallMissedEvent -> {
626
+ val userId = event.user.id
627
+ val callCid = event.call.cid
628
+
629
+ // Update call state
630
+ callStates[callCid]?.let { callState ->
631
+ callState.participantResponses[userId] = "missed"
632
+ }
633
+
633
634
  val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
634
635
  if (keyguardManager.isKeyguardLocked) {
635
636
  android.util.Log.d("StreamCallPlugin", "Stop ringing and move to background")
636
637
  this.ringtonePlayer?.stopRinging()
637
638
  moveAllActivitiesToBackgroundOrKill(context)
638
639
  }
640
+
641
+ val data = JSObject().apply {
642
+ put("callId", callCid)
643
+ put("state", "missed")
644
+ put("userId", userId)
645
+ }
646
+ notifyListeners("callEvent", data)
647
+
648
+ // Check if all participants have responded
649
+ checkAllParticipantsResponded(callCid)
650
+ }
651
+
652
+ is CallAcceptedEvent -> {
653
+ val userId = event.user.id
654
+ val callCid = event.call.cid
655
+
656
+ // Update call state
657
+ callStates[callCid]?.let { callState ->
658
+ callState.participantResponses[userId] = "accepted"
659
+
660
+ // Since someone accepted, cancel the timeout timer
661
+ android.util.Log.d("StreamCallPlugin", "Call accepted by $userId, canceling timeout timer for $callCid")
662
+ callState.timer?.removeCallbacksAndMessages(null)
663
+ callState.timer = null
664
+ }
665
+
666
+ val data = JSObject().apply {
667
+ put("callId", callCid)
668
+ put("state", "accepted")
669
+ put("userId", userId)
670
+ }
671
+ notifyListeners("callEvent", data)
672
+ }
673
+
674
+ is CallEndedEvent -> {
675
+ runOnMainThread {
676
+ android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallEndedEvent for call ${event.call.cid}")
677
+ // Clean up call resources
678
+ val callCid = event.call.cid
679
+ cleanupCall(callCid)
680
+ }
681
+ val data = JSObject().apply {
682
+ put("callId", event.call.cid)
683
+ put("state", "left")
684
+ }
685
+ notifyListeners("callEvent", data)
686
+ }
687
+
688
+ is CallSessionEndedEvent -> {
689
+ runOnMainThread {
690
+ android.util.Log.d("StreamCallPlugin", "Setting overlay invisible due to CallSessionEndedEvent for call ${event.call.cid}")
691
+ // Clean up call resources
692
+ val callCid = event.call.cid
693
+ cleanupCall(callCid)
694
+ }
695
+ val data = JSObject().apply {
696
+ put("callId", event.call.cid)
697
+ put("state", "left")
698
+ }
699
+ notifyListeners("callEvent", data)
700
+ }
701
+
702
+ else -> {
703
+ val data = JSObject().apply {
704
+ put("callId", streamVideoClient?.state?.activeCall?.value?.cid)
705
+ put("state", event.getEventType())
706
+ }
707
+ notifyListeners("callEvent", data)
639
708
  }
640
709
  }
641
- val data = JSObject().apply {
642
- put("callId", streamVideoClient?.state?.activeCall?.value?.cid)
643
- put("state", event.getEventType())
644
- }
645
- notifyListeners("callEvent", data)
646
710
  }
647
711
 
648
712
  // Add call state subscription using collect
@@ -1015,6 +1079,7 @@ public class StreamCallPlugin : Plugin() {
1015
1079
  val callType = call.getString("type") ?: "default"
1016
1080
  val shouldRing = call.getBoolean("ring") ?: true
1017
1081
  val callId = java.util.UUID.randomUUID().toString()
1082
+ val callCid = "$callType:$callId"
1018
1083
 
1019
1084
  android.util.Log.d("StreamCallPlugin", "Creating call:")
1020
1085
  android.util.Log.d("StreamCallPlugin", "- Call ID: $callId")
@@ -1027,59 +1092,10 @@ public class StreamCallPlugin : Plugin() {
1027
1092
  try {
1028
1093
  // Create the call object
1029
1094
  val streamCall = streamVideoClient?.call(type = callType, id = callId)
1030
-
1031
- // Track participants responses
1032
- val participantResponses = mutableMapOf<String, String>()
1033
- val totalParticipants = userIds.size
1034
-
1035
- // Subscribe to call events for this specific call
1036
- streamCall?.subscribe { event ->
1037
- when (event) {
1038
- is CallRejectedEvent -> {
1039
- val userId = event.user.id
1040
- android.util.Log.d("StreamCallPlugin", "Call was rejected by user: $userId")
1041
- participantResponses[userId] = "rejected"
1042
-
1043
- val data = JSObject().apply {
1044
- put("callId", callId)
1045
- put("state", "rejected")
1046
- put("userId", userId)
1047
- }
1048
- notifyListeners("callEvent", data)
1049
-
1050
- // Check if all participants have rejected or missed
1051
- checkAllParticipantsResponded(participantResponses, totalParticipants, streamCall)
1052
- }
1053
- is CallMissedEvent -> {
1054
- val userId = event.user.id
1055
- android.util.Log.d("StreamCallPlugin", "Call was missed by user: $userId")
1056
- participantResponses[userId] = "missed"
1057
-
1058
- val data = JSObject().apply {
1059
- put("callId", callId)
1060
- put("state", "missed")
1061
- put("userId", userId)
1062
- }
1063
- notifyListeners("callEvent", data)
1064
-
1065
- // Check if all participants have rejected or missed
1066
- checkAllParticipantsResponded(participantResponses, totalParticipants, streamCall)
1067
- }
1068
- is CallAcceptedEvent -> {
1069
- val userId = event.user.id
1070
- android.util.Log.d("StreamCallPlugin", "Call was accepted by user: $userId")
1071
- participantResponses[userId] = "accepted"
1072
-
1073
- val data = JSObject().apply {
1074
- put("callId", callId)
1075
- put("state", "accepted")
1076
- put("userId", userId)
1077
- }
1078
- notifyListeners("callEvent", data)
1079
- }
1080
- }
1081
- }
1082
-
1095
+
1096
+ // Note: We no longer start tracking here - we'll wait for CallSessionStartedEvent
1097
+ // instead, which contains the actual participant list
1098
+
1083
1099
  android.util.Log.d("StreamCallPlugin", "Creating call with members...")
1084
1100
  // Create the call with all members
1085
1101
  streamCall?.create(
@@ -1115,40 +1131,158 @@ public class StreamCallPlugin : Plugin() {
1115
1131
  }
1116
1132
  }
1117
1133
 
1118
- @OptIn(DelicateCoroutinesApi::class)
1119
- private fun checkAllParticipantsResponded(
1120
- participantResponses: Map<String, String>,
1121
- totalParticipants: Int,
1122
- streamCall: Call
1123
- ) {
1124
- kotlinx.coroutines.GlobalScope.launch {
1125
- try {
1126
- val allResponded = participantResponses.size == totalParticipants
1127
- val allRejectedOrMissed = participantResponses.values.all { it == "rejected" || it == "missed" }
1134
+ private fun startCallTimeoutMonitor(callCid: String, memberIds: List<String>) {
1135
+ val callState = CallState(members = memberIds)
1136
+
1137
+ val handler = Handler(Looper.getMainLooper())
1138
+ val timeoutRunnable = object : Runnable {
1139
+ override fun run() {
1140
+ checkCallTimeout(callCid)
1141
+ handler.postDelayed(this, 1000)
1142
+ }
1143
+ }
1144
+
1145
+ handler.postDelayed(timeoutRunnable, 1000)
1146
+ callState.timer = handler
1147
+
1148
+ callStates[callCid] = callState
1149
+
1150
+ android.util.Log.d("StreamCallPlugin", "Started timeout monitor for call $callCid with ${memberIds.size} members")
1151
+ }
1152
+
1153
+ private fun checkCallTimeout(callCid: String) {
1154
+ val callState = callStates[callCid] ?: return
1155
+
1156
+ val now = System.currentTimeMillis()
1157
+ val elapsedSeconds = (now - callState.createdAt) / 1000
1158
+
1159
+ if (elapsedSeconds >= 30) {
1160
+ android.util.Log.d("StreamCallPlugin", "Call $callCid has timed out after $elapsedSeconds seconds")
1161
+
1162
+ val hasAccepted = callState.participantResponses.values.any { it == "accepted" }
1163
+
1164
+ if (!hasAccepted) {
1165
+ android.util.Log.d("StreamCallPlugin", "No one accepted call $callCid, marking all non-responders as missed")
1128
1166
 
1129
- if (allResponded && allRejectedOrMissed) {
1130
- android.util.Log.d("StreamCallPlugin", "All participants have rejected or missed the call")
1131
- streamCall.leave()
1132
- activity?.runOnUiThread {
1133
- overlayView?.setContent {
1134
- CallOverlayView(
1135
- context = context,
1136
- streamVideo = streamVideoClient,
1137
- call = null
1138
- )
1167
+ // First, remove the timer to prevent further callbacks
1168
+ callState.timer?.removeCallbacksAndMessages(null)
1169
+ callState.timer = null
1170
+
1171
+ callState.members.forEach { memberId ->
1172
+ if (memberId !in callState.participantResponses) {
1173
+ callState.participantResponses[memberId] = "missed"
1174
+
1175
+ val data = JSObject().apply {
1176
+ put("callId", callCid)
1177
+ put("state", "missed")
1178
+ put("userId", memberId)
1139
1179
  }
1140
- overlayView?.isVisible = false
1180
+ notifyListeners("callEvent", data)
1141
1181
  }
1182
+ }
1183
+
1184
+ val callIdParts = callCid.split(":")
1185
+ if (callIdParts.size >= 2) {
1186
+ val callType = callIdParts[0]
1187
+ val callId = callIdParts[1]
1142
1188
 
1143
- val data = JSObject().apply {
1144
- put("callId", streamCall.id)
1145
- put("state", "ended")
1146
- put("reason", "all_rejected_or_missed")
1189
+ streamVideoClient?.call(type = callType, id = callId)?.let { call ->
1190
+ kotlinx.coroutines.GlobalScope.launch {
1191
+ try {
1192
+ // Use endCallRaw instead of manual cleanup
1193
+ endCallRaw(call)
1194
+
1195
+ // Clean up state - we don't need to do this in endCallRaw because we already did it here
1196
+ callStates.remove(callCid)
1197
+
1198
+ // Notify that call has ended
1199
+ val data = JSObject().apply {
1200
+ put("callId", callCid)
1201
+ put("state", "ended")
1202
+ put("reason", "timeout")
1203
+ }
1204
+ notifyListeners("callEvent", data)
1205
+ } catch (e: Exception) {
1206
+ android.util.Log.e("StreamCallPlugin", "Error ending timed out call", e)
1207
+ }
1208
+ }
1209
+ }
1210
+ }
1211
+ }
1212
+ }
1213
+ }
1214
+
1215
+ private fun cleanupCall(callCid: String) {
1216
+ // Get the call state
1217
+ val callState = callStates[callCid]
1218
+
1219
+ if (callState != null) {
1220
+ // Ensure timer is properly canceled
1221
+ android.util.Log.d("StreamCallPlugin", "Stopping timer for call: $callCid")
1222
+ callState.timer?.removeCallbacksAndMessages(null)
1223
+ callState.timer = null
1224
+ }
1225
+
1226
+ // Remove from callStates
1227
+ callStates.remove(callCid)
1228
+
1229
+ // Hide UI elements directly without setting content
1230
+ runOnMainThread {
1231
+ android.util.Log.d("StreamCallPlugin", "Hiding UI elements for call $callCid (one-time cleanup)")
1232
+ overlayView?.isVisible = false
1233
+ ringtonePlayer?.stopRinging()
1234
+ incomingCallView?.isVisible = false
1235
+ }
1236
+
1237
+ android.util.Log.d("StreamCallPlugin", "Cleaned up resources for ended call: $callCid")
1238
+ }
1239
+
1240
+ private fun checkAllParticipantsResponded(callCid: String) {
1241
+ val callState = callStates[callCid] ?: return
1242
+
1243
+ val totalParticipants = callState.members.size
1244
+ val responseCount = callState.participantResponses.size
1245
+
1246
+ android.util.Log.d("StreamCallPlugin", "Checking responses for call $callCid: $responseCount / $totalParticipants")
1247
+
1248
+ val allResponded = responseCount >= totalParticipants
1249
+ val allRejectedOrMissed = allResponded &&
1250
+ callState.participantResponses.values.all { it == "rejected" || it == "missed" }
1251
+
1252
+ if (allResponded && allRejectedOrMissed) {
1253
+ android.util.Log.d("StreamCallPlugin", "All participants have rejected or missed the call $callCid")
1254
+
1255
+ // Cancel the timer immediately to prevent further callbacks
1256
+ callState.timer?.removeCallbacksAndMessages(null)
1257
+ callState.timer = null
1258
+
1259
+ // End the call using endCallRaw
1260
+ val callIdParts = callCid.split(":")
1261
+ if (callIdParts.size >= 2) {
1262
+ val callType = callIdParts[0]
1263
+ val callId = callIdParts[1]
1264
+
1265
+ streamVideoClient?.call(type = callType, id = callId)?.let { call ->
1266
+ kotlinx.coroutines.GlobalScope.launch {
1267
+ try {
1268
+ // Use endCallRaw instead of manual cleanup
1269
+ endCallRaw(call)
1270
+
1271
+ // Clean up state - we don't need to do this in endCallRaw because we already did it here
1272
+ callStates.remove(callCid)
1273
+
1274
+ // Notify that call has ended
1275
+ val data = JSObject().apply {
1276
+ put("callId", callCid)
1277
+ put("state", "ended")
1278
+ put("reason", "all_rejected_or_missed")
1279
+ }
1280
+ notifyListeners("callEvent", data)
1281
+ } catch (e: Exception) {
1282
+ android.util.Log.e("StreamCallPlugin", "Error ending call after all rejected/missed", e)
1283
+ }
1147
1284
  }
1148
- notifyListeners("callEvent", data)
1149
1285
  }
1150
- } catch (e: Exception) {
1151
- android.util.Log.e("StreamCallPlugin", "Error checking participant responses: ${e.message}")
1152
1286
  }
1153
1287
  }
1154
1288
  }
@@ -1215,4 +1349,11 @@ public class StreamCallPlugin : Plugin() {
1215
1349
  android.util.Log.e("StreamCallPlugin", "Error in magicDeviceDelete", e)
1216
1350
  }
1217
1351
  }
1352
+
1353
+ data class CallState(
1354
+ val members: List<String>,
1355
+ val participantResponses: MutableMap<String, String> = mutableMapOf(),
1356
+ val createdAt: Long = System.currentTimeMillis(),
1357
+ var timer: Handler? = null
1358
+ )
1218
1359
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-stream-call",
3
- "version": "0.0.5",
3
+ "version": "0.0.6",
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",