@stream-io/video-client 1.11.10 → 1.11.12

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/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [1.11.12](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.11...@stream-io/video-client-1.11.12) (2024-12-03)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * handle timeout on SFU WS connections ([#1600](https://github.com/GetStream/stream-video-js/issues/1600)) ([5f2db7b](https://github.com/GetStream/stream-video-js/commit/5f2db7bd5cfdf57cdc04d6a6ed752f43e5b06657))
11
+
12
+ ## [1.11.11](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.10...@stream-io/video-client-1.11.11) (2024-11-29)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * revert [#1604](https://github.com/GetStream/stream-video-js/issues/1604) ([#1607](https://github.com/GetStream/stream-video-js/issues/1607)) ([567e4fb](https://github.com/GetStream/stream-video-js/commit/567e4fb309509b6b0d814826856d0a15efe16271))
18
+
5
19
  ## [1.11.10](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.9...@stream-io/video-client-1.11.10) (2024-11-28)
6
20
 
7
21
 
@@ -3297,7 +3297,7 @@ const retryable = async (rpc, signal) => {
3297
3297
  return result;
3298
3298
  };
3299
3299
 
3300
- const version = "1.11.10";
3300
+ const version = "1.11.12";
3301
3301
  const [major, minor, patch] = version.split('.');
3302
3302
  let sdkInfo = {
3303
3303
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -6374,6 +6374,32 @@ const createWebSocketSignalChannel = (opts) => {
6374
6374
  return ws;
6375
6375
  };
6376
6376
 
6377
+ /**
6378
+ * Saving a long-lived reference to a promise that can reject can be unsafe,
6379
+ * since rejecting the promise causes an unhandled rejection error (even if the
6380
+ * rejection is handled everywhere promise result is expected).
6381
+ *
6382
+ * To avoid that, we add both resolution and rejection handlers to the promise.
6383
+ * That way, the saved promise never rejects. A callback is provided as return
6384
+ * value to build a *new* promise, that resolves and rejects along with
6385
+ * the original promise.
6386
+ * @param promise Promise to wrap, which possibly rejects
6387
+ * @returns Callback to build a new promise, which resolves and rejects along
6388
+ * with the original promise
6389
+ */
6390
+ function makeSafePromise(promise) {
6391
+ let isPending = true;
6392
+ const safePromise = promise
6393
+ .then((result) => ({ status: 'resolved', result }), (error) => ({ status: 'rejected', error }))
6394
+ .finally(() => (isPending = false));
6395
+ const unwrapPromise = () => safePromise.then((fulfillment) => {
6396
+ if (fulfillment.status === 'rejected')
6397
+ throw fulfillment.error;
6398
+ return fulfillment.result;
6399
+ });
6400
+ unwrapPromise.checkPending = () => isPending;
6401
+ return unwrapPromise;
6402
+ }
6377
6403
  /**
6378
6404
  * Creates a new promise with resolvers.
6379
6405
  *
@@ -6426,7 +6452,6 @@ class StreamSfuClient {
6426
6452
  this.isLeaving = false;
6427
6453
  this.pingIntervalInMs = 10 * 1000;
6428
6454
  this.unhealthyTimeoutInMs = this.pingIntervalInMs + 5 * 1000;
6429
- this.restoreWebSocketConcurrencyTag = Symbol('recoverWebSocket');
6430
6455
  /**
6431
6456
  * Promise that resolves when the JoinResponse is received.
6432
6457
  * Rejects after a certain threshold if the response is not received.
@@ -6447,28 +6472,22 @@ class StreamSfuClient {
6447
6472
  },
6448
6473
  });
6449
6474
  this.signalWs.addEventListener('close', this.handleWebSocketClose);
6450
- this.signalWs.addEventListener('error', this.restoreWebSocket);
6451
- this.signalReady = new Promise((resolve) => {
6452
- const onOpen = () => {
6453
- this.signalWs.removeEventListener('open', onOpen);
6454
- resolve(this.signalWs);
6455
- };
6456
- this.signalWs.addEventListener('open', onOpen);
6457
- });
6475
+ this.signalReady = makeSafePromise(Promise.race([
6476
+ new Promise((resolve) => {
6477
+ const onOpen = () => {
6478
+ this.signalWs.removeEventListener('open', onOpen);
6479
+ resolve(this.signalWs);
6480
+ };
6481
+ this.signalWs.addEventListener('open', onOpen);
6482
+ }),
6483
+ new Promise((resolve, reject) => {
6484
+ setTimeout(() => reject(new Error('SFU WS connection timed out')), this.joinResponseTimeout);
6485
+ }),
6486
+ ]));
6458
6487
  };
6459
6488
  this.cleanUpWebSocket = () => {
6460
- this.signalWs.removeEventListener('error', this.restoreWebSocket);
6461
6489
  this.signalWs.removeEventListener('close', this.handleWebSocketClose);
6462
6490
  };
6463
- this.restoreWebSocket = () => {
6464
- withoutConcurrency(this.restoreWebSocketConcurrencyTag, async () => {
6465
- await this.networkAvailableTask?.promise;
6466
- this.logger('debug', 'Restoring SFU WS connection');
6467
- this.cleanUpWebSocket();
6468
- await sleep(500);
6469
- this.createWebSocket();
6470
- }).catch((err) => this.logger('debug', `Can't restore WS connection`, err));
6471
- };
6472
6491
  this.handleWebSocketClose = () => {
6473
6492
  this.signalWs.removeEventListener('close', this.handleWebSocketClose);
6474
6493
  clearInterval(this.keepAliveInterval);
@@ -6562,7 +6581,7 @@ class StreamSfuClient {
6562
6581
  };
6563
6582
  this.join = async (data) => {
6564
6583
  // wait for the signal web socket to be ready before sending "joinRequest"
6565
- await this.signalReady;
6584
+ await this.signalReady();
6566
6585
  if (this.joinResponseTask.isResolved || this.joinResponseTask.isRejected) {
6567
6586
  // we need to lock the RPC requests until we receive a JoinResponse.
6568
6587
  // that's why we have this primitive lock mechanism.
@@ -6617,7 +6636,7 @@ class StreamSfuClient {
6617
6636
  }));
6618
6637
  };
6619
6638
  this.send = async (message) => {
6620
- await this.signalReady; // wait for the signal ws to be open
6639
+ await this.signalReady(); // wait for the signal ws to be open
6621
6640
  const msgJson = SfuRequest.toJson(message);
6622
6641
  if (this.signalWs.readyState !== WebSocket.OPEN) {
6623
6642
  this.logger('debug', 'Signal WS is not open. Skipping message', msgJson);
@@ -8346,7 +8365,7 @@ const getAudioStream = async (trackConstraints) => {
8346
8365
  const constraints = {
8347
8366
  audio: {
8348
8367
  ...audioDeviceConstraints.audio,
8349
- ...normalizeContraints(trackConstraints),
8368
+ ...trackConstraints,
8350
8369
  },
8351
8370
  };
8352
8371
  try {
@@ -8357,6 +8376,13 @@ const getAudioStream = async (trackConstraints) => {
8357
8376
  return await getStream(constraints);
8358
8377
  }
8359
8378
  catch (error) {
8379
+ if (error instanceof DOMException &&
8380
+ error.name === 'OverconstrainedError' &&
8381
+ trackConstraints?.deviceId) {
8382
+ const { deviceId, ...relaxedContraints } = trackConstraints;
8383
+ getLogger(['devices'])('warn', 'Failed to get audio stream, will try again with relaxed contraints', { error, constraints, relaxedContraints });
8384
+ return getAudioStream(relaxedContraints);
8385
+ }
8360
8386
  getLogger(['devices'])('error', 'Failed to get audio stream', {
8361
8387
  error,
8362
8388
  constraints,
@@ -8376,7 +8402,7 @@ const getVideoStream = async (trackConstraints) => {
8376
8402
  const constraints = {
8377
8403
  video: {
8378
8404
  ...videoDeviceConstraints.video,
8379
- ...normalizeContraints(trackConstraints),
8405
+ ...trackConstraints,
8380
8406
  },
8381
8407
  };
8382
8408
  try {
@@ -8387,6 +8413,13 @@ const getVideoStream = async (trackConstraints) => {
8387
8413
  return await getStream(constraints);
8388
8414
  }
8389
8415
  catch (error) {
8416
+ if (error instanceof DOMException &&
8417
+ error.name === 'OverconstrainedError' &&
8418
+ trackConstraints?.deviceId) {
8419
+ const { deviceId, ...relaxedContraints } = trackConstraints;
8420
+ getLogger(['devices'])('warn', 'Failed to get video stream, will try again with relaxed contraints', { error, constraints, relaxedContraints });
8421
+ return getVideoStream(relaxedContraints);
8422
+ }
8390
8423
  getLogger(['devices'])('error', 'Failed to get video stream', {
8391
8424
  error,
8392
8425
  constraints,
@@ -8394,16 +8427,6 @@ const getVideoStream = async (trackConstraints) => {
8394
8427
  throw error;
8395
8428
  }
8396
8429
  };
8397
- function normalizeContraints(constraints) {
8398
- if (constraints?.deviceId === 'default' ||
8399
- (typeof constraints?.deviceId === 'object' &&
8400
- 'exact' in constraints.deviceId &&
8401
- constraints.deviceId.exact === 'default')) {
8402
- const { deviceId, ...contraintsWithoutDeviceId } = constraints;
8403
- return contraintsWithoutDeviceId;
8404
- }
8405
- return constraints;
8406
- }
8407
8430
  /**
8408
8431
  * Prompts the user for a permission to share a screen.
8409
8432
  * If the user grants the permission, a screen sharing stream is returned. Throws otherwise.
@@ -8630,7 +8653,6 @@ class InputMediaDeviceManager {
8630
8653
  }
8631
8654
  catch (error) {
8632
8655
  this.state.setDevice(prevDeviceId);
8633
- await this.applySettingsToStream();
8634
8656
  throw error;
8635
8657
  }
8636
8658
  }
@@ -9831,6 +9853,7 @@ class Call {
9831
9853
  this.reconnectAttempts = 0;
9832
9854
  this.reconnectStrategy = WebsocketReconnectStrategy.UNSPECIFIED;
9833
9855
  this.fastReconnectDeadlineSeconds = 0;
9856
+ this.disconnectionTimeoutSeconds = 0;
9834
9857
  this.lastOfflineTimestamp = 0;
9835
9858
  // maintain the order of publishing tracks to restore them after a reconnection
9836
9859
  // it shouldn't contain duplicates
@@ -10347,6 +10370,10 @@ class Call {
10347
10370
  */
10348
10371
  this.handleSfuSignalClose = (sfuClient) => {
10349
10372
  this.logger('debug', '[Reconnect] SFU signal connection closed');
10373
+ // SFU WS closed before we finished current join, no need to schedule reconnect
10374
+ // because join operation will fail
10375
+ if (this.state.callingState === CallingState.JOINING)
10376
+ return;
10350
10377
  // normal close, no need to reconnect
10351
10378
  if (sfuClient.isLeaving)
10352
10379
  return;
@@ -10362,10 +10389,21 @@ class Call {
10362
10389
  * @param strategy the reconnection strategy to use.
10363
10390
  */
10364
10391
  this.reconnect = async (strategy) => {
10392
+ if (this.state.callingState === CallingState.RECONNECTING ||
10393
+ this.state.callingState === CallingState.RECONNECTING_FAILED)
10394
+ return;
10365
10395
  return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
10366
10396
  this.logger('info', `[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[strategy]}`);
10397
+ let reconnectStartTime = Date.now();
10367
10398
  this.reconnectStrategy = strategy;
10368
10399
  do {
10400
+ if (this.disconnectionTimeoutSeconds > 0 &&
10401
+ (Date.now() - reconnectStartTime) / 1000 >
10402
+ this.disconnectionTimeoutSeconds) {
10403
+ this.logger('warn', '[Reconnect] Stopping reconnection attempts after reaching disconnection timeout');
10404
+ this.state.setCallingState(CallingState.RECONNECTING_FAILED);
10405
+ return;
10406
+ }
10369
10407
  // we don't increment reconnect attempts for the FAST strategy.
10370
10408
  if (this.reconnectStrategy !== WebsocketReconnectStrategy.FAST) {
10371
10409
  this.reconnectAttempts++;
@@ -10465,7 +10503,7 @@ class Call {
10465
10503
  const currentPublisher = this.publisher;
10466
10504
  currentSubscriber?.detachEventHandlers();
10467
10505
  currentPublisher?.detachEventHandlers();
10468
- const migrationTask = currentSfuClient.enterMigration();
10506
+ const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
10469
10507
  try {
10470
10508
  const currentSfu = currentSfuClient.edgeName;
10471
10509
  await this.join({ ...this.joinCallData, migrating_from: currentSfu });
@@ -10481,7 +10519,7 @@ class Call {
10481
10519
  // Wait for the migration to complete, then close the previous SFU client
10482
10520
  // and the peer connection instances. In case of failure, the migration
10483
10521
  // task would throw an error and REJOIN would be attempted.
10484
- await migrationTask;
10522
+ await migrationTask();
10485
10523
  // in MIGRATE, we can consider the call as joined only after
10486
10524
  // `participantMigrationComplete` event is received, signaled by
10487
10525
  // the `migrationTask`
@@ -11337,6 +11375,14 @@ class Call {
11337
11375
  this.dynascaleManager.setVideoTrackSubscriptionOverrides(enabled ? undefined : { enabled: false });
11338
11376
  this.dynascaleManager.applyTrackSubscriptions();
11339
11377
  };
11378
+ /**
11379
+ * Sets the maximum amount of time a user can remain waiting for a reconnect
11380
+ * after a network disruption
11381
+ * @param timeoutSeconds Timeout in seconds, or 0 to keep reconnecting indefinetely
11382
+ */
11383
+ this.setDisconnectionTimeout = (timeoutSeconds) => {
11384
+ this.disconnectionTimeoutSeconds = timeoutSeconds;
11385
+ };
11340
11386
  this.type = type;
11341
11387
  this.id = id;
11342
11388
  this.cid = `${type}:${id}`;
@@ -11492,33 +11538,6 @@ class Call {
11492
11538
 
11493
11539
  var https = null;
11494
11540
 
11495
- /**
11496
- * Saving a long-lived reference to a promise that can reject can be unsafe,
11497
- * since rejecting the promise causes an unhandled rejection error (even if the
11498
- * rejection is handled everywhere promise result is expected).
11499
- *
11500
- * To avoid that, we add both resolution and rejection handlers to the promise.
11501
- * That way, the saved promise never rejects. A callback is provided as return
11502
- * value to build a *new* promise, that resolves and rejects along with
11503
- * the original promise.
11504
- * @param promise Promise to wrap, which possibly rejects
11505
- * @returns Callback to build a new promise, which resolves and rejects along
11506
- * with the original promise
11507
- */
11508
- function makeSafePromise(promise) {
11509
- let isPending = true;
11510
- const safePromise = promise
11511
- .then((result) => ({ status: 'resolved', result }), (error) => ({ status: 'rejected', error }))
11512
- .finally(() => (isPending = false));
11513
- const unwrapPromise = () => safePromise.then((fulfillment) => {
11514
- if (fulfillment.status === 'rejected')
11515
- throw fulfillment.error;
11516
- return fulfillment.result;
11517
- });
11518
- unwrapPromise.checkPending = () => isPending;
11519
- return unwrapPromise;
11520
- }
11521
-
11522
11541
  /**
11523
11542
  * StableWSConnection - A WS connection that reconnects upon failure.
11524
11543
  * - the browser will sometimes report that you're online or offline
@@ -12617,7 +12636,7 @@ class StreamClient {
12617
12636
  return await this.wsConnection.connect(this.defaultWSTimeout);
12618
12637
  };
12619
12638
  this.getUserAgent = () => {
12620
- const version = "1.11.10";
12639
+ const version = "1.11.12";
12621
12640
  return (this.userAgent ||
12622
12641
  `stream-video-javascript-client-${this.node ? 'node' : 'browser'}-${version}`);
12623
12642
  };