@stream-io/video-client 1.27.3 → 1.27.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/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.5](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.27.4...@stream-io/video-client-1.27.5) (2025-08-15)
6
+
7
+ ### Bug Fixes
8
+
9
+ - synchronize ring events ([#1888](https://github.com/GetStream/stream-video-js/issues/1888)) ([0951e6d](https://github.com/GetStream/stream-video-js/commit/0951e6d4c825806937d6bdc548df9f186c531466))
10
+
11
+ ## [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)
12
+
13
+ ### Bug Fixes
14
+
15
+ - expose isSupportedBrowser() utility ([#1859](https://github.com/GetStream/stream-video-js/issues/1859)) ([f51a434](https://github.com/GetStream/stream-video-js/commit/f51a4341f57407210ab2e9ba57f41818ddbd7ed9))
16
+
5
17
  ## [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
18
 
7
19
  ### Bug Fixes
@@ -4300,13 +4300,6 @@ class StreamVideoWriteableStateStore {
4300
4300
  */
4301
4301
  class StreamVideoReadOnlyStateStore {
4302
4302
  constructor(store) {
4303
- /**
4304
- * This method allows you the get the current value of a state variable.
4305
- *
4306
- * @param observable the observable to get the current value of.
4307
- * @returns the current value of the observable.
4308
- */
4309
- this.getCurrentValue = getCurrentValue;
4310
4303
  // convert and expose subjects as observables
4311
4304
  this.connectedUser$ = store.connectedUserSubject.asObservable();
4312
4305
  this.calls$ = store.callsSubject.asObservable();
@@ -5643,6 +5636,151 @@ const getSdkVersion = (sdk) => {
5643
5636
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
5644
5637
  };
5645
5638
 
5639
+ const version = "1.27.5";
5640
+ const [major, minor, patch] = version.split('.');
5641
+ let sdkInfo = {
5642
+ type: SdkType.PLAIN_JAVASCRIPT,
5643
+ major,
5644
+ minor,
5645
+ patch,
5646
+ };
5647
+ let osInfo;
5648
+ let deviceInfo;
5649
+ let webRtcInfo;
5650
+ let deviceState = { oneofKind: undefined };
5651
+ const setSdkInfo = (info) => {
5652
+ sdkInfo = info;
5653
+ };
5654
+ const getSdkInfo = () => {
5655
+ return sdkInfo;
5656
+ };
5657
+ const setOSInfo = (info) => {
5658
+ osInfo = info;
5659
+ };
5660
+ const setDeviceInfo = (info) => {
5661
+ deviceInfo = info;
5662
+ };
5663
+ const getWebRTCInfo = () => {
5664
+ return webRtcInfo;
5665
+ };
5666
+ const setWebRTCInfo = (info) => {
5667
+ webRtcInfo = info;
5668
+ };
5669
+ const setThermalState = (state) => {
5670
+ if (!osInfo) {
5671
+ deviceState = { oneofKind: undefined };
5672
+ return;
5673
+ }
5674
+ if (osInfo.name === 'android') {
5675
+ const thermalState = AndroidThermalState[state] ||
5676
+ AndroidThermalState.UNSPECIFIED;
5677
+ deviceState = {
5678
+ oneofKind: 'android',
5679
+ android: {
5680
+ thermalState,
5681
+ isPowerSaverMode: deviceState?.oneofKind === 'android' &&
5682
+ deviceState.android.isPowerSaverMode,
5683
+ },
5684
+ };
5685
+ }
5686
+ if (osInfo.name.toLowerCase() === 'ios') {
5687
+ const thermalState = AppleThermalState[state] ||
5688
+ AppleThermalState.UNSPECIFIED;
5689
+ deviceState = {
5690
+ oneofKind: 'apple',
5691
+ apple: {
5692
+ thermalState,
5693
+ isLowPowerModeEnabled: deviceState?.oneofKind === 'apple' &&
5694
+ deviceState.apple.isLowPowerModeEnabled,
5695
+ },
5696
+ };
5697
+ }
5698
+ };
5699
+ const setPowerState = (powerMode) => {
5700
+ if (!osInfo) {
5701
+ deviceState = { oneofKind: undefined };
5702
+ return;
5703
+ }
5704
+ if (osInfo.name === 'android') {
5705
+ deviceState = {
5706
+ oneofKind: 'android',
5707
+ android: {
5708
+ thermalState: deviceState?.oneofKind === 'android'
5709
+ ? deviceState.android.thermalState
5710
+ : AndroidThermalState.UNSPECIFIED,
5711
+ isPowerSaverMode: powerMode,
5712
+ },
5713
+ };
5714
+ }
5715
+ if (osInfo.name.toLowerCase() === 'ios') {
5716
+ deviceState = {
5717
+ oneofKind: 'apple',
5718
+ apple: {
5719
+ thermalState: deviceState?.oneofKind === 'apple'
5720
+ ? deviceState.apple.thermalState
5721
+ : AppleThermalState.UNSPECIFIED,
5722
+ isLowPowerModeEnabled: powerMode,
5723
+ },
5724
+ };
5725
+ }
5726
+ };
5727
+ const getDeviceState = () => {
5728
+ return deviceState;
5729
+ };
5730
+ const getClientDetails = async () => {
5731
+ if (isReactNative()) {
5732
+ // Since RN doesn't support web, sharing browser info is not required
5733
+ return {
5734
+ sdk: sdkInfo,
5735
+ os: osInfo,
5736
+ device: deviceInfo,
5737
+ };
5738
+ }
5739
+ // @ts-expect-error - userAgentData is not yet in the TS types
5740
+ const userAgentDataApi = navigator.userAgentData;
5741
+ let userAgentData;
5742
+ if (userAgentDataApi && userAgentDataApi.getHighEntropyValues) {
5743
+ try {
5744
+ userAgentData = await userAgentDataApi.getHighEntropyValues([
5745
+ 'platform',
5746
+ 'platformVersion',
5747
+ 'fullVersionList',
5748
+ ]);
5749
+ }
5750
+ catch {
5751
+ // Ignore the error
5752
+ }
5753
+ }
5754
+ const userAgent = new UAParser(navigator.userAgent);
5755
+ const { browser, os, device, cpu } = userAgent.getResult();
5756
+ // If userAgentData is available, it means we are in a modern Chromium browser,
5757
+ // and we can use it to get more accurate browser information.
5758
+ // We hook into the fullVersionList to find the browser name and version and
5759
+ // eventually detect exotic browsers like Samsung Internet, AVG Secure Browser, etc.
5760
+ // who by default they identify themselves as "Chromium" in the user agent string.
5761
+ // Eliminates the generic "Chromium" name and "Not)A_Brand" name from the list.
5762
+ // https://wicg.github.io/ua-client-hints/#create-arbitrary-brands-section
5763
+ const uaBrowser = userAgentData?.fullVersionList?.find((v) => !v.brand.includes('Chromium') && !v.brand.match(/[()\-./:;=?_]/g));
5764
+ return {
5765
+ sdk: sdkInfo,
5766
+ browser: {
5767
+ name: uaBrowser?.brand || browser.name || navigator.userAgent,
5768
+ version: uaBrowser?.version || browser.version || '',
5769
+ },
5770
+ os: {
5771
+ name: userAgentData?.platform || os.name || '',
5772
+ version: userAgentData?.platformVersion || os.version || '',
5773
+ architecture: cpu.architecture || '',
5774
+ },
5775
+ device: {
5776
+ name: [device.vendor, device.model, device.type]
5777
+ .filter(Boolean)
5778
+ .join(' '),
5779
+ version: '',
5780
+ },
5781
+ };
5782
+ };
5783
+
5646
5784
  /**
5647
5785
  * Checks whether the current browser is Safari.
5648
5786
  */
@@ -5667,12 +5805,34 @@ const isChrome = () => {
5667
5805
  return false;
5668
5806
  return navigator.userAgent?.includes('Chrome');
5669
5807
  };
5808
+ /**
5809
+ * Checks whether the current browser is among the list of first-class supported browsers.
5810
+ * This includes Chrome, Edge, Firefox, and Safari.
5811
+ *
5812
+ * Although the Stream Video SDK may work in other browsers, these are the ones we officially support.
5813
+ */
5814
+ const isSupportedBrowser = async () => {
5815
+ const { browser } = await getClientDetails();
5816
+ if (!browser)
5817
+ return false; // we aren't running in a browser
5818
+ const name = browser.name.toLowerCase();
5819
+ const [major] = browser.version.split('.');
5820
+ const version = parseInt(major, 10);
5821
+ return ((name.includes('chrome') && version >= 124) ||
5822
+ (name.includes('edge') && version >= 124) ||
5823
+ (name.includes('firefox') && version >= 124) ||
5824
+ (name.includes('safari') && version >= 17) ||
5825
+ (name.includes('webkit') && version >= 605) || // WebView on iOS
5826
+ (name.includes('webview') && version >= 124) // WebView on Android
5827
+ );
5828
+ };
5670
5829
 
5671
5830
  var browsers = /*#__PURE__*/Object.freeze({
5672
5831
  __proto__: null,
5673
5832
  isChrome: isChrome,
5674
5833
  isFirefox: isFirefox,
5675
- isSafari: isSafari
5834
+ isSafari: isSafari,
5835
+ isSupportedBrowser: isSupportedBrowser
5676
5836
  });
5677
5837
 
5678
5838
  /**
@@ -5914,142 +6074,6 @@ const aggregate = (stats) => {
5914
6074
  return report;
5915
6075
  };
5916
6076
 
5917
- const version = "1.27.3";
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
6077
  class SfuStatsReporter {
6054
6078
  constructor(sfuClient, { options, clientDetails, subscriber, publisher, microphone, camera, state, tracer, unifiedSessionId, }) {
6055
6079
  this.logger = getLogger(['SfuStatsReporter']);
@@ -14514,7 +14538,7 @@ class StreamClient {
14514
14538
  this.getUserAgent = () => {
14515
14539
  if (!this.cachedUserAgent) {
14516
14540
  const { clientAppIdentifier = {} } = this.options;
14517
- const { sdkName = 'js', sdkVersion = "1.27.3", ...extras } = clientAppIdentifier;
14541
+ const { sdkName = 'js', sdkVersion = "1.27.5", ...extras } = clientAppIdentifier;
14518
14542
  this.cachedUserAgent = [
14519
14543
  `stream-video-${sdkName}-v${sdkVersion}`,
14520
14544
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -14635,6 +14659,13 @@ class StreamClient {
14635
14659
  const getInstanceKey = (apiKey, user) => {
14636
14660
  return `${apiKey}/${user.id}`;
14637
14661
  };
14662
+ /**
14663
+ * Returns a concurrency tag for call initialization.
14664
+ * @internal
14665
+ *
14666
+ * @param cid the call cid.
14667
+ */
14668
+ const getCallInitConcurrencyTag = (cid) => `call.init-${cid}`;
14638
14669
  /**
14639
14670
  * Utility function to get the client app identifier.
14640
14671
  */
@@ -14703,7 +14734,7 @@ class StreamVideoClient {
14703
14734
  this.registerEffects = () => {
14704
14735
  if (this.effectsRegistered)
14705
14736
  return;
14706
- this.eventHandlersToUnregister.push(this.on('connection.changed', (event) => {
14737
+ this.eventHandlersToUnregister.push(this.on('call.created', (event) => this.initCallFromEvent(event)), this.on('call.ring', (event) => this.initCallFromEvent(event)), this.on('connection.changed', (event) => {
14707
14738
  if (!event.online)
14708
14739
  return;
14709
14740
  const callsToReWatch = this.writeableStateStore.calls
@@ -14720,50 +14751,52 @@ class StreamVideoClient {
14720
14751
  this.logger('error', 'Failed to re-watch calls', err);
14721
14752
  });
14722
14753
  }));
14723
- this.eventHandlersToUnregister.push(this.on('call.created', (event) => {
14724
- const { call, members } = event;
14725
- if (this.state.connectedUser?.id === call.created_by.id) {
14726
- this.logger('warn', 'Received `call.created` sent by the current user');
14727
- return;
14728
- }
14729
- this.logger('info', `New call created and registered: ${call.cid}`);
14730
- const newCall = new Call({
14731
- streamClient: this.streamClient,
14732
- type: call.type,
14733
- id: call.id,
14734
- members,
14735
- clientStore: this.writeableStateStore,
14736
- });
14737
- newCall.state.updateFromCallResponse(call);
14738
- this.writeableStateStore.registerCall(newCall);
14739
- }));
14740
- this.eventHandlersToUnregister.push(this.on('call.ring', async (event) => {
14741
- const { call, members } = event;
14742
- if (this.state.connectedUser?.id === call.created_by.id) {
14743
- this.logger('debug', 'Received `call.ring` sent by the current user so ignoring the event');
14744
- return;
14745
- }
14746
- // if `call.created` was received before `call.ring`.
14747
- // the client already has the call instance and we just need to update the state
14748
- const theCall = this.writeableStateStore.findCall(call.type, call.id);
14749
- if (theCall) {
14750
- await theCall.updateFromRingingEvent(event);
14751
- }
14752
- else {
14753
- // if client doesn't have the call instance, create the instance and fetch the latest state
14754
- // Note: related - we also have onRingingCall method to handle this case from push notifications
14755
- const newCallInstance = new Call({
14754
+ this.effectsRegistered = true;
14755
+ };
14756
+ /**
14757
+ * Initializes a call from a call created or ringing event.
14758
+ * @param e the event.
14759
+ */
14760
+ this.initCallFromEvent = async (e) => {
14761
+ if (this.state.connectedUser?.id === e.call.created_by.id) {
14762
+ this.logger('debug', `Ignoring ${e.type} event sent by the current user`);
14763
+ return;
14764
+ }
14765
+ try {
14766
+ const concurrencyTag = getCallInitConcurrencyTag(e.call_cid);
14767
+ await withoutConcurrency(concurrencyTag, async () => {
14768
+ const ringing = e.type === 'call.ring';
14769
+ let call = this.writeableStateStore.findCall(e.call.type, e.call.id);
14770
+ if (call) {
14771
+ if (ringing) {
14772
+ await call.updateFromRingingEvent(e);
14773
+ }
14774
+ else {
14775
+ call.state.updateFromCallResponse(e.call);
14776
+ }
14777
+ return;
14778
+ }
14779
+ call = new Call({
14756
14780
  streamClient: this.streamClient,
14757
- type: call.type,
14758
- id: call.id,
14759
- members,
14781
+ type: e.call.type,
14782
+ id: e.call.id,
14783
+ members: e.members,
14760
14784
  clientStore: this.writeableStateStore,
14761
- ringing: true,
14785
+ ringing,
14762
14786
  });
14763
- await newCallInstance.get();
14764
- }
14765
- }));
14766
- this.effectsRegistered = true;
14787
+ call.state.updateFromCallResponse(e.call);
14788
+ if (ringing) {
14789
+ await call.get();
14790
+ }
14791
+ else {
14792
+ this.writeableStateStore.registerCall(call);
14793
+ this.logger('info', `New call created and registered: ${call.cid}`);
14794
+ }
14795
+ });
14796
+ }
14797
+ catch (err) {
14798
+ this.logger('error', `Failed to init call from event ${e.type}`, err);
14799
+ }
14767
14800
  };
14768
14801
  /**
14769
14802
  * Connects the given user to the client.
@@ -14863,6 +14896,7 @@ class StreamVideoClient {
14863
14896
  *
14864
14897
  * @param type the type of the call.
14865
14898
  * @param id the id of the call.
14899
+ * @param options additional options for call creation.
14866
14900
  */
14867
14901
  this.call = (type, id, options = {}) => {
14868
14902
  const call = options.reuseInstance
@@ -14993,22 +15027,24 @@ class StreamVideoClient {
14993
15027
  * @returns
14994
15028
  */
14995
15029
  this.onRingingCall = async (call_cid) => {
14996
- // if we find the call and is already ringing, we don't need to create a new call
14997
- // as client would have received the call.ring state because the app had WS alive when receiving push notifications
14998
- let call = this.state.calls.find((c) => c.cid === call_cid && c.ringing);
14999
- if (!call) {
15000
- // if not it means that WS is not alive when receiving the push notifications and we need to fetch the call
15001
- const [callType, callId] = call_cid.split(':');
15002
- call = new Call({
15003
- streamClient: this.streamClient,
15004
- type: callType,
15005
- id: callId,
15006
- clientStore: this.writeableStateStore,
15007
- ringing: true,
15008
- });
15009
- await call.get();
15010
- }
15011
- return call;
15030
+ return withoutConcurrency(getCallInitConcurrencyTag(call_cid), async () => {
15031
+ // if we find the call and is already ringing, we don't need to create a new call
15032
+ // as client would have received the call.ring state because the app had WS alive when receiving push notifications
15033
+ let call = this.state.calls.find((c) => c.cid === call_cid && c.ringing);
15034
+ if (!call) {
15035
+ // if not it means that WS is not alive when receiving the push notifications and we need to fetch the call
15036
+ const [callType, callId] = call_cid.split(':');
15037
+ call = new Call({
15038
+ streamClient: this.streamClient,
15039
+ type: callType,
15040
+ id: callId,
15041
+ clientStore: this.writeableStateStore,
15042
+ ringing: true,
15043
+ });
15044
+ await call.get();
15045
+ }
15046
+ return call;
15047
+ });
15012
15048
  };
15013
15049
  /**
15014
15050
  * Connects the given anonymous user to the client.