@stream-io/video-client 1.27.2 → 1.27.4

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.4](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.27.3...@stream-io/video-client-1.27.4) (2025-08-13)
6
+
7
+ ### Bug Fixes
8
+
9
+ - expose isSupportedBrowser() utility ([#1859](https://github.com/GetStream/stream-video-js/issues/1859)) ([f51a434](https://github.com/GetStream/stream-video-js/commit/f51a4341f57407210ab2e9ba57f41818ddbd7ed9))
10
+
11
+ ## [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)
12
+
13
+ ### Bug Fixes
14
+
15
+ - 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))
16
+
5
17
  ## [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)
6
18
 
7
19
  ### Bug Fixes
@@ -5643,6 +5643,151 @@ const getSdkVersion = (sdk) => {
5643
5643
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
5644
5644
  };
5645
5645
 
5646
+ const version = "1.27.4";
5647
+ const [major, minor, patch] = version.split('.');
5648
+ let sdkInfo = {
5649
+ type: SdkType.PLAIN_JAVASCRIPT,
5650
+ major,
5651
+ minor,
5652
+ patch,
5653
+ };
5654
+ let osInfo;
5655
+ let deviceInfo;
5656
+ let webRtcInfo;
5657
+ let deviceState = { oneofKind: undefined };
5658
+ const setSdkInfo = (info) => {
5659
+ sdkInfo = info;
5660
+ };
5661
+ const getSdkInfo = () => {
5662
+ return sdkInfo;
5663
+ };
5664
+ const setOSInfo = (info) => {
5665
+ osInfo = info;
5666
+ };
5667
+ const setDeviceInfo = (info) => {
5668
+ deviceInfo = info;
5669
+ };
5670
+ const getWebRTCInfo = () => {
5671
+ return webRtcInfo;
5672
+ };
5673
+ const setWebRTCInfo = (info) => {
5674
+ webRtcInfo = info;
5675
+ };
5676
+ const setThermalState = (state) => {
5677
+ if (!osInfo) {
5678
+ deviceState = { oneofKind: undefined };
5679
+ return;
5680
+ }
5681
+ if (osInfo.name === 'android') {
5682
+ const thermalState = AndroidThermalState[state] ||
5683
+ AndroidThermalState.UNSPECIFIED;
5684
+ deviceState = {
5685
+ oneofKind: 'android',
5686
+ android: {
5687
+ thermalState,
5688
+ isPowerSaverMode: deviceState?.oneofKind === 'android' &&
5689
+ deviceState.android.isPowerSaverMode,
5690
+ },
5691
+ };
5692
+ }
5693
+ if (osInfo.name.toLowerCase() === 'ios') {
5694
+ const thermalState = AppleThermalState[state] ||
5695
+ AppleThermalState.UNSPECIFIED;
5696
+ deviceState = {
5697
+ oneofKind: 'apple',
5698
+ apple: {
5699
+ thermalState,
5700
+ isLowPowerModeEnabled: deviceState?.oneofKind === 'apple' &&
5701
+ deviceState.apple.isLowPowerModeEnabled,
5702
+ },
5703
+ };
5704
+ }
5705
+ };
5706
+ const setPowerState = (powerMode) => {
5707
+ if (!osInfo) {
5708
+ deviceState = { oneofKind: undefined };
5709
+ return;
5710
+ }
5711
+ if (osInfo.name === 'android') {
5712
+ deviceState = {
5713
+ oneofKind: 'android',
5714
+ android: {
5715
+ thermalState: deviceState?.oneofKind === 'android'
5716
+ ? deviceState.android.thermalState
5717
+ : AndroidThermalState.UNSPECIFIED,
5718
+ isPowerSaverMode: powerMode,
5719
+ },
5720
+ };
5721
+ }
5722
+ if (osInfo.name.toLowerCase() === 'ios') {
5723
+ deviceState = {
5724
+ oneofKind: 'apple',
5725
+ apple: {
5726
+ thermalState: deviceState?.oneofKind === 'apple'
5727
+ ? deviceState.apple.thermalState
5728
+ : AppleThermalState.UNSPECIFIED,
5729
+ isLowPowerModeEnabled: powerMode,
5730
+ },
5731
+ };
5732
+ }
5733
+ };
5734
+ const getDeviceState = () => {
5735
+ return deviceState;
5736
+ };
5737
+ const getClientDetails = async () => {
5738
+ if (isReactNative()) {
5739
+ // Since RN doesn't support web, sharing browser info is not required
5740
+ return {
5741
+ sdk: sdkInfo,
5742
+ os: osInfo,
5743
+ device: deviceInfo,
5744
+ };
5745
+ }
5746
+ // @ts-expect-error - userAgentData is not yet in the TS types
5747
+ const userAgentDataApi = navigator.userAgentData;
5748
+ let userAgentData;
5749
+ if (userAgentDataApi && userAgentDataApi.getHighEntropyValues) {
5750
+ try {
5751
+ userAgentData = await userAgentDataApi.getHighEntropyValues([
5752
+ 'platform',
5753
+ 'platformVersion',
5754
+ 'fullVersionList',
5755
+ ]);
5756
+ }
5757
+ catch {
5758
+ // Ignore the error
5759
+ }
5760
+ }
5761
+ const userAgent = new UAParser(navigator.userAgent);
5762
+ const { browser, os, device, cpu } = userAgent.getResult();
5763
+ // If userAgentData is available, it means we are in a modern Chromium browser,
5764
+ // and we can use it to get more accurate browser information.
5765
+ // We hook into the fullVersionList to find the browser name and version and
5766
+ // eventually detect exotic browsers like Samsung Internet, AVG Secure Browser, etc.
5767
+ // who by default they identify themselves as "Chromium" in the user agent string.
5768
+ // Eliminates the generic "Chromium" name and "Not)A_Brand" name from the list.
5769
+ // https://wicg.github.io/ua-client-hints/#create-arbitrary-brands-section
5770
+ const uaBrowser = userAgentData?.fullVersionList?.find((v) => !v.brand.includes('Chromium') && !v.brand.match(/[()\-./:;=?_]/g));
5771
+ return {
5772
+ sdk: sdkInfo,
5773
+ browser: {
5774
+ name: uaBrowser?.brand || browser.name || navigator.userAgent,
5775
+ version: uaBrowser?.version || browser.version || '',
5776
+ },
5777
+ os: {
5778
+ name: userAgentData?.platform || os.name || '',
5779
+ version: userAgentData?.platformVersion || os.version || '',
5780
+ architecture: cpu.architecture || '',
5781
+ },
5782
+ device: {
5783
+ name: [device.vendor, device.model, device.type]
5784
+ .filter(Boolean)
5785
+ .join(' '),
5786
+ version: '',
5787
+ },
5788
+ };
5789
+ };
5790
+
5646
5791
  /**
5647
5792
  * Checks whether the current browser is Safari.
5648
5793
  */
