@stream-io/video-client 1.27.1 → 1.27.3

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,18 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [1.27.3](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.27.2...@stream-io/video-client-1.27.3) (2025-08-07)
6
+
7
+ ### Bug Fixes
8
+
9
+ - extended telemetry data for the signal websocket ([#1881](https://github.com/GetStream/stream-video-js/issues/1881)) ([984703d](https://github.com/GetStream/stream-video-js/commit/984703dabb8c6189eaf4d6925421568f6d0fd7fc))
10
+
11
+ ## [1.27.2](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.27.1...@stream-io/video-client-1.27.2) (2025-08-05)
12
+
13
+ ### Bug Fixes
14
+
15
+ - improved logging and tracing ([#1874](https://github.com/GetStream/stream-video-js/issues/1874)) ([e450ce2](https://github.com/GetStream/stream-video-js/commit/e450ce2a294d6f80480fcc709591c13d9ede79e4))
16
+
5
17
  ## [1.27.1](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.27.0...@stream-io/video-client-1.27.1) (2025-07-25)
6
18
 
7
19
  ### Bug Fixes
@@ -5914,7 +5914,7 @@ const aggregate = (stats) => {
5914
5914
  return report;
5915
5915
  };
5916
5916
 
5917
- const version = "1.27.1";
5917
+ const version = "1.27.3";
5918
5918
  const [major, minor, patch] = version.split('.');
5919
5919
  let sdkInfo = {
5920
5920
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -7633,19 +7633,22 @@ class Subscriber extends BasePeerConnection {
7633
7633
  }
7634
7634
 
7635
7635
  const createWebSocketSignalChannel = (opts) => {
7636
- const { endpoint, onMessage, tag } = opts;
7636
+ const { endpoint, onMessage, tag, tracer } = opts;
7637
7637
  const logger = getLogger(['SfuClientWS', tag]);
7638
7638
  logger('debug', 'Creating signaling WS channel:', endpoint);
7639
7639
  const ws = new WebSocket(endpoint);
7640
7640
  ws.binaryType = 'arraybuffer'; // do we need this?
7641
7641
  ws.addEventListener('error', (e) => {
7642
7642
  logger('error', 'Signaling WS channel error', e);
7643
+ tracer?.trace('signal.ws.error', e);
7643
7644
  });
7644
7645
  ws.addEventListener('close', (e) => {
7645
7646
  logger('info', 'Signaling WS channel is closed', e);
7647
+ tracer?.trace('signal.ws.close', e);
7646
7648
  });
7647
7649
  ws.addEventListener('open', (e) => {
7648
7650
  logger('info', 'Signaling WS channel is open', e);
7651
+ tracer?.trace('signal.ws.open', e);
7649
7652
  });
7650
7653
  ws.addEventListener('message', (e) => {
7651
7654
  try {
@@ -7655,7 +7658,9 @@ const createWebSocketSignalChannel = (opts) => {
7655
7658
  onMessage(message);
7656
7659
  }
7657
7660
  catch (err) {
7658
- logger('error', 'Failed to decode a message. Check whether the Proto models match.', { event: e, error: err });
7661
+ const message = 'Failed to decode a message. Check whether the Proto models match.';
7662
+ logger('error', message, { event: e, error: err });
7663
+ tracer?.trace('signal.ws.message.error', message);
7659
7664
  }
7660
7665
  });
7661
7666
  return ws;
@@ -7927,6 +7932,7 @@ class StreamSfuClient {
7927
7932
  this.signalWs = createWebSocketSignalChannel({
7928
7933
  tag: this.tag,
7929
7934
  endpoint: `${this.credentials.server.ws_endpoint}?${new URLSearchParams(params).toString()}`,
7935
+ tracer: this.tracer,
7930
7936
  onMessage: (message) => {
7931
7937
  this.lastMessageTimestamp = new Date();
7932
7938
  this.scheduleConnectionCheck();
@@ -7937,9 +7943,13 @@ class StreamSfuClient {
7937
7943
  this.dispatcher.dispatch(message, this.tag);
7938
7944
  },
7939
7945
  });
7946
+ let timeoutId;
7940
7947
  this.signalReady = makeSafePromise(Promise.race([
7941
7948
  new Promise((resolve, reject) => {
7949
+ let didOpen = false;
7942
7950
  const onOpen = () => {
7951
+ didOpen = true;
7952
+ clearTimeout(timeoutId);
7943
7953
  this.signalWs.removeEventListener('open', onOpen);
7944
7954
  resolve(this.signalWs);
7945
7955
  };
@@ -7947,19 +7957,25 @@ class StreamSfuClient {
7947
7957
  this.signalWs.addEventListener('close', (e) => {
7948
7958
  this.handleWebSocketClose(e);
7949
7959
  // Normally, this shouldn't have any effect, because WS should never emit 'close'
7950
- // before emitting 'open'. However, strager things have happened, and we don't
7951
- // want to leave signalReady in pending state.
7952
- reject(new Error(`SFU WS closed or connection can't be established`));
7960
+ // before emitting 'open'. However, stranger things have happened, and we don't
7961
+ // want to leave signalReady in a pending state.
7962
+ const message = didOpen
7963
+ ? `SFU WS closed: ${e.code} ${e.reason}`
7964
+ : `SFU WS connection can't be established: ${e.code} ${e.reason}`;
7965
+ this.tracer?.trace('signal.close', message);
7966
+ clearTimeout(timeoutId);
7967
+ reject(new Error(message));
7953
7968
  });
7954
7969
  }),
7955
7970
  new Promise((resolve, reject) => {
7956
- setTimeout(() => reject(new Error('SFU WS connection timed out')), this.joinResponseTimeout);
7971
+ timeoutId = setTimeout(() => {
7972
+ const message = `SFU WS connection failed to open after ${this.joinResponseTimeout}ms`;
7973
+ this.tracer?.trace('signal.timeout', message);
7974
+ reject(new Error(message));
7975
+ }, this.joinResponseTimeout);
7957
7976
  }),
7958
7977
  ]));
7959
7978
  };
7960
- this.cleanUpWebSocket = () => {
7961
- this.signalWs.removeEventListener('close', this.handleWebSocketClose);
7962
- };
7963
7979
  this.handleWebSocketClose = (e) => {
7964
7980
  this.signalWs.removeEventListener('close', this.handleWebSocketClose);
7965
7981
  getTimers().clearInterval(this.keepAliveInterval);
@@ -7971,7 +7987,7 @@ class StreamSfuClient {
7971
7987
  if (this.signalWs.readyState === WebSocket.OPEN) {
7972
7988
  this.logger('debug', `Closing SFU WS connection: ${code} - ${reason}`);
7973
7989
  this.signalWs.close(code, `js-client: ${reason}`);
7974
- this.cleanUpWebSocket();
7990
+ this.signalWs.removeEventListener('close', this.handleWebSocketClose);
7975
7991
  }
7976
7992
  this.dispose();
7977
7993
  };
@@ -8069,7 +8085,6 @@ class StreamSfuClient {
8069
8085
  const current = this.joinResponseTask;
8070
8086
  let timeoutId = undefined;
8071
8087
  const unsubscribe = this.dispatcher.on('joinResponse', (joinResponse) => {
8072
- this.logger('debug', 'Received joinResponse', joinResponse);
8073
8088
  clearTimeout(timeoutId);
8074
8089
  unsubscribe();
8075
8090
  this.keepAlive();
@@ -8077,7 +8092,9 @@ class StreamSfuClient {
8077
8092
  });
8078
8093
  timeoutId = setTimeout(() => {
8079
8094
  unsubscribe();
8080
- current.reject(new Error('Waiting for "joinResponse" has timed out'));
8095
+ const message = `Waiting for "joinResponse" has timed out after ${this.joinResponseTimeout}ms`;
8096
+ this.tracer?.trace('joinRequestTimeout', message);
8097
+ current.reject(new Error(message));
8081
8098
  }, this.joinResponseTimeout);
8082
8099
  const joinRequest = SfuRequest.create({
8083
8100
  requestPayload: {
@@ -9421,6 +9438,25 @@ const CallTypes = new CallTypesRegistry([
9421
9438
  }),
9422
9439
  ]);
9423
9440
 
9441
+ /**
9442
+ * Deactivates MediaStream (stops and removes tracks) to be later garbage collected
9443
+ *
9444
+ * @param stream MediaStream
9445
+ * @returns void
9446
+ */
9447
+ const disposeOfMediaStream = (stream) => {
9448
+ if (!stream.active)
9449
+ return;
9450
+ stream.getTracks().forEach((track) => {
9451
+ track.stop();
9452
+ });
9453
+ // @ts-expect-error release() is present in react-native-webrtc and must be called to dispose the stream
9454
+ if (typeof stream.release === 'function') {
9455
+ // @ts-expect-error - release() is present in react-native-webrtc
9456
+ stream.release();
9457
+ }
9458
+ };
9459
+
9424
9460
  class BrowserPermission {
9425
9461
  constructor(permission) {
9426
9462
  this.permission = permission;
@@ -9818,24 +9854,6 @@ const deviceIds$ = typeof navigator !== 'undefined' &&
9818
9854
  typeof navigator.mediaDevices !== 'undefined'
9819
9855
  ? getDeviceChangeObserver().pipe(startWith(undefined), concatMap(() => navigator.mediaDevices.enumerateDevices()), shareReplay(1))
9820
9856
  : undefined;
9821
- /**
9822
- * Deactivates MediaStream (stops and removes tracks) to be later garbage collected
9823
- *
9824
- * @param stream MediaStream
9825
- * @returns void
9826
- */
9827
- const disposeOfMediaStream = (stream) => {
9828
- if (!stream.active)
9829
- return;
9830
- stream.getTracks().forEach((track) => {
9831
- track.stop();
9832
- });
9833
- // @ts-expect-error release() is present in react-native-webrtc and must be called to dispose the stream
9834
- if (typeof stream.release === 'function') {
9835
- // @ts-expect-error - release() is present in react-native-webrtc
9836
- stream.release();
9837
- }
9838
- };
9839
9857
  /**
9840
9858
  * Resolves `default` device id into the real device id. Some browsers (notably,
9841
9859
  * Chromium-based) report device with id `default` among audio input and output
@@ -11849,6 +11867,12 @@ class Call {
11849
11867
  }
11850
11868
  catch (err) {
11851
11869
  this.logger('warn', `Failed to join call (${attempt})`, this.cid);
11870
+ if (err instanceof ErrorFromResponse && err.unrecoverable) {
11871
+ // if the error is unrecoverable, we should not retry as that signals
11872
+ // that connectivity is good, but the coordinator doesn't allow the user
11873
+ // to join the call due to some reason (e.g., ended call, expired token...)
11874
+ throw err;
11875
+ }
11852
11876
  const sfuId = this.credentials?.server.edge_name || '';
11853
11877
  const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
11854
11878
  sfuJoinFailures.set(sfuId, failures);
@@ -14360,12 +14384,6 @@ class StreamClient {
14360
14384
  response,
14361
14385
  });
14362
14386
  };
14363
- this._logApiError = (type, url, error) => {
14364
- this.logger('error', `client:${type} - Error - url: ${url}`, {
14365
- url,
14366
- error,
14367
- });
14368
- };
14369
14387
  this.doAxiosRequest = async (type, url, data, options = {}) => {
14370
14388
  if (!options.publicEndpoint) {
14371
14389
  await Promise.all([
@@ -14412,27 +14430,36 @@ class StreamClient {
14412
14430
  }
14413
14431
  this._logApiResponse(type, url, response);
14414
14432
  this.consecutiveFailures = 0;
14415
- return this.handleResponse(response);
14433
+ return response.data;
14416
14434
  }
14417
14435
  catch (e /**TODO: generalize error types */) {
14418
14436
  e.client_request_id = requestConfig.headers?.['x-client-request-id'];
14419
14437
  this.consecutiveFailures += 1;
14420
- if (e.response) {
14421
- this._logApiError(type, url, e.response);
14422
- /** connection_fallback depends on this token expiration logic */
14423
- if (e.response.data.code === KnownCodes.TOKEN_EXPIRED &&
14424
- !this.tokenManager.isStatic()) {
14425
- if (this.consecutiveFailures > 1) {
14426
- await sleep(retryInterval(this.consecutiveFailures));
14427
- }
14428
- await this.tokenManager.loadToken();
14429
- return await this.doAxiosRequest(type, url, data, options);
14438
+ const { response } = e;
14439
+ if (!response || !isErrorResponse(response)) {
14440
+ this.logger('error', `client:${type} url: ${url}`, e);
14441
+ throw e;
14442
+ }
14443
+ const { data: responseData, status } = response;
14444
+ const isTokenExpired = responseData.code === KnownCodes.TOKEN_EXPIRED;
14445
+ if (isTokenExpired && !this.tokenManager.isStatic()) {
14446
+ this.logger('warn', `client:${type}: url: ${url}`, response);
14447
+ if (this.consecutiveFailures > 1) {
14448
+ await sleep(retryInterval(this.consecutiveFailures));
14430
14449
  }
14431
- return this.handleResponse(e.response);
14450
+ // refresh and retry the request
14451
+ await this.tokenManager.loadToken();
14452
+ return await this.doAxiosRequest(type, url, data, options);
14432
14453
  }
14433
14454
  else {
14434
- this._logApiError(type, url, e);
14435
- throw e;
14455
+ this.logger('error', `client:${type} url: ${url}`, response);
14456
+ throw new ErrorFromResponse({
14457
+ message: `Stream error code ${responseData.code}: ${responseData.message}`,
14458
+ code: responseData.code ?? null,
14459
+ unrecoverable: responseData.unrecoverable ?? null,
14460
+ response: response,
14461
+ status: status,
14462
+ });
14436
14463
  }
14437
14464
  }
14438
14465
  };
@@ -14455,23 +14482,6 @@ class StreamClient {
14455
14482
  params,
14456
14483
  });
14457
14484
  };
14458
- this.errorFromResponse = (response) => {
14459
- const { data, status } = response;
14460
- return new ErrorFromResponse({
14461
- message: `Stream error code ${data.code}: ${data.message}`,
14462
- code: data.code ?? null,
14463
- unrecoverable: data.unrecoverable ?? null,
14464
- response: response,
14465
- status: status,
14466
- });
14467
- };
14468
- this.handleResponse = (response) => {
14469
- const data = response.data;
14470
- if (isErrorResponse(response)) {
14471
- throw this.errorFromResponse(response);
14472
- }
14473
- return data;
14474
- };
14475
14485
  this.dispatchEvent = (event) => {
14476
14486
  this.logger('debug', `Dispatching event: ${event.type}`, event);
14477
14487
  if (!this.listeners)
@@ -14504,7 +14514,7 @@ class StreamClient {
14504
14514
  this.getUserAgent = () => {
14505
14515
  if (!this.cachedUserAgent) {
14506
14516
  const { clientAppIdentifier = {} } = this.options;
14507
- const { sdkName = 'js', sdkVersion = "1.27.1", ...extras } = clientAppIdentifier;
14517
+ const { sdkName = 'js', sdkVersion = "1.27.3", ...extras } = clientAppIdentifier;
14508
14518
  this.cachedUserAgent = [
14509
14519
  `stream-video-${sdkName}-v${sdkVersion}`,
14510
14520
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),