@capgo/capacitor-stream-call 0.0.43 → 0.0.50

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.
package/README.md CHANGED
@@ -140,123 +140,96 @@ Add string resources for different languages:
140
140
  </resources>
141
141
  ```
142
142
 
143
- ## Usage
143
+ ## Displaying Caller Information
144
144
 
145
- ### Handling Incoming Calls (Android)
146
- On Android, to handle incoming calls when the device is locked, you need to listen for the `incomingCall` event and manage the call state properly:
145
+ When receiving incoming calls, you can access caller information including name, user ID, and profile image. This information is automatically extracted from the call data and passed through the event system.
147
146
 
148
- ```typescript
149
- import { StreamCall } from '@capgo/capacitor-stream-call';
147
+ ### Getting Caller Information
150
148
 
151
- export class CallService {
152
- private incomingCallId: string | null = null;
153
- private isLockscreenIncoming = false;
149
+ The caller information is available in two ways:
154
150
 
155
- constructor() {
156
- this.setupIncomingCallListener();
157
- }
151
+ **1. Through Call Events**
158
152
 
159
- private setupIncomingCallListener() {
160
- StreamCall.addListener('incomingCall', async (payload) => {
161
- console.log('Incoming call from lockscreen:', payload);
162
-
163
- // Store the call ID and show incoming call UI
164
- this.incomingCallId = payload.cid;
165
- this.isLockscreenIncoming = true;
166
-
167
- // Show your custom incoming call screen
168
- this.showIncomingCallScreen(payload);
169
- });
170
- }
153
+ The `callEvent` listener provides caller information for incoming calls:
171
154
 
172
- // Accept the incoming call
173
- async acceptCall() {
174
- if (!this.incomingCallId) {
175
- console.warn('No incoming call to accept');
176
- return;
177
- }
178
-
179
- try {
180
- await StreamCall.acceptCall();
181
- this.clearIncomingCall();
182
- console.log('Call accepted successfully');
183
- } catch (error) {
184
- console.error('Failed to accept call:', error);
185
- }
186
- }
187
-
188
- // Reject the incoming call
189
- async rejectCall() {
190
- if (!this.incomingCallId) {
191
- console.warn('No incoming call to reject');
192
- return;
193
- }
194
-
195
- try {
196
- await StreamCall.rejectCall();
197
- this.clearIncomingCall();
198
- console.log('Call rejected successfully');
199
- } catch (error) {
200
- console.error('Failed to reject call:', error);
201
- }
155
+ ```typescript
156
+ StreamCall.addListener('callEvent', (event) => {
157
+ if (event.state === 'ringing' && event.caller) {
158
+ console.log('Incoming call from:', event.caller.name || event.caller.userId);
159
+ console.log('Caller image:', event.caller.imageURL);
160
+ // Update your UI to show caller information
161
+ showIncomingCallUI(event.caller);
202
162
  }
163
+ });
164
+ ```
203
165
 
204
- private clearIncomingCall() {
205
- this.incomingCallId = null;
206
- this.isLockscreenIncoming = false;
207
- // Hide your incoming call UI
208
- this.hideIncomingCallScreen();
209
- }
166
+ **2. Through Incoming Call Events (Android lock-screen)**
210
167
 
211
- private showIncomingCallScreen(payload: any) {
212
- // Implement your custom incoming call UI
213
- // This should show accept/reject buttons and caller information
214
- // Example: navigate to incoming call page or show modal
215
- }
168
+ The `incomingCall` listener also includes caller information:
216
169
 
217
- private hideIncomingCallScreen() {
218
- // Hide the incoming call UI
219
- // Example: navigate away from incoming call page or close modal
170
+ ```typescript
171
+ StreamCall.addListener('incomingCall', (payload) => {
172
+ if (payload.caller) {
173
+ console.log('Lock-screen call from:', payload.caller.name || payload.caller.userId);
174
+ // Update your lock-screen UI
175
+ updateLockScreenUI(payload.caller);
220
176
  }
221
- }
177
+ });
222
178
  ```
223
179
 
224
- > **Important:** This lock-screen handling is only required on Android. On iOS, the system handles incoming call UI automatically.
180
+ ### Caller Information Structure
225
181
 
226
- ### Basic Call Operations
227
182
  ```typescript
228
- import { StreamCall } from '@capgo/capacitor-stream-call';
229
-
230
- // Login to Stream Video
231
- await StreamCall.login({
232
- token: 'your_user_token',
233
- userId: 'user_id',
234
- name: 'User Name',
235
- apiKey: 'your_api_key',
236
- magicDivId: 'video-container'
237
- });
183
+ interface CallMember {
184
+ userId: string; // User ID (always present)
185
+ name?: string; // Display name (optional)
186
+ imageURL?: string; // Profile image URL (optional)
187
+ role?: string; // User role (optional)
188
+ }
189
+ ```
238
190
 
239
- // Make a call
240
- await StreamCall.call({
241
- userIds: ['user_to_call'],
242
- type: 'default',
243
- ring: true
244
- });
191
+ ### Example Implementation
245
192
 
246
- // End call
247
- await StreamCall.endCall();
193
+ Here's how to implement a proper incoming call screen with caller information:
248
194
 
249
- // Toggle microphone
250
- await StreamCall.setMicrophoneEnabled({ enabled: false });
195
+ ```typescript
196
+ export class CallService {
197
+ private callerInfo: CallMember | null = null;
251
198
 
252
- // Toggle camera
253
- await StreamCall.setCameraEnabled({ enabled: false });
199
+ constructor() {
200
+ this.setupCallListeners();
201
+ }
254
202
 
255
- // Switch camera
256
- await StreamCall.switchCamera({ camera: 'front' });
203
+ private setupCallListeners() {
204
+ StreamCall.addListener('callEvent', (event) => {
205
+ if (event.state === 'ringing') {
206
+ this.callerInfo = event.caller || null;
207
+ this.showIncomingCallScreen();
208
+ } else if (event.state === 'joined' || event.state === 'left') {
209
+ this.callerInfo = null;
210
+ this.hideIncomingCallScreen();
211
+ }
212
+ });
257
213
 
258
- // Logout
259
- await StreamCall.logout();
214
+ // Android lock-screen support
215
+ if (Capacitor.getPlatform() === 'android') {
216
+ StreamCall.addListener('incomingCall', (payload) => {
217
+ this.callerInfo = payload.caller || null;
218
+ this.showLockScreenIncomingCall();
219
+ });
220
+ }
221
+ }
222
+
223
+ private showIncomingCallScreen() {
224
+ const callerName = this.callerInfo?.name || 'Unknown Caller';
225
+ const callerImage = this.callerInfo?.imageURL || 'default-avatar.png';
226
+
227
+ // Update your UI components
228
+ document.getElementById('caller-name').textContent = callerName;
229
+ document.getElementById('caller-image').src = callerImage;
230
+ document.getElementById('incoming-call-screen').style.display = 'block';
231
+ }
232
+ }
260
233
  ```
261
234
 
262
235
  ## API
@@ -278,6 +251,7 @@ await StreamCall.logout();
278
251
  * [`getCallStatus()`](#getcallstatus)
279
252
  * [`setSpeaker(...)`](#setspeaker)
280
253
  * [`switchCamera(...)`](#switchcamera)
254
+ * [`getCallInfo(...)`](#getcallinfo)
281
255
  * [Interfaces](#interfaces)
282
256
  * [Type Aliases](#type-aliases)
283
257
  * [Enums](#enums)
@@ -515,6 +489,23 @@ Switch camera
515
489
  --------------------
516
490
 
517
491
 
492
+ ### getCallInfo(...)
493
+
494
+ ```typescript
495
+ getCallInfo(options: { callId: string; }) => Promise<CallEvent>
496
+ ```
497
+
498
+ Get detailed information about an active call including caller details
499
+
500
+ | Param | Type | Description |
501
+ | ------------- | -------------------------------- | -------------------------------- |
502
+ | **`options`** | <code>{ callId: string; }</code> | - Options containing the call ID |
503
+
504
+ **Returns:** <code>Promise&lt;<a href="#callevent">CallEvent</a>&gt;</code>
505
+
506
+ --------------------
507
+
508
+
518
509
  ### Interfaces
519
510
 
520
511
 
@@ -549,12 +540,14 @@ Switch camera
549
540
 
550
541
  #### CallEvent
551
542
 
552
- | Prop | Type | Description |
553
- | ------------ | ----------------------------------------------- | -------------------------------------------------------------- |
554
- | **`callId`** | <code>string</code> | ID of the call |
555
- | **`state`** | <code><a href="#callstate">CallState</a></code> | Current state of the call |
556
- | **`userId`** | <code>string</code> | User ID of the participant in the call who triggered the event |
557
- | **`reason`** | <code>string</code> | Reason for the call state change, if applicable |
543
+ | Prop | Type | Description |
544
+ | ------------- | ------------------------------------------------- | -------------------------------------------------------------- |
545
+ | **`callId`** | <code>string</code> | ID of the call |
546
+ | **`state`** | <code><a href="#callstate">CallState</a></code> | Current state of the call |
547
+ | **`userId`** | <code>string</code> | User ID of the participant in the call who triggered the event |
548
+ | **`reason`** | <code>string</code> | Reason for the call state change, if applicable |
549
+ | **`caller`** | <code><a href="#callmember">CallMember</a></code> | Information about the caller (for incoming calls) |
550
+ | **`members`** | <code>CallMember[]</code> | List of call members |
558
551
 
559
552
 
560
553
  #### CallState
@@ -748,12 +741,23 @@ The JSON representation for <a href="#listvalue">`ListValue`</a> is JSON array.
748
741
  | **`sessionId`** | <code>string</code> | the user sesion_id to pin, if not provided, applies to all sessions |
749
742
 
750
743
 
744
+ #### CallMember
745
+
746
+ | Prop | Type | Description |
747
+ | -------------- | ------------------- | ----------------------------- |
748
+ | **`userId`** | <code>string</code> | User ID of the member |
749
+ | **`name`** | <code>string</code> | Display name of the user |
750
+ | **`imageURL`** | <code>string</code> | Profile image URL of the user |
751
+ | **`role`** | <code>string</code> | Role of the user in the call |
752
+
753
+
751
754
  #### IncomingCallPayload
752
755
 
753
- | Prop | Type | Description |
754
- | ---------- | ----------------------- | ---------------------------------------- |
755
- | **`cid`** | <code>string</code> | Full call CID (e.g. default:123) |
756
- | **`type`** | <code>'incoming'</code> | Event type (currently always "incoming") |
756
+ | Prop | Type | Description |
757
+ | ------------ | ------------------------------------------------- | ---------------------------------------- |
758
+ | **`cid`** | <code>string</code> | Full call CID (e.g. default:123) |
759
+ | **`type`** | <code>'incoming'</code> | Event type (currently always "incoming") |
760
+ | **`caller`** | <code><a href="#callmember">CallMember</a></code> | Information about the caller |
757
761
 
758
762
 
759
763
  #### CameraEnabledResponse
@@ -63,6 +63,7 @@ import io.getstream.video.android.core.CameraDirection
63
63
  import android.content.BroadcastReceiver
64
64
  import android.content.Intent
65
65
  import android.content.IntentFilter
66
+ import com.getcapacitor.JSArray
66
67
  import io.getstream.android.video.generated.models.CallSessionParticipantLeftEvent
67
68
  import io.getstream.video.android.core.RealtimeConnection
68
69
  import io.getstream.video.android.core.events.ParticipantLeftEvent
@@ -215,15 +216,37 @@ public class StreamCallPlugin : Plugin() {
215
216
  // Start ringtone only; UI handled in web layer
216
217
  ringtonePlayer?.startRinging()
217
218
 
218
- // Notify WebView/JS about incoming call so it can render its own UI
219
- try {
220
- val payload = com.getcapacitor.JSObject().apply {
221
- put("cid", cid.cid)
222
- put("type", "incoming")
219
+ // Try to get caller information from the call
220
+ kotlinx.coroutines.GlobalScope.launch {
221
+ try {
222
+ val callInfo = call?.get()
223
+ val callerInfo = callInfo?.getOrNull()?.call?.createdBy
224
+
225
+ val payload = com.getcapacitor.JSObject().apply {
226
+ put("cid", cid.cid)
227
+ put("type", "incoming")
228
+ if (callerInfo != null) {
229
+ val caller = com.getcapacitor.JSObject().apply {
230
+ put("userId", callerInfo.id)
231
+ put("name", callerInfo.name ?: "")
232
+ put("imageURL", callerInfo.image ?: "")
233
+ put("role", callerInfo.role ?: "")
234
+ }
235
+ put("caller", caller)
236
+ }
237
+ }
238
+
239
+ // Notify WebView/JS about incoming call so it can render its own UI
240
+ notifyListeners("incomingCall", payload, true)
241
+ } catch (e: Exception) {
242
+ android.util.Log.e("StreamCallPlugin", "Error getting call info for incoming call", e)
243
+ // Fallback to basic payload without caller info
244
+ val payload = com.getcapacitor.JSObject().apply {
245
+ put("cid", cid.cid)
246
+ put("type", "incoming")
247
+ }
248
+ notifyListeners("incomingCall", payload, true)
223
249
  }
224
- notifyListeners("incomingCall", payload, true)
225
- } catch (e: Exception) {
226
- android.util.Log.e("StreamCallPlugin", "Error notifying JS about incoming call", e)
227
250
  }
228
251
 
229
252
  bringAppToForeground()
@@ -602,7 +625,38 @@ public class StreamCallPlugin : Plugin() {
602
625
  android.util.Log.v("StreamCallPlugin", "Received an event ${event.getEventType()} $event")
603
626
  when (event) {
604
627
  is CallRingEvent -> {
605
- updateCallStatusAndNotify(event.callCid, "ringing")
628
+ // Extract caller information from the ringing call
629
+ kotlinx.coroutines.GlobalScope.launch {
630
+ try {
631
+ val callCid = event.callCid
632
+ val callIdParts = callCid.split(":")
633
+ if (callIdParts.size >= 2) {
634
+ val callType = callIdParts[0]
635
+ val callId = callIdParts[1]
636
+ val call = streamVideoClient?.call(type = callType, id = callId)
637
+ val callInfo = call?.get()
638
+ val callerInfo = callInfo?.getOrNull()?.call?.createdBy
639
+
640
+ // Pass caller information to the ringing event
641
+ if (callerInfo != null) {
642
+ val caller = mapOf(
643
+ "userId" to callerInfo.id,
644
+ "name" to (callerInfo.name ?: ""),
645
+ "imageURL" to (callerInfo.image ?: ""),
646
+ "role" to (callerInfo.role ?: "")
647
+ )
648
+ updateCallStatusAndNotify(event.callCid, "ringing", null, null, null, caller)
649
+ } else {
650
+ updateCallStatusAndNotify(event.callCid, "ringing")
651
+ }
652
+ } else {
653
+ updateCallStatusAndNotify(event.callCid, "ringing")
654
+ }
655
+ } catch (e: Exception) {
656
+ android.util.Log.e("StreamCallPlugin", "Error getting caller info for ringing event", e)
657
+ updateCallStatusAndNotify(event.callCid, "ringing")
658
+ }
659
+ }
606
660
  }
607
661
  // Handle CallCreatedEvent differently - only log it but don't try to access members yet
608
662
  is CallCreatedEvent -> {
@@ -611,22 +665,59 @@ public class StreamCallPlugin : Plugin() {
611
665
  android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: All members from event: ${event.members.joinToString { it.user.id + " (role: " + it.user.role + ")" }}")
612
666
  android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: Self user ID from SDK: ${this@StreamCallPlugin.streamVideoClient?.userId}")
613
667
 
614
- val callParticipants = event.members.filter {
615
- val selfId = this@StreamCallPlugin.streamVideoClient?.userId
616
- val memberId = it.user.id
617
- val isSelf = memberId == selfId
618
- android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: Filtering member $memberId. Self ID: $selfId. Is self: $isSelf")
619
- !isSelf
620
- }.map { it.user.id }
621
-
622
- android.util.Log.d("StreamCallPlugin", "Call created for $callCid with ${callParticipants.size} remote participants: ${callParticipants.joinToString()}.")
623
-
624
- // Start tracking this call now that we have the member list
625
- startCallTimeoutMonitor(callCid, callParticipants)
626
-
627
- // Use direction from event if available
628
- val callType = callCid.split(":").firstOrNull() ?: "default"
629
- updateCallStatusAndNotify(callCid, "created")
668
+ // Only send "created" event for outgoing calls (calls created by current user)
669
+ // For incoming calls, we'll only send "ringing" event in CallRingEvent handler
670
+ kotlinx.coroutines.GlobalScope.launch {
671
+ try {
672
+ val callIdParts = callCid.split(":")
673
+ if (callIdParts.size >= 2) {
674
+ val callType = callIdParts[0]
675
+ val callId = callIdParts[1]
676
+ val call = streamVideoClient?.call(type = callType, id = callId)
677
+ val callInfo = call?.get()
678
+ val createdBy = callInfo?.getOrNull()?.call?.createdBy
679
+ val currentUserId = streamVideoClient?.userId
680
+
681
+ android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: Call created by: ${createdBy?.id}, Current user: $currentUserId")
682
+
683
+ // Only notify for outgoing calls (where current user is the creator)
684
+ if (createdBy?.id == currentUserId) {
685
+ android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: This is an outgoing call, sending created event")
686
+
687
+ val callParticipants = event.members.filter {
688
+ val selfId = this@StreamCallPlugin.streamVideoClient?.userId
689
+ val memberId = it.user.id
690
+ val isSelf = memberId == selfId
691
+ android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: Filtering member $memberId. Self ID: $selfId. Is self: $isSelf")
692
+ !isSelf
693
+ }.map { it.user.id }
694
+
695
+ android.util.Log.d("StreamCallPlugin", "Call created for $callCid with ${callParticipants.size} remote participants: ${callParticipants.joinToString()}.")
696
+
697
+ // Start tracking this call now that we have the member list
698
+ startCallTimeoutMonitor(callCid, callParticipants)
699
+
700
+ // Extract all members information (including self) for UI display
701
+ val allMembers = event.members.map { member ->
702
+ mapOf(
703
+ "userId" to member.user.id,
704
+ "name" to (member.user.name ?: ""),
705
+ "imageURL" to (member.user.image ?: ""),
706
+ "role" to (member.user.role ?: "")
707
+ )
708
+ }
709
+
710
+ updateCallStatusAndNotify(callCid, "created", null, null, allMembers)
711
+ } else {
712
+ android.util.Log.d("StreamCallPlugin", "CallCreatedEvent: This is an incoming call (created by ${createdBy?.id}), not sending created event")
713
+ }
714
+ } else {
715
+ android.util.Log.w("StreamCallPlugin", "CallCreatedEvent: Invalid call CID format: $callCid")
716
+ }
717
+ } catch (e: Exception) {
718
+ android.util.Log.e("StreamCallPlugin", "Error processing CallCreatedEvent", e)
719
+ }
720
+ }
630
721
  }
631
722
  // Add handler for CallSessionStartedEvent which contains participant information
632
723
  is CallSessionStartedEvent -> {
@@ -1153,7 +1244,35 @@ public class StreamCallPlugin : Plugin() {
1153
1244
  private suspend fun endCallRaw(call: Call) {
1154
1245
  val callId = call.id
1155
1246
  android.util.Log.d("StreamCallPlugin", "Attempting to end call $callId")
1156
- call.leave()
1247
+
1248
+ try {
1249
+ // Get call information to make the decision
1250
+ val callInfo = call.get()
1251
+ val callData = callInfo?.getOrNull()?.call
1252
+ val currentUserId = streamVideoClient?.userId
1253
+ val createdBy = callData?.createdBy?.id
1254
+ val isCreator = createdBy == currentUserId
1255
+
1256
+ // Use call.state.totalParticipants to get participant count (as per StreamVideo Android SDK docs)
1257
+ val totalParticipants = call.state.totalParticipants.value ?: 0
1258
+ val shouldEndCall = isCreator || totalParticipants <= 2
1259
+
1260
+ android.util.Log.d("StreamCallPlugin", "Call $callId - Creator: $createdBy, CurrentUser: $currentUserId, IsCreator: $isCreator, TotalParticipants: $totalParticipants, ShouldEnd: $shouldEndCall")
1261
+
1262
+ if (shouldEndCall) {
1263
+ // End the call for everyone if I'm the creator or only 2 people
1264
+ android.util.Log.d("StreamCallPlugin", "Ending call $callId for all participants (creator: $isCreator, participants: $totalParticipants)")
1265
+ call.end()
1266
+ } else {
1267
+ // Just leave the call if there are more than 2 people and I'm not the creator
1268
+ android.util.Log.d("StreamCallPlugin", "Leaving call $callId (not creator, >2 participants)")
1269
+ call.leave()
1270
+ }
1271
+ } catch (e: Exception) {
1272
+ android.util.Log.e("StreamCallPlugin", "Error getting call info for $callId, defaulting to leave()", e)
1273
+ // Fallback to leave if we can't determine the call info
1274
+ call.leave()
1275
+ }
1157
1276
 
1158
1277
  // Capture context from the overlayView
1159
1278
  val currentContext = overlayView?.context ?: this.savedContext
@@ -1250,16 +1369,22 @@ public class StreamCallPlugin : Plugin() {
1250
1369
  @PluginMethod
1251
1370
  fun endCall(call: PluginCall) {
1252
1371
  try {
1253
- val activeCall = streamVideoClient?.state?.activeCall
1254
- if (activeCall == null) {
1255
- android.util.Log.w("StreamCallPlugin", "Attempted to end call but no active call found")
1372
+ val activeCall = streamVideoClient?.state?.activeCall?.value
1373
+ val ringingCall = streamVideoClient?.state?.ringingCall?.value
1374
+
1375
+ val callToEnd = activeCall ?: ringingCall
1376
+
1377
+ if (callToEnd == null) {
1378
+ android.util.Log.w("StreamCallPlugin", "Attempted to end call but no active or ringing call found")
1256
1379
  call.reject("No active call to end")
1257
1380
  return
1258
1381
  }
1259
1382
 
1383
+ android.util.Log.d("StreamCallPlugin", "Ending call: activeCall=${activeCall?.id}, ringingCall=${ringingCall?.id}, callToEnd=${callToEnd.id}")
1384
+
1260
1385
  kotlinx.coroutines.GlobalScope.launch {
1261
1386
  try {
1262
- activeCall.value?.let { endCallRaw(it) }
1387
+ endCallRaw(callToEnd)
1263
1388
  call.resolve(JSObject().apply {
1264
1389
  put("success", true)
1265
1390
  })
@@ -1587,7 +1712,7 @@ public class StreamCallPlugin : Plugin() {
1587
1712
  }
1588
1713
 
1589
1714
  // Helper method to update call status and notify listeners
1590
- private fun updateCallStatusAndNotify(callId: String, state: String, userId: String? = null, reason: String? = null) {
1715
+ private fun updateCallStatusAndNotify(callId: String, state: String, userId: String? = null, reason: String? = null, members: List<Map<String, Any>>? = null, caller: Map<String, Any>? = null) {
1591
1716
  android.util.Log.d("StreamCallPlugin", "updateCallStatusAndNotify called: callId=$callId, state=$state, userId=$userId, reason=$reason")
1592
1717
  // Update stored call info
1593
1718
  currentCallId = callId
@@ -1608,6 +1733,26 @@ public class StreamCallPlugin : Plugin() {
1608
1733
  reason?.let {
1609
1734
  put("reason", it)
1610
1735
  }
1736
+ members?.let { membersList ->
1737
+ val membersArray = JSArray()
1738
+ membersList.forEach { member ->
1739
+ val memberObj = JSObject().apply {
1740
+ member.forEach { (key, value) ->
1741
+ put(key, value)
1742
+ }
1743
+ }
1744
+ membersArray.put(memberObj)
1745
+ }
1746
+ put("members", membersArray)
1747
+ }
1748
+ caller?.let { callerInfo ->
1749
+ val callerObj = JSObject().apply {
1750
+ callerInfo.forEach { (key, value) ->
1751
+ put(key, value)
1752
+ }
1753
+ }
1754
+ put("caller", callerObj)
1755
+ }
1611
1756
  }
1612
1757
 
1613
1758
  // Notify listeners