@@ -5667,12 +5812,34 @@ const isChrome = () => {
5667
5812
  return false;
5668
5813
  return navigator.userAgent?.includes('Chrome');
5669
5814
  };
5815
+ /**
5816
+ * Checks whether the current browser is among the list of first-class supported browsers.
5817
+ * This includes Chrome, Edge, Firefox, and Safari.
5818
+ *
5819
+ * Although the Stream Video SDK may work in other browsers, these are the ones we officially support.
5820
+ */
5821
+ const isSupportedBrowser = async () => {
5822
+ const { browser } = await getClientDetails();
5823
+ if (!browser)
5824
+ return false; // we aren't running in a browser
5825
+ const name = browser.name.toLowerCase();
5826
+ const [major] = browser.version.split('.');
5827
+ const version = parseInt(major, 10);
5828
+ return ((name.includes('chrome') && version >= 124) ||
5829
+ (name.includes('edge') && version >= 124) ||
5830
+ (name.includes('firefox') && version >= 124) ||
5831
+ (name.includes('safari') && version >= 17) ||
5832
+ (name.includes('webkit') && version >= 605) || // WebView on iOS
5833
+ (name.includes('webview') && version >= 124) // WebView on Android
5834
+ );
5835
+ };
5670
5836
 
5671
5837
  var browsers = /*#__PURE__*/Object.freeze({
5672
5838
  __proto__: null,
5673
5839
  isChrome: isChrome,
5674
5840
  isFirefox: isFirefox,
5675
- isSafari: isSafari
5841
+ isSafari: isSafari,
5842
+ isSupportedBrowser: isSupportedBrowser
5676
5843
  });
5677
5844
 
5678
5845
  /**
@@ -5914,142 +6081,6 @@ const aggregate = (stats) => {
5914
6081
  return report;
5915
6082
  };
5916
6083
 
5917
- const version = "1.27.2";
5918
- const [major, minor, patch] = version.split('.');
5919
- let sdkInfo = {
5920
- type: SdkType.PLAIN_JAVASCRIPT,
5921
- major,
5922
- minor,
5923
- patch,
5924
- };
5925
- let osInfo;
5926
- let deviceInfo;
5927
- let webRtcInfo;
5928
- let deviceState = { oneofKind: undefined };
5929
- const setSdkInfo = (info) => {
5930
- sdkInfo = info;
5931
- };
5932
- const getSdkInfo = () => {
5933
- return sdkInfo;
5934
- };
5935
- const setOSInfo = (info) => {
5936
- osInfo = info;
5937
- };
5938
- const setDeviceInfo = (info) => {
5939
- deviceInfo = info;
5940
- };
5941
- const getWebRTCInfo = () => {
5942
- return webRtcInfo;
5943
- };
5944
- const setWebRTCInfo = (info) => {
5945
- webRtcInfo = info;
5946
- };
5947
- const setThermalState = (state) => {
5948
- if (!osInfo) {
5949
- deviceState = { oneofKind: undefined };
5950
- return;
5951
- }
5952
- if (osInfo.name === 'android') {
5953
- const thermalState = AndroidThermalState[state] ||
5954
- AndroidThermalState.UNSPECIFIED;
5955
- deviceState = {
5956
- oneofKind: 'android',
5957
- android: {
5958
- thermalState,
5959
- isPowerSaverMode: deviceState?.oneofKind === 'android' &&
5960
- deviceState.android.isPowerSaverMode,
5961
- },
5962
- };
5963
- }
5964
- if (osInfo.name.toLowerCase() === 'ios') {
5965
- const thermalState = AppleThermalState[state] ||
5966
- AppleThermalState.UNSPECIFIED;
5967
- deviceState = {
5968
- oneofKind: 'apple',
5969
- apple: {
5970
- thermalState,
5971
- isLowPowerModeEnabled: deviceState?.oneofKind === 'apple' &&
5972
- deviceState.apple.isLowPowerModeEnabled,
5973
- },
5974
- };
5975
- }
5976
- };
5977
- const setPowerState = (powerMode) => {
5978
- if (!osInfo) {
5979
- deviceState = { oneofKind: undefined };
5980
- return;
5981
- }
5982
- if (osInfo.name === 'android') {
5983
- deviceState = {
5984
- oneofKind: 'android',
5985
- android: {
5986
- thermalState: deviceState?.oneofKind === 'android'
5987
- ? deviceState.android.thermalState
5988
- : AndroidThermalState.UNSPECIFIED,
5989
- isPowerSaverMode: powerMode,
5990
- },
5991
- };
5992
- }
5993
- if (osInfo.name.toLowerCase() === 'ios') {
5994
- deviceState = {
5995
- oneofKind: 'apple',
5996
- apple: {
5997
- thermalState: deviceState?.oneofKind === 'apple'
5998
- ? deviceState.apple.thermalState
5999
- : AppleThermalState.UNSPECIFIED,
6000
- isLowPowerModeEnabled: powerMode,
6001
- },
6002
- };
6003
- }
6004
- };
6005
- const getDeviceState = () => {
6006
- return deviceState;
6007
- };
6008
- const getClientDetails = async () => {
6009
- if (isReactNative()) {
6010
- // Since RN doesn't support web, sharing browser info is not required
6011
- return {
6012
- sdk: sdkInfo,
6013
- os: osInfo,
6014
- device: deviceInfo,
6015
- };
6016
- }
6017
- // @ts-expect-error - userAgentData is not yet in the TS types
6018
- const userAgentDataApi = navigator.userAgentData;
6019
- let userAgentData;
6020
- if (userAgentDataApi && userAgentDataApi.getHighEntropyValues) {
6021
- try {
6022
- userAgentData = await userAgentDataApi.getHighEntropyValues([
6023
- 'platform',
6024
- 'platformVersion',
6025
- ]);
6026
- }
6027
- catch {
6028
- // Ignore the error
6029
- }
6030
- }
6031
- const userAgent = new UAParser(navigator.userAgent);
6032
- const { browser, os, device, cpu } = userAgent.getResult();
6033
- return {
6034
- sdk: sdkInfo,
6035
- browser: {
6036
- name: browser.name || navigator.userAgent,
6037
- version: browser.version || '',
6038
- },
6039
- os: {
6040
- name: userAgentData?.platform || os.name || '',
6041
- version: userAgentData?.platformVersion || os.version || '',
6042
- architecture: cpu.architecture || '',
6043
- },
6044
- device: {
6045
- name: [device.vendor, device.model, device.type]
6046
- .filter(Boolean)
6047
- .join(' '),
6048
- version: '',
6049
- },
6050
- };
6051
- };
6052
-
6053
6084
  class SfuStatsReporter {
6054
6085
  constructor(sfuClient, { options, clientDetails, subscriber, publisher, microphone, camera, state, tracer, unifiedSessionId, }) {
6055
6086
  this.logger = getLogger(['SfuStatsReporter']);
@@ -7633,19 +7664,22 @@ class Subscriber extends BasePeerConnection {
7633
7664
  }
7634
7665
 
7635
7666
  const createWebSocketSignalChannel = (opts) => {
7636
- const { endpoint, onMessage, tag } = opts;
7667
+ const { endpoint, onMessage, tag, tracer } = opts;
7637
7668
  const logger = getLogger(['SfuClientWS', tag]);
7638
7669
  logger('debug', 'Creating signaling WS channel:', endpoint);
7639
7670
  const ws = new WebSocket(endpoint);
7640
7671
  ws.binaryType = 'arraybuffer'; // do we need this?
7641
7672
  ws.addEventListener('error', (e) => {
7642
7673
  logger('error', 'Signaling WS channel error', e);
7674
+ tracer?.trace('signal.ws.error', e);
7643
7675
  });
7644
7676
  ws.addEventListener('close', (e) => {
7645
7677
  logger('info', 'Signaling WS channel is closed', e);
7678
+ tracer?.trace('signal.ws.close', e);
7646
7679
  });
7647
7680
  ws.addEventListener('open', (e) => {
7648
7681
  logger('info', 'Signaling WS channel is open', e);
7682
+ tracer?.trace('signal.ws.open', e);
7649
7683
  });
7650
7684
  ws.addEventListener('message', (e) => {
7651
7685
  try {
@@ -7655,7 +7689,9 @@ const createWebSocketSignalChannel = (opts) => {
7655
7689
  onMessage(message);
7656
7690
  }
7657
7691
  catch (err) {
7658
- logger('error', 'Failed to decode a message. Check whether the Proto models match.', { event: e, error: err });
7692
+ const message = 'Failed to decode a message. Check whether the Proto models match.';
7693
+ logger('error', message, { event: e, error: err });
7694
+ tracer?.trace('signal.ws.message.error', message);
7659
7695
  }
7660
7696
  });
7661
7697
  return ws;
@@ -7927,6 +7963,7 @@ class StreamSfuClient {
7927
7963
  this.signalWs = createWebSocketSignalChannel({
7928
7964
  tag: this.tag,
7929
7965
  endpoint: `${this.credentials.server.ws_endpoint}?${new URLSearchParams(params).toString()}`,
7966
+ tracer: this.tracer,
7930
7967
  onMessage: (message) => {
7931
7968
  this.lastMessageTimestamp = new Date();
7932
7969
  this.scheduleConnectionCheck();
@@ -7937,9 +7974,13 @@ class StreamSfuClient {
7937
7974
  this.dispatcher.dispatch(message, this.tag);
7938
7975
  },
7939
7976
  });
7977
+ let timeoutId;
7940
7978
  this.signalReady = makeSafePromise(Promise.race([
7941
7979
  new Promise((resolve, reject) => {
7980
+ let didOpen = false;
7942
7981
  const onOpen = () => {
7982
+ didOpen = true;
7983
+ clearTimeout(timeoutId);
7943
7984
  this.signalWs.removeEventListener('open', onOpen);
7944
7985
  resolve(this.signalWs);
7945
7986
  };
@@ -7947,19 +7988,25 @@ class StreamSfuClient {
7947
7988
  this.signalWs.addEventListener('close', (e) => {
7948
7989
  this.handleWebSocketClose(e);
7949
7990
  // 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`));
7991
+ // before emitting 'open'. However, stranger things have happened, and we don't
7992
+ // want to leave signalReady in a pending state.
7993
+ const message = didOpen
7994
+ ? `SFU WS closed: ${e.code} ${e.reason}`
7995
+ : `SFU WS connection can't be established: ${e.code} ${e.reason}`;
7996
+ this.tracer?.trace('signal.close', message);
7997
+ clearTimeout(timeoutId);
7998
+ reject(new Error(message));
7953
7999
  });
7954
8000
  }),
7955
8001
  new Promise((resolve, reject) => {
7956
- setTimeout(() => reject(new Error('SFU WS connection timed out')), this.joinResponseTimeout);
8002
+ timeoutId = setTimeout(() => {
8003
+ const message = `SFU WS connection failed to open after ${this.joinResponseTimeout}ms`;
8004
+ this.tracer?.trace('signal.timeout', message);
8005
+ reject(new Error(message));
8006
+ }, this.joinResponseTimeout);
7957
8007
  }),
7958
8008
  ]));
7959
8009
  };
7960
- this.cleanUpWebSocket = () => {
7961
- this.signalWs.removeEventListener('close', this.handleWebSocketClose);
7962
- };
7963
8010
  this.handleWebSocketClose = (e) => {
7964
8011
  this.signalWs.removeEventListener('close', this.handleWebSocketClose);
7965
8012
  getTimers().clearInterval(this.keepAliveInterval);
@@ -7971,7 +8018,7 @@ class StreamSfuClient {
7971
8018
  if (this.signalWs.readyState === WebSocket.OPEN) {
7972
8019
  this.logger('debug', `Closing SFU WS connection: ${code} - ${reason}`);
7973
8020
  this.signalWs.close(code, `js-client: ${reason}`);
7974
- this.cleanUpWebSocket();
8021
+ this.signalWs.removeEventListener('close', this.handleWebSocketClose);
7975
8022
  }
7976
8023
  this.dispose();
7977
8024
  };
@@ -14498,7 +14545,7 @@ class StreamClient {
14498
14545
  this.getUserAgent = () => {
14499
14546
  if (!this.cachedUserAgent) {
14500
14547
  const { clientAppIdentifier = {} } = this.options;
14501
- const { sdkName = 'js', sdkVersion = "1.27.2", ...extras } = clientAppIdentifier;
14548
+ const { sdkName = 'js', sdkVersion = "1.27.4", ...extras } = clientAppIdentifier;
14502
14549
  this.cachedUserAgent = [
14503
14550
  `stream-video-${sdkName}-v${sdkVersion}`,
14504
14551
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),