@stream-io/video-client 1.31.0 → 1.32.0

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.
@@ -30,6 +30,7 @@ export declare class StreamVideoClient {
30
30
  private eventHandlersToUnregister;
31
31
  private readonly connectionConcurrencyTag;
32
32
  private static _instances;
33
+ private rejectCallWhenBusy;
33
34
  /**
34
35
  * You should create only one instance of `StreamVideoClient`.
35
36
  */
@@ -176,4 +177,5 @@ export declare class StreamVideoClient {
176
177
  * @param tokenOrProvider a token or a function that returns a token.
177
178
  */
178
179
  private connectAnonymousUser;
180
+ private shouldRejectCall;
179
181
  }
@@ -149,6 +149,10 @@ export type StreamClientOptions = Partial<AxiosRequestConfig> & {
149
149
  * @param allErrors all errors.
150
150
  */
151
151
  onConnectUserError?: (lastError: Error, allErrors: Error[]) => void;
152
+ /**
153
+ * When set to true, the incoming calls are rejected when the user is busy in an another call.
154
+ */
155
+ rejectCallWhenBusy?: boolean;
152
156
  };
153
157
  export type ClientAppIdentifier = {
154
158
  sdkName?: 'react' | 'react-native' | 'plain-javascript' | (string & {});
@@ -420,6 +420,14 @@ export interface JoinRequest {
420
420
  * @generated from protobuf field: string session_id = 2;
421
421
  */
422
422
  sessionId: string;
423
+ /**
424
+ * user_session id can change during reconnects, this helps us to
425
+ * identify the user across reconnects and should remain consistent until the user explicitly
426
+ * disconnects, is kicked or the call is ended.
427
+ *
428
+ * @generated from protobuf field: string unified_session_id = 13;
429
+ */
430
+ unifiedSessionId: string;
423
431
  /**
424
432
  * dumb SDP that allow us to extract subscriber's decode codecs
425
433
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stream-io/video-client",
3
- "version": "1.31.0",
3
+ "version": "1.32.0",
4
4
  "main": "dist/index.cjs.js",
5
5
  "module": "dist/index.es.js",
6
6
  "browser": "dist/index.browser.es.js",
package/src/Call.ts CHANGED
@@ -874,8 +874,6 @@ export class Call {
874
874
  throw new Error(`Illegal State: call.join() shall be called only once`);
875
875
  }
876
876
 
877
- this.state.setCallingState(CallingState.JOINING);
878
-
879
877
  // we will count the number of join failures per SFU.
880
878
  // once the number of failures reaches 2, we will piggyback on the `migrating_from`
881
879
  // field to force the coordinator to provide us another SFU
@@ -905,8 +903,6 @@ export class Call {
905
903
  }
906
904
 
907
905
  if (attempt === maxJoinRetries - 1) {
908
- // restore the previous call state if the join-flow fails
909
- this.state.setCallingState(callingState);
910
906
  throw err;
911
907
  }
912
908
  }
@@ -981,6 +977,7 @@ export class Call {
981
977
  })
982
978
  : previousSfuClient;
983
979
  this.sfuClient = sfuClient;
980
+ this.unifiedSessionId ??= sfuClient.sessionId;
984
981
  this.dynascaleManager.setSfuClient(sfuClient);
985
982
 
986
983
  const clientDetails = await getClientDetails();
@@ -1008,6 +1005,7 @@ export class Call {
1008
1005
  try {
1009
1006
  const { callState, fastReconnectDeadlineSeconds, publishOptions } =
1010
1007
  await sfuClient.join({
1008
+ unifiedSessionId: this.unifiedSessionId,
1011
1009
  subscriberSdp,
1012
1010
  publisherSdp,
1013
1011
  clientDetails,
@@ -1061,6 +1059,7 @@ export class Call {
1061
1059
  statsOptions,
1062
1060
  publishOptions: this.currentPublishOptions || [],
1063
1061
  closePreviousInstances: !performingMigration,
1062
+ unifiedSessionId: this.unifiedSessionId,
1064
1063
  });
1065
1064
  }
1066
1065
 
@@ -1222,6 +1221,7 @@ export class Call {
1222
1221
  clientDetails: ClientDetails;
1223
1222
  publishOptions: PublishOption[];
1224
1223
  closePreviousInstances: boolean;
1224
+ unifiedSessionId: string;
1225
1225
  }) => {
1226
1226
  const {
1227
1227
  sfuClient,
@@ -1230,6 +1230,7 @@ export class Call {
1230
1230
  statsOptions,
1231
1231
  publishOptions,
1232
1232
  closePreviousInstances,
1233
+ unifiedSessionId,
1233
1234
  } = opts;
1234
1235
  const { enable_rtc_stats: enableTracing } = statsOptions;
1235
1236
  if (closePreviousInstances && this.subscriber) {
@@ -1288,7 +1289,6 @@ export class Call {
1288
1289
  this.tracer.setEnabled(enableTracing);
1289
1290
  this.sfuStatsReporter?.stop();
1290
1291
  if (statsOptions?.reporting_interval_ms > 0) {
1291
- this.unifiedSessionId ??= sfuClient.sessionId;
1292
1292
  this.sfuStatsReporter = new SfuStatsReporter(sfuClient, {
1293
1293
  clientDetails,
1294
1294
  options: statsOptions,
@@ -1298,7 +1298,7 @@ export class Call {
1298
1298
  camera: this.camera,
1299
1299
  state: this.state,
1300
1300
  tracer: this.tracer,
1301
- unifiedSessionId: this.unifiedSessionId,
1301
+ unifiedSessionId,
1302
1302
  });
1303
1303
  this.sfuStatsReporter.start();
1304
1304
  }
@@ -1,6 +1,7 @@
1
1
  import { Call } from './Call';
2
2
  import { StreamClient } from './coordinator/connection/client';
3
3
  import {
4
+ CallingState,
4
5
  StreamVideoReadOnlyStateStore,
5
6
  StreamVideoWriteableStateStore,
6
7
  } from './store';
@@ -74,6 +75,7 @@ export class StreamVideoClient {
74
75
  );
75
76
 
76
77
  private static _instances = new Map<string, StreamVideoClient>();
78
+ private rejectCallWhenBusy = false;
77
79
 
78
80
  /**
79
81
  * You should create only one instance of `StreamVideoClient`.
@@ -95,6 +97,7 @@ export class StreamVideoClient {
95
97
  setLogger(rootLogger, clientOptions?.logLevel || 'warn');
96
98
 
97
99
  this.logger = getLogger(['client']);
100
+ this.rejectCallWhenBusy = clientOptions?.rejectCallWhenBusy ?? false;
98
101
 
99
102
  this.streamClient = createCoordinatorClient(apiKey, clientOptions);
100
103
 
@@ -206,7 +209,18 @@ export class StreamVideoClient {
206
209
  let call = this.writeableStateStore.findCall(e.call.type, e.call.id);
207
210
  if (call) {
208
211
  if (ringing) {
209
- await call.updateFromRingingEvent(e as CallRingEvent);
212
+ if (this.shouldRejectCall(call.cid)) {
213
+ this.logger(
214
+ 'info',
215
+ `Leaving call with busy reject reason ${call.cid} because user is busy`,
216
+ );
217
+ // remove the instance from the state store
218
+ await call.leave();
219
+ // explicitly reject the call with busy reason as calling state was not ringing before and leave would not call it therefore
220
+ await call.reject('busy');
221
+ } else {
222
+ await call.updateFromRingingEvent(e as CallRingEvent);
223
+ }
210
224
  } else {
211
225
  call.state.updateFromCallResponse(e.call);
212
226
  }
@@ -221,11 +235,21 @@ export class StreamVideoClient {
221
235
  clientStore: this.writeableStateStore,
222
236
  ringing,
223
237
  });
224
- call.state.updateFromCallResponse(e.call);
225
238
 
226
239
  if (ringing) {
227
- await call.get();
240
+ if (this.shouldRejectCall(call.cid)) {
241
+ this.logger(
242
+ 'info',
243
+ `Rejecting call ${call.cid} because user is busy`,
244
+ );
245
+ // call is not in the state store yet, so just reject api is enough
246
+ await call.reject('busy');
247
+ } else {
248
+ await call.updateFromRingingEvent(e as CallRingEvent);
249
+ await call.get();
250
+ }
228
251
  } else {
252
+ call.state.updateFromCallResponse(e.call);
229
253
  this.writeableStateStore.registerCall(call);
230
254
  this.logger('info', `New call created and registered: ${call.cid}`);
231
255
  }
@@ -572,4 +596,19 @@ export class StreamVideoClient {
572
596
  this.streamClient.connectAnonymousUser(user, tokenOrProvider),
573
597
  );
574
598
  };
599
+
600
+ private shouldRejectCall = (currentCallId: string) => {
601
+ if (!this.rejectCallWhenBusy) return false;
602
+
603
+ const hasOngoingRingingCall = this.state.calls.some(
604
+ (c) =>
605
+ c.cid !== currentCallId &&
606
+ c.ringing &&
607
+ c.state.callingState !== CallingState.IDLE &&
608
+ c.state.callingState !== CallingState.LEFT &&
609
+ c.state.callingState !== CallingState.RECONNECTING_FAILED,
610
+ );
611
+
612
+ return hasOngoingRingingCall;
613
+ };
575
614
  }
@@ -216,6 +216,11 @@ export type StreamClientOptions = Partial<AxiosRequestConfig> & {
216
216
  * @param allErrors all errors.
217
217
  */
218
218
  onConnectUserError?: (lastError: Error, allErrors: Error[]) => void;
219
+
220
+ /**
221
+ * When set to true, the incoming calls are rejected when the user is busy in an another call.
222
+ */
223
+ rejectCallWhenBusy?: boolean;
219
224
  };
220
225
 
221
226
  export type ClientAppIdentifier = {
@@ -471,6 +471,14 @@ export interface JoinRequest {
471
471
  * @generated from protobuf field: string session_id = 2;
472
472
  */
473
473
  sessionId: string;
474
+ /**
475
+ * user_session id can change during reconnects, this helps us to
476
+ * identify the user across reconnects and should remain consistent until the user explicitly
477
+ * disconnects, is kicked or the call is ended.
478
+ *
479
+ * @generated from protobuf field: string unified_session_id = 13;
480
+ */
481
+ unifiedSessionId: string;
474
482
  /**
475
483
  * dumb SDP that allow us to extract subscriber's decode codecs
476
484
  *
@@ -1353,6 +1361,12 @@ class JoinRequest$Type extends MessageType<JoinRequest> {
1353
1361
  super('stream.video.sfu.event.JoinRequest', [
1354
1362
  { no: 1, name: 'token', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
1355
1363
  { no: 2, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
1364
+ {
1365
+ no: 13,
1366
+ name: 'unified_session_id',
1367
+ kind: 'scalar',
1368
+ T: 9 /*ScalarType.STRING*/,
1369
+ },
1356
1370
  {
1357
1371
  no: 3,
1358
1372
  name: 'subscriber_sdp',