@capgo/capacitor-stream-call 0.0.3 → 0.0.5
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 +1 -0
- package/android/src/main/java/ee/forgr/capacitor/streamcall/CallOverlayView.kt +5 -16
- package/android/src/main/java/ee/forgr/capacitor/streamcall/CustomNotificationHandler.kt +1 -1
- package/android/src/main/java/ee/forgr/capacitor/streamcall/IncomingCallView.kt +7 -2
- package/android/src/main/java/ee/forgr/capacitor/streamcall/StreamCallPlugin.kt +82 -11
- package/dist/docs.json +7 -0
- package/dist/esm/definitions.d.ts +2 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +10 -0
- package/dist/esm/web.js +243 -8
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +243 -8
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +243 -8
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/StreamCallPlugin/StreamCallPlugin.swift +256 -24
- package/ios/Sources/StreamCallPlugin/TouchInterceptView.swift +3 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -337,6 +337,7 @@ isCameraEnabled() => Promise<CameraEnabledResponse>
|
|
|
337
337
|
| **`callId`** | <code>string</code> | ID of the call |
|
|
338
338
|
| **`state`** | <code>string</code> | Current state of the call |
|
|
339
339
|
| **`userId`** | <code>string</code> | User ID of the participant in the call who triggered the event |
|
|
340
|
+
| **`reason`** | <code>string</code> | Reason for the call state change |
|
|
340
341
|
|
|
341
342
|
|
|
342
343
|
#### CameraEnabledResponse
|
|
@@ -206,22 +206,11 @@ fun CallOverlayView(
|
|
|
206
206
|
floatingVideoRenderer = floatingVideoRender
|
|
207
207
|
)
|
|
208
208
|
} else {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
color = VideoTheme.colors.basePrimary
|
|
215
|
-
)
|
|
216
|
-
} else {
|
|
217
|
-
Text(
|
|
218
|
-
modifier = Modifier.padding(30.dp),
|
|
219
|
-
text = "Join call ${call.id} in your browser to see the video here",
|
|
220
|
-
fontSize = 30.sp,
|
|
221
|
-
color = VideoTheme.colors.basePrimary,
|
|
222
|
-
textAlign = TextAlign.Center
|
|
223
|
-
)
|
|
224
|
-
}
|
|
209
|
+
Box(
|
|
210
|
+
modifier = Modifier
|
|
211
|
+
.fillMaxSize()
|
|
212
|
+
.background(VideoTheme.colors.baseSenary)
|
|
213
|
+
)
|
|
225
214
|
}
|
|
226
215
|
}
|
|
227
216
|
}
|
|
@@ -136,8 +136,13 @@ fun IncomingCallView(
|
|
|
136
136
|
isCameraEnabled = isCameraEnabled,
|
|
137
137
|
onCallAction = { action ->
|
|
138
138
|
when (action) {
|
|
139
|
-
DeclineCall ->
|
|
140
|
-
|
|
139
|
+
DeclineCall -> {
|
|
140
|
+
onDeclineCall?.invoke(call)
|
|
141
|
+
}
|
|
142
|
+
AcceptCall -> {
|
|
143
|
+
call.camera.setEnabled(isCameraEnabled)
|
|
144
|
+
onAcceptCall?.invoke(call)
|
|
145
|
+
}
|
|
141
146
|
is ToggleCamera -> {
|
|
142
147
|
call.camera.setEnabled(action.isEnabled)
|
|
143
148
|
}
|
|
@@ -35,6 +35,7 @@ import io.getstream.video.android.model.StreamCallId
|
|
|
35
35
|
import io.getstream.video.android.model.User
|
|
36
36
|
import io.getstream.video.android.model.streamCallId
|
|
37
37
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
38
|
+
import kotlinx.coroutines.flow.Flow
|
|
38
39
|
import kotlinx.coroutines.launch
|
|
39
40
|
import org.openapitools.client.models.CallAcceptedEvent
|
|
40
41
|
import org.openapitools.client.models.CallEndedEvent
|
|
@@ -42,6 +43,8 @@ import org.openapitools.client.models.CallMissedEvent
|
|
|
42
43
|
import org.openapitools.client.models.CallRejectedEvent
|
|
43
44
|
import org.openapitools.client.models.CallSessionEndedEvent
|
|
44
45
|
import org.openapitools.client.models.VideoEvent
|
|
46
|
+
import io.getstream.video.android.model.Device
|
|
47
|
+
import kotlinx.coroutines.flow.first
|
|
45
48
|
|
|
46
49
|
// I am not a religious pearson, but at this point, I am not sure even god himself would understand this code
|
|
47
50
|
// It's a spaghetti-like, tangled, unreadable mess and frankly, I am deeply sorry for the code crimes commited in the Android impl
|
|
@@ -202,7 +205,7 @@ public class StreamCallPlugin : Plugin() {
|
|
|
202
205
|
private fun declineCall(call: Call) {
|
|
203
206
|
kotlinx.coroutines.GlobalScope.launch {
|
|
204
207
|
try {
|
|
205
|
-
call.
|
|
208
|
+
call.reject()
|
|
206
209
|
|
|
207
210
|
// Stop ringtone
|
|
208
211
|
ringtonePlayer?.stopRinging()
|
|
@@ -339,7 +342,7 @@ public class StreamCallPlugin : Plugin() {
|
|
|
339
342
|
SecureUserRepository.getInstance(context).save(credentials)
|
|
340
343
|
|
|
341
344
|
// Initialize Stream Video with new credentials
|
|
342
|
-
if (!hadSavedCredentials) {
|
|
345
|
+
if (!hadSavedCredentials || (savedCredentials!!.user.id !== userId)) {
|
|
343
346
|
initializeStreamVideo()
|
|
344
347
|
}
|
|
345
348
|
|
|
@@ -358,15 +361,20 @@ public class StreamCallPlugin : Plugin() {
|
|
|
358
361
|
SecureUserRepository.getInstance(context).removeCurrentUser()
|
|
359
362
|
|
|
360
363
|
// Properly cleanup the client
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
364
|
+
kotlinx.coroutines.GlobalScope.launch {
|
|
365
|
+
streamVideoClient?.let {
|
|
366
|
+
magicDeviceDelete(it)
|
|
367
|
+
it.logOut()
|
|
368
|
+
StreamVideo.removeClient()
|
|
369
|
+
}
|
|
366
370
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
371
|
+
streamVideoClient = null
|
|
372
|
+
state = State.NOT_INITIALIZED
|
|
373
|
+
|
|
374
|
+
val ret = JSObject()
|
|
375
|
+
ret.put("success", true)
|
|
376
|
+
call.resolve(ret)
|
|
377
|
+
}
|
|
370
378
|
} catch (e: Exception) {
|
|
371
379
|
call.reject("Failed to logout", e)
|
|
372
380
|
}
|
|
@@ -733,7 +741,7 @@ public class StreamCallPlugin : Plugin() {
|
|
|
733
741
|
}
|
|
734
742
|
|
|
735
743
|
// Join the call without affecting others
|
|
736
|
-
call.
|
|
744
|
+
call.accept()
|
|
737
745
|
|
|
738
746
|
// Notify that call has started
|
|
739
747
|
val data = JSObject().apply {
|
|
@@ -1144,4 +1152,67 @@ public class StreamCallPlugin : Plugin() {
|
|
|
1144
1152
|
}
|
|
1145
1153
|
}
|
|
1146
1154
|
}
|
|
1155
|
+
|
|
1156
|
+
private suspend fun magicDeviceDelete(streamVideoClient: StreamVideo) {
|
|
1157
|
+
try {
|
|
1158
|
+
android.util.Log.d("StreamCallPlugin", "Starting magicDeviceDelete reflection operation")
|
|
1159
|
+
|
|
1160
|
+
// Get the streamNotificationManager field from StreamVideo
|
|
1161
|
+
val streamVideoClass = streamVideoClient.javaClass
|
|
1162
|
+
val notificationManagerField = streamVideoClass.getDeclaredField("streamNotificationManager")
|
|
1163
|
+
notificationManagerField.isAccessible = true
|
|
1164
|
+
val notificationManager = notificationManagerField.get(streamVideoClient)
|
|
1165
|
+
|
|
1166
|
+
if (notificationManager == null) {
|
|
1167
|
+
android.util.Log.e("StreamCallPlugin", "streamNotificationManager is null")
|
|
1168
|
+
return
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
android.util.Log.d("StreamCallPlugin", "Successfully accessed streamNotificationManager")
|
|
1172
|
+
|
|
1173
|
+
// Get deviceTokenStorage from notification manager
|
|
1174
|
+
val notificationManagerClass = notificationManager.javaClass
|
|
1175
|
+
val deviceTokenStorageField = notificationManagerClass.getDeclaredField("deviceTokenStorage")
|
|
1176
|
+
deviceTokenStorageField.isAccessible = true
|
|
1177
|
+
val deviceTokenStorage = deviceTokenStorageField.get(notificationManager)
|
|
1178
|
+
|
|
1179
|
+
if (deviceTokenStorage == null) {
|
|
1180
|
+
android.util.Log.e("StreamCallPlugin", "deviceTokenStorage is null")
|
|
1181
|
+
return
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
android.util.Log.d("StreamCallPlugin", "Successfully accessed deviceTokenStorage")
|
|
1185
|
+
|
|
1186
|
+
// Access the DeviceTokenStorage object dynamically without hardcoding class
|
|
1187
|
+
val deviceTokenStorageClass = deviceTokenStorage.javaClass
|
|
1188
|
+
|
|
1189
|
+
// Get the userDevice Flow from deviceTokenStorage
|
|
1190
|
+
val userDeviceField = deviceTokenStorageClass.getDeclaredField("userDevice")
|
|
1191
|
+
userDeviceField.isAccessible = true
|
|
1192
|
+
val userDeviceFlow = userDeviceField.get(deviceTokenStorage)
|
|
1193
|
+
|
|
1194
|
+
if (userDeviceFlow == null) {
|
|
1195
|
+
android.util.Log.e("StreamCallPlugin", "userDevice Flow is null")
|
|
1196
|
+
return
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
android.util.Log.d("StreamCallPlugin", "Successfully accessed userDevice Flow: $userDeviceFlow")
|
|
1200
|
+
|
|
1201
|
+
val castedUserDeviceFlow = userDeviceFlow as Flow<Device?>
|
|
1202
|
+
try {
|
|
1203
|
+
castedUserDeviceFlow.first {
|
|
1204
|
+
if (it == null) {
|
|
1205
|
+
android.util.Log.d("StreamCallPlugin", "Device is null. Nothing to remove")
|
|
1206
|
+
return@first true;
|
|
1207
|
+
}
|
|
1208
|
+
streamVideoClient.deleteDevice(it)
|
|
1209
|
+
return@first true;
|
|
1210
|
+
}
|
|
1211
|
+
} catch (e: Throwable) {
|
|
1212
|
+
android.util.Log.e("StreamCallPlugin", "Cannot collect flow in magicDeviceDelete", e)
|
|
1213
|
+
}
|
|
1214
|
+
} catch (e: Exception) {
|
|
1215
|
+
android.util.Log.e("StreamCallPlugin", "Error in magicDeviceDelete", e)
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1147
1218
|
}
|
package/dist/docs.json
CHANGED
|
@@ -506,6 +506,13 @@
|
|
|
506
506
|
"docs": "User ID of the participant in the call who triggered the event",
|
|
507
507
|
"complexTypes": [],
|
|
508
508
|
"type": "string | undefined"
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
"name": "reason",
|
|
512
|
+
"tags": [],
|
|
513
|
+
"docs": "Reason for the call state change",
|
|
514
|
+
"complexTypes": [],
|
|
515
|
+
"type": "string | undefined"
|
|
509
516
|
}
|
|
510
517
|
]
|
|
511
518
|
},
|
|
@@ -59,6 +59,8 @@ export interface CallEvent {
|
|
|
59
59
|
state: string;
|
|
60
60
|
/** User ID of the participant in the call who triggered the event */
|
|
61
61
|
userId?: string;
|
|
62
|
+
/** Reason for the call state change */
|
|
63
|
+
reason?: string;
|
|
62
64
|
}
|
|
63
65
|
export interface CameraEnabledResponse {
|
|
64
66
|
enabled: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * @interface LoginOptions\n * @description Configuration options for logging into the Stream Video service\n * @property {string} token - Stream Video API token for authentication\n * @property {string} userId - Unique identifier for the current user\n * @property {string} name - Display name for the current user\n * @property {string} [imageURL] - Avatar URL for the current user\n * @property {string} apiKey - Stream Video API key for your application\n * @property {string} [magicDivId] - DOM element ID where video will be rendered\n */\nexport interface LoginOptions {\n /** Stream Video API token */\n token: string;\n /** User ID for the current user */\n userId: string;\n /** Display name for the current user */\n name: string;\n /** Optional avatar URL for the current user */\n imageURL?: string;\n /** Stream Video API key */\n apiKey: string;\n /** ID of the HTML element where the video will be rendered */\n magicDivId?: string;\n}\n\n/**\n * @interface CallOptions\n * @description Options for initiating a video call\n * @property {string} userId - ID of the user to call\n * @property {string} [type=default] - Type of call\n * @property {boolean} [ring=true] - Whether to send ring notification\n */\nexport interface CallOptions {\n /** User ID of the person to call */\n userIds: string[];\n /** Type of call, defaults to 'default' */\n type?: string;\n /** Whether to ring the other user, defaults to true */\n ring?: boolean;\n}\n\n/**\n * @interface SuccessResponse\n * @description Standard response indicating operation success/failure\n * @property {boolean} success - Whether the operation succeeded\n */\nexport interface SuccessResponse {\n /** Whether the operation was successful */\n success: boolean;\n}\n\n/**\n * @interface CallEvent\n * @description Event emitted when call state changes\n * @property {string} callId - Unique identifier of the call\n * @property {string} state - Current state of the call (joined, left, ringing, etc)\n */\nexport interface CallEvent {\n /** ID of the call */\n callId: string;\n /** Current state of the call */\n state: string;\n /** User ID of the participant in the call who triggered the event */\n userId?: string\n}\n\nexport interface CameraEnabledResponse {\n enabled: boolean;\n}\n\n/**\n * @interface StreamCallPlugin\n * @description Capacitor plugin for Stream Video calling functionality\n */\nexport interface StreamCallPlugin {\n /**\n * Login to Stream Video service\n * @param {LoginOptions} options - Login configuration\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.login({\n * token: 'your-token',\n * userId: 'user-123',\n * name: 'John Doe',\n * apiKey: 'your-api-key'\n * });\n */\n login(options: LoginOptions): Promise<SuccessResponse>;\n\n /**\n * Logout from Stream Video service\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.logout();\n */\n logout(): Promise<SuccessResponse>;\n\n /**\n * Initiate a call to another user\n * @param {CallOptions} options - Call configuration\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.call({\n * userId: 'user-456',\n * type: 'video',\n * ring: true\n * });\n */\n call(options: CallOptions): Promise<SuccessResponse>;\n\n /**\n * End the current call\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.endCall();\n */\n endCall(): Promise<SuccessResponse>;\n\n /**\n * Enable or disable microphone\n * @param {{ enabled: boolean }} options - Microphone state\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.setMicrophoneEnabled({ enabled: false });\n */\n setMicrophoneEnabled(options: { enabled: boolean }): Promise<SuccessResponse>;\n\n /**\n * Enable or disable camera\n * @param {{ enabled: boolean }} options - Camera state\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.setCameraEnabled({ enabled: false });\n */\n setCameraEnabled(options: { enabled: boolean }): Promise<SuccessResponse>;\n\n /**\n * Add listener for call events\n * @param {'callEvent'} eventName - Name of the event to listen for\n * @param {(event: CallEvent) => void} listenerFunc - Callback function\n * @returns {Promise<{ remove: () => Promise<void> }>} Function to remove listener\n * @example\n * const listener = await StreamCall.addListener('callEvent', (event) => {\n * console.log(`Call ${event.callId} is now ${event.state}`);\n * });\n */\n addListener(\n eventName: 'callEvent',\n listenerFunc: (event: CallEvent) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Remove all event listeners\n * @returns {Promise<void>}\n * @example\n * await StreamCall.removeAllListeners();\n */\n removeAllListeners(): Promise<void>;\n\n /**\n * Accept an incoming call\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.acceptCall();\n */\n acceptCall(): Promise<SuccessResponse>;\n\n /**\n * Reject an incoming call\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.rejectCall();\n */\n rejectCall(): Promise<SuccessResponse>;\n isCameraEnabled(): Promise<CameraEnabledResponse>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * @interface LoginOptions\n * @description Configuration options for logging into the Stream Video service\n * @property {string} token - Stream Video API token for authentication\n * @property {string} userId - Unique identifier for the current user\n * @property {string} name - Display name for the current user\n * @property {string} [imageURL] - Avatar URL for the current user\n * @property {string} apiKey - Stream Video API key for your application\n * @property {string} [magicDivId] - DOM element ID where video will be rendered\n */\nexport interface LoginOptions {\n /** Stream Video API token */\n token: string;\n /** User ID for the current user */\n userId: string;\n /** Display name for the current user */\n name: string;\n /** Optional avatar URL for the current user */\n imageURL?: string;\n /** Stream Video API key */\n apiKey: string;\n /** ID of the HTML element where the video will be rendered */\n magicDivId?: string;\n}\n\n/**\n * @interface CallOptions\n * @description Options for initiating a video call\n * @property {string} userId - ID of the user to call\n * @property {string} [type=default] - Type of call\n * @property {boolean} [ring=true] - Whether to send ring notification\n */\nexport interface CallOptions {\n /** User ID of the person to call */\n userIds: string[];\n /** Type of call, defaults to 'default' */\n type?: string;\n /** Whether to ring the other user, defaults to true */\n ring?: boolean;\n}\n\n/**\n * @interface SuccessResponse\n * @description Standard response indicating operation success/failure\n * @property {boolean} success - Whether the operation succeeded\n */\nexport interface SuccessResponse {\n /** Whether the operation was successful */\n success: boolean;\n}\n\n/**\n * @interface CallEvent\n * @description Event emitted when call state changes\n * @property {string} callId - Unique identifier of the call\n * @property {string} state - Current state of the call (joined, left, ringing, etc)\n */\nexport interface CallEvent {\n /** ID of the call */\n callId: string;\n /** Current state of the call */\n state: string;\n /** User ID of the participant in the call who triggered the event */\n userId?: string\n /** Reason for the call state change */\n reason?: string;\n}\n\nexport interface CameraEnabledResponse {\n enabled: boolean;\n}\n\n/**\n * @interface StreamCallPlugin\n * @description Capacitor plugin for Stream Video calling functionality\n */\nexport interface StreamCallPlugin {\n /**\n * Login to Stream Video service\n * @param {LoginOptions} options - Login configuration\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.login({\n * token: 'your-token',\n * userId: 'user-123',\n * name: 'John Doe',\n * apiKey: 'your-api-key'\n * });\n */\n login(options: LoginOptions): Promise<SuccessResponse>;\n\n /**\n * Logout from Stream Video service\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.logout();\n */\n logout(): Promise<SuccessResponse>;\n\n /**\n * Initiate a call to another user\n * @param {CallOptions} options - Call configuration\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.call({\n * userId: 'user-456',\n * type: 'video',\n * ring: true\n * });\n */\n call(options: CallOptions): Promise<SuccessResponse>;\n\n /**\n * End the current call\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.endCall();\n */\n endCall(): Promise<SuccessResponse>;\n\n /**\n * Enable or disable microphone\n * @param {{ enabled: boolean }} options - Microphone state\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.setMicrophoneEnabled({ enabled: false });\n */\n setMicrophoneEnabled(options: { enabled: boolean }): Promise<SuccessResponse>;\n\n /**\n * Enable or disable camera\n * @param {{ enabled: boolean }} options - Camera state\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.setCameraEnabled({ enabled: false });\n */\n setCameraEnabled(options: { enabled: boolean }): Promise<SuccessResponse>;\n\n /**\n * Add listener for call events\n * @param {'callEvent'} eventName - Name of the event to listen for\n * @param {(event: CallEvent) => void} listenerFunc - Callback function\n * @returns {Promise<{ remove: () => Promise<void> }>} Function to remove listener\n * @example\n * const listener = await StreamCall.addListener('callEvent', (event) => {\n * console.log(`Call ${event.callId} is now ${event.state}`);\n * });\n */\n addListener(\n eventName: 'callEvent',\n listenerFunc: (event: CallEvent) => void,\n ): Promise<{ remove: () => Promise<void> }>;\n\n /**\n * Remove all event listeners\n * @returns {Promise<void>}\n * @example\n * await StreamCall.removeAllListeners();\n */\n removeAllListeners(): Promise<void>;\n\n /**\n * Accept an incoming call\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.acceptCall();\n */\n acceptCall(): Promise<SuccessResponse>;\n\n /**\n * Reject an incoming call\n * @returns {Promise<SuccessResponse>} Success status\n * @example\n * await StreamCall.rejectCall();\n */\n rejectCall(): Promise<SuccessResponse>;\n isCameraEnabled(): Promise<CameraEnabledResponse>;\n}\n"]}
|
package/dist/esm/web.d.ts
CHANGED
|
@@ -11,11 +11,21 @@ export declare class StreamCallWeb extends WebPlugin implements StreamCallPlugin
|
|
|
11
11
|
private audioBindings;
|
|
12
12
|
private participantJoinedListener?;
|
|
13
13
|
private participantLeftListener?;
|
|
14
|
+
private participantResponses;
|
|
15
|
+
private callMembersExpected;
|
|
14
16
|
private setupCallRingListener;
|
|
17
|
+
private setupCallEventListeners;
|
|
15
18
|
private ringCallback;
|
|
16
19
|
private setupParticipantListener;
|
|
17
20
|
private setupParticipantVideo;
|
|
18
21
|
private setupParticipantAudio;
|
|
22
|
+
private callSessionStartedCallback;
|
|
23
|
+
private callRejectedCallback;
|
|
24
|
+
private callAcceptedCallback;
|
|
25
|
+
private callMissedCallback;
|
|
26
|
+
private callStates;
|
|
27
|
+
private checkCallTimeout;
|
|
28
|
+
private checkAllParticipantsResponded;
|
|
19
29
|
private cleanupCall;
|
|
20
30
|
login(options: LoginOptions): Promise<SuccessResponse>;
|
|
21
31
|
logout(): Promise<SuccessResponse>;
|
package/dist/esm/web.js
CHANGED
|
@@ -5,6 +5,8 @@ export class StreamCallWeb extends WebPlugin {
|
|
|
5
5
|
super(...arguments);
|
|
6
6
|
this.videoBindings = new Map();
|
|
7
7
|
this.audioBindings = new Map();
|
|
8
|
+
this.participantResponses = new Map();
|
|
9
|
+
this.callMembersExpected = new Map();
|
|
8
10
|
this.ringCallback = (event) => {
|
|
9
11
|
var _a, _b;
|
|
10
12
|
console.log('Call ringing', event, this.currentCall);
|
|
@@ -12,7 +14,10 @@ export class StreamCallWeb extends WebPlugin {
|
|
|
12
14
|
if (!this.currentCall) {
|
|
13
15
|
console.log('Creating new call', event.call.id);
|
|
14
16
|
this.currentCall = (_a = this.client) === null || _a === void 0 ? void 0 : _a.call(event.call.type, event.call.id);
|
|
17
|
+
// this.currentActiveCallId = this.currentCall?.cid;
|
|
15
18
|
this.notifyListeners('callEvent', { callId: event.call.id, state: CallingState.RINGING });
|
|
19
|
+
// Clear previous responses when a new call starts
|
|
20
|
+
this.participantResponses.clear();
|
|
16
21
|
}
|
|
17
22
|
if (this.currentCall) {
|
|
18
23
|
console.log('Call found', this.currentCall.id);
|
|
@@ -21,6 +26,7 @@ export class StreamCallWeb extends WebPlugin {
|
|
|
21
26
|
console.log('Call state', s);
|
|
22
27
|
if (s === CallingState.JOINED) {
|
|
23
28
|
this.setupParticipantListener();
|
|
29
|
+
this.setupCallEventListeners();
|
|
24
30
|
}
|
|
25
31
|
else if (s === CallingState.LEFT || s === CallingState.RECONNECTING_FAILED) {
|
|
26
32
|
this.cleanupCall();
|
|
@@ -34,11 +40,110 @@ export class StreamCallWeb extends WebPlugin {
|
|
|
34
40
|
});
|
|
35
41
|
}
|
|
36
42
|
};
|
|
43
|
+
this.callSessionStartedCallback = (event) => {
|
|
44
|
+
console.log('Call created (session started)', event);
|
|
45
|
+
if (event.call && event.call.session && event.call.session.participants) {
|
|
46
|
+
// Store the number of expected participants for this call
|
|
47
|
+
const callCid = event.call.cid;
|
|
48
|
+
const memberCount = event.call.session.participants.length;
|
|
49
|
+
console.log(`Call ${callCid} created with ${memberCount} members`);
|
|
50
|
+
this.callMembersExpected.set(callCid, memberCount);
|
|
51
|
+
// Store call members in callStates
|
|
52
|
+
this.callStates.set(callCid, {
|
|
53
|
+
members: event.call.session.participants.map(p => { var _a; return ({ user_id: ((_a = p.user) === null || _a === void 0 ? void 0 : _a.id) || '' }); }),
|
|
54
|
+
participantResponses: new Map(),
|
|
55
|
+
expectedMemberCount: memberCount,
|
|
56
|
+
createdAt: new Date(),
|
|
57
|
+
});
|
|
58
|
+
// Start a timeout task that runs every second
|
|
59
|
+
const timeoutTask = setInterval(() => this.checkCallTimeout(callCid), 1000);
|
|
60
|
+
// Update the callState with the timeout task
|
|
61
|
+
const callState = this.callStates.get(callCid);
|
|
62
|
+
if (callState) {
|
|
63
|
+
callState.timer = timeoutTask;
|
|
64
|
+
this.callStates.set(callCid, callState);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
this.callRejectedCallback = (event) => {
|
|
69
|
+
console.log('Call rejected', event);
|
|
70
|
+
if (event.user && event.user.id) {
|
|
71
|
+
this.participantResponses.set(event.user.id, 'rejected');
|
|
72
|
+
// Update the combined callStates map
|
|
73
|
+
const callState = this.callStates.get(event.call_cid);
|
|
74
|
+
if (callState) {
|
|
75
|
+
callState.participantResponses.set(event.user.id, 'rejected');
|
|
76
|
+
this.callStates.set(event.call_cid, callState);
|
|
77
|
+
}
|
|
78
|
+
this.notifyListeners('callEvent', {
|
|
79
|
+
callId: event.call_cid,
|
|
80
|
+
state: 'rejected',
|
|
81
|
+
userId: event.user.id
|
|
82
|
+
});
|
|
83
|
+
this.checkAllParticipantsResponded();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
this.callAcceptedCallback = (event) => {
|
|
87
|
+
console.log('Call accepted', event);
|
|
88
|
+
if (event.user && event.user.id) {
|
|
89
|
+
this.participantResponses.set(event.user.id, 'accepted');
|
|
90
|
+
// Update the combined callStates map
|
|
91
|
+
const callState = this.callStates.get(event.call_cid);
|
|
92
|
+
if (callState) {
|
|
93
|
+
callState.participantResponses.set(event.user.id, 'accepted');
|
|
94
|
+
// If someone accepted, clear the timer as we don't need to check anymore
|
|
95
|
+
if (callState.timer) {
|
|
96
|
+
clearInterval(callState.timer);
|
|
97
|
+
callState.timer = undefined;
|
|
98
|
+
}
|
|
99
|
+
this.callStates.set(event.call_cid, callState);
|
|
100
|
+
}
|
|
101
|
+
this.notifyListeners('callEvent', {
|
|
102
|
+
callId: event.call_cid,
|
|
103
|
+
state: 'accepted',
|
|
104
|
+
userId: event.user.id
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
this.callMissedCallback = (event) => {
|
|
109
|
+
console.log('Call missed', event);
|
|
110
|
+
if (event.user && event.user.id) {
|
|
111
|
+
this.participantResponses.set(event.user.id, 'missed');
|
|
112
|
+
// Update the combined callStates map
|
|
113
|
+
const callState = this.callStates.get(event.call_cid);
|
|
114
|
+
if (callState) {
|
|
115
|
+
callState.participantResponses.set(event.user.id, 'missed');
|
|
116
|
+
this.callStates.set(event.call_cid, callState);
|
|
117
|
+
}
|
|
118
|
+
this.notifyListeners('callEvent', {
|
|
119
|
+
callId: event.call_cid,
|
|
120
|
+
state: 'missed',
|
|
121
|
+
userId: event.user.id
|
|
122
|
+
});
|
|
123
|
+
this.checkAllParticipantsResponded();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
// Add a combined map for call states, mirroring the iOS implementation
|
|
127
|
+
this.callStates = new Map();
|
|
37
128
|
}
|
|
129
|
+
// private currentActiveCallId?: string;
|
|
38
130
|
setupCallRingListener() {
|
|
39
|
-
var _a, _b;
|
|
131
|
+
var _a, _b, _c, _d;
|
|
40
132
|
(_a = this.client) === null || _a === void 0 ? void 0 : _a.off('call.ring', this.ringCallback);
|
|
41
|
-
(_b = this.client) === null || _b === void 0 ? void 0 : _b.
|
|
133
|
+
(_b = this.client) === null || _b === void 0 ? void 0 : _b.off('call.session_started', this.callSessionStartedCallback);
|
|
134
|
+
(_c = this.client) === null || _c === void 0 ? void 0 : _c.on('call.ring', this.ringCallback);
|
|
135
|
+
(_d = this.client) === null || _d === void 0 ? void 0 : _d.on('call.session_started', this.callSessionStartedCallback);
|
|
136
|
+
}
|
|
137
|
+
setupCallEventListeners() {
|
|
138
|
+
var _a, _b, _c, _d, _e, _f;
|
|
139
|
+
// Clear previous listeners if any
|
|
140
|
+
(_a = this.client) === null || _a === void 0 ? void 0 : _a.off('call.rejected', this.callRejectedCallback);
|
|
141
|
+
(_b = this.client) === null || _b === void 0 ? void 0 : _b.off('call.accepted', this.callAcceptedCallback);
|
|
142
|
+
(_c = this.client) === null || _c === void 0 ? void 0 : _c.off('call.missed', this.callMissedCallback);
|
|
143
|
+
// Register event listeners
|
|
144
|
+
(_d = this.client) === null || _d === void 0 ? void 0 : _d.on('call.rejected', this.callRejectedCallback);
|
|
145
|
+
(_e = this.client) === null || _e === void 0 ? void 0 : _e.on('call.accepted', this.callAcceptedCallback);
|
|
146
|
+
(_f = this.client) === null || _f === void 0 ? void 0 : _f.on('call.missed', this.callMissedCallback);
|
|
42
147
|
}
|
|
43
148
|
setupParticipantListener() {
|
|
44
149
|
// Subscribe to participant changes
|
|
@@ -55,6 +160,7 @@ export class StreamCallWeb extends WebPlugin {
|
|
|
55
160
|
}
|
|
56
161
|
};
|
|
57
162
|
this.participantLeftListener = (event) => {
|
|
163
|
+
var _a, _b;
|
|
58
164
|
if (this.magicDivId && event.participant) {
|
|
59
165
|
const videoId = `video-${event.participant.sessionId}`;
|
|
60
166
|
const audioId = `audio-${event.participant.sessionId}`;
|
|
@@ -70,10 +176,9 @@ export class StreamCallWeb extends WebPlugin {
|
|
|
70
176
|
if (tracks) {
|
|
71
177
|
tracks.getTracks().forEach((track) => {
|
|
72
178
|
track.stop();
|
|
73
|
-
track.enabled = false;
|
|
74
179
|
});
|
|
75
|
-
videoEl.srcObject = null;
|
|
76
180
|
}
|
|
181
|
+
videoEl.srcObject = null;
|
|
77
182
|
videoEl.remove();
|
|
78
183
|
}
|
|
79
184
|
// Remove audio element
|
|
@@ -88,13 +193,44 @@ export class StreamCallWeb extends WebPlugin {
|
|
|
88
193
|
if (tracks) {
|
|
89
194
|
tracks.getTracks().forEach((track) => {
|
|
90
195
|
track.stop();
|
|
91
|
-
track.enabled = false;
|
|
92
196
|
});
|
|
93
|
-
audioEl.srcObject = null;
|
|
94
197
|
}
|
|
198
|
+
audioEl.srcObject = null;
|
|
95
199
|
audioEl.remove();
|
|
96
200
|
}
|
|
97
201
|
}
|
|
202
|
+
// Check if we're the only participant left in the call
|
|
203
|
+
if (this.currentCall && this.currentCall.state.session) {
|
|
204
|
+
// Get the remaining participants count (we need to subtract 1 as we haven't been removed from the list yet)
|
|
205
|
+
const remainingParticipants = this.currentCall.state.session.participants.length - 1;
|
|
206
|
+
// If we're the only one left, end the call
|
|
207
|
+
if (remainingParticipants <= 1) {
|
|
208
|
+
console.log(`We are left solo in a call. Ending. cID: ${this.currentCall.cid}`);
|
|
209
|
+
// End the call
|
|
210
|
+
this.currentCall.leave();
|
|
211
|
+
// Clean up resources
|
|
212
|
+
const callCid = this.currentCall.cid;
|
|
213
|
+
// Invalidate and remove timer
|
|
214
|
+
const callState = (_a = this.callStates) === null || _a === void 0 ? void 0 : _a.get(callCid);
|
|
215
|
+
if (callState === null || callState === void 0 ? void 0 : callState.timer) {
|
|
216
|
+
clearInterval(callState.timer);
|
|
217
|
+
}
|
|
218
|
+
// Remove from callStates
|
|
219
|
+
(_b = this.callStates) === null || _b === void 0 ? void 0 : _b.delete(callCid);
|
|
220
|
+
// Reset the current call
|
|
221
|
+
this.currentCall = undefined;
|
|
222
|
+
// this.currentActiveCallId = undefined;
|
|
223
|
+
// Clean up
|
|
224
|
+
this.cleanupCall();
|
|
225
|
+
console.log(`Cleaned up resources for ended call: ${callCid}`);
|
|
226
|
+
// Notify that the call has ended
|
|
227
|
+
this.notifyListeners('callEvent', {
|
|
228
|
+
callId: callCid,
|
|
229
|
+
state: 'left',
|
|
230
|
+
reason: 'participant_left'
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
98
234
|
};
|
|
99
235
|
this.currentCall.on('participantJoined', this.participantJoinedListener);
|
|
100
236
|
this.currentCall.on('participantLeft', this.participantLeftListener);
|
|
@@ -139,6 +275,101 @@ export class StreamCallWeb extends WebPlugin {
|
|
|
139
275
|
this.audioBindings.set(id, unbind);
|
|
140
276
|
}
|
|
141
277
|
}
|
|
278
|
+
checkCallTimeout(callCid) {
|
|
279
|
+
const callState = this.callStates.get(callCid);
|
|
280
|
+
if (!callState)
|
|
281
|
+
return;
|
|
282
|
+
// Calculate time elapsed since call creation
|
|
283
|
+
const now = new Date();
|
|
284
|
+
const elapsedSeconds = (now.getTime() - callState.createdAt.getTime()) / 1000;
|
|
285
|
+
// Check if 30 seconds have passed
|
|
286
|
+
if (elapsedSeconds >= 30) {
|
|
287
|
+
console.log(`Call ${callCid} has timed out after ${elapsedSeconds} seconds`);
|
|
288
|
+
// Check if anyone has accepted
|
|
289
|
+
const hasAccepted = Array.from(callState.participantResponses.values())
|
|
290
|
+
.some(response => response === 'accepted');
|
|
291
|
+
if (!hasAccepted) {
|
|
292
|
+
console.log(`No one accepted call ${callCid}, marking all non-responders as missed`);
|
|
293
|
+
// Mark all members who haven't responded as "missed"
|
|
294
|
+
callState.members.forEach(member => {
|
|
295
|
+
if (!callState.participantResponses.has(member.user_id)) {
|
|
296
|
+
callState.participantResponses.set(member.user_id, 'missed');
|
|
297
|
+
this.participantResponses.set(member.user_id, 'missed');
|
|
298
|
+
this.notifyListeners('callEvent', {
|
|
299
|
+
callId: callCid,
|
|
300
|
+
state: 'missed',
|
|
301
|
+
userId: member.user_id
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
// End the call
|
|
306
|
+
if (this.currentCall && this.currentCall.cid === callCid) {
|
|
307
|
+
this.currentCall.leave();
|
|
308
|
+
}
|
|
309
|
+
// Clear the timeout task
|
|
310
|
+
if (callState.timer) {
|
|
311
|
+
clearInterval(callState.timer);
|
|
312
|
+
callState.timer = undefined;
|
|
313
|
+
}
|
|
314
|
+
// Remove from callStates
|
|
315
|
+
this.callStates.delete(callCid);
|
|
316
|
+
// Clean up
|
|
317
|
+
this.cleanupCall();
|
|
318
|
+
// Notify that the call has ended
|
|
319
|
+
this.notifyListeners('callEvent', {
|
|
320
|
+
callId: callCid,
|
|
321
|
+
state: 'ended',
|
|
322
|
+
reason: 'timeout'
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
checkAllParticipantsResponded() {
|
|
328
|
+
if (!this.currentCall)
|
|
329
|
+
return;
|
|
330
|
+
const callCid = this.currentCall.cid;
|
|
331
|
+
const totalParticipants = this.callMembersExpected.get(callCid);
|
|
332
|
+
if (!totalParticipants) {
|
|
333
|
+
console.log(`No expected participant count found for call: ${callCid}`);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
console.log(`Total expected participants: ${totalParticipants}`);
|
|
337
|
+
// Count rejections and misses
|
|
338
|
+
let rejectedOrMissedCount = 0;
|
|
339
|
+
this.participantResponses.forEach(response => {
|
|
340
|
+
if (response === 'rejected' || response === 'missed') {
|
|
341
|
+
rejectedOrMissedCount++;
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
console.log(`Participants responded: ${this.participantResponses.size}/${totalParticipants}`);
|
|
345
|
+
console.log(`Rejected or missed: ${rejectedOrMissedCount}`);
|
|
346
|
+
const allResponded = this.participantResponses.size >= totalParticipants;
|
|
347
|
+
const allRejectedOrMissed = allResponded &&
|
|
348
|
+
Array.from(this.participantResponses.values()).every(response => response === 'rejected' || response === 'missed');
|
|
349
|
+
// If all participants have rejected or missed the call
|
|
350
|
+
if (allResponded && allRejectedOrMissed) {
|
|
351
|
+
console.log('All participants have rejected or missed the call');
|
|
352
|
+
// End the call
|
|
353
|
+
this.currentCall.leave();
|
|
354
|
+
// Clean up the timer if exists in callStates
|
|
355
|
+
const callState = this.callStates.get(callCid);
|
|
356
|
+
if (callState === null || callState === void 0 ? void 0 : callState.timer) {
|
|
357
|
+
clearInterval(callState.timer);
|
|
358
|
+
}
|
|
359
|
+
// Remove from callStates
|
|
360
|
+
this.callStates.delete(callCid);
|
|
361
|
+
// Clear the responses
|
|
362
|
+
this.participantResponses.clear();
|
|
363
|
+
// Clean up
|
|
364
|
+
this.cleanupCall();
|
|
365
|
+
// Notify that the call has ended
|
|
366
|
+
this.notifyListeners('callEvent', {
|
|
367
|
+
callId: callCid,
|
|
368
|
+
state: 'ended',
|
|
369
|
+
reason: 'all_rejected_or_missed'
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
142
373
|
cleanupCall() {
|
|
143
374
|
var _a;
|
|
144
375
|
// First cleanup the call listeners
|
|
@@ -240,10 +471,14 @@ export class StreamCallWeb extends WebPlugin {
|
|
|
240
471
|
}
|
|
241
472
|
const call = this.client.call(options.type || 'default', crypto.randomUUID());
|
|
242
473
|
const members = options.userIds.map((userId) => ({ user_id: userId }));
|
|
243
|
-
if (this.client.streamClient.userID && options.userIds.includes(this.client.streamClient.userID)) {
|
|
474
|
+
if (this.client.streamClient.userID && !options.userIds.includes(this.client.streamClient.userID)) {
|
|
244
475
|
members.push({ user_id: this.client.streamClient.userID });
|
|
245
476
|
}
|
|
246
477
|
await call.getOrCreate({ data: { members } });
|
|
478
|
+
// Store the expected member count for this call
|
|
479
|
+
// -1, because we don't count the caller themselves
|
|
480
|
+
this.callMembersExpected.set(call.cid, members.length);
|
|
481
|
+
console.log(`Setting expected members for call ${call.cid}: ${members.length}`);
|
|
247
482
|
this.currentCall = call;
|
|
248
483
|
if (options.ring) {
|
|
249
484
|
this.outgoingCall = call.cid;
|
|
@@ -312,7 +547,7 @@ export class StreamCallWeb extends WebPlugin {
|
|
|
312
547
|
console.log('Rejecting call', this.incomingCall);
|
|
313
548
|
const call = this.client.call(this.incomingCall.type, this.incomingCall.id);
|
|
314
549
|
console.log('Leaving call', call);
|
|
315
|
-
await call.
|
|
550
|
+
await call.reject();
|
|
316
551
|
this.incomingCall = undefined;
|
|
317
552
|
console.log('Rejected call', call);
|
|
318
553
|
this.notifyListeners('callEvent', { callId: call.id, state: CallingState.LEFT });
|