@stream-io/video-client 1.30.1 → 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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [1.32.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.31.0...@stream-io/video-client-1.32.0) (2025-09-29)
6
+
7
+ ### Features
8
+
9
+ - **react-native:** reject call when busy ([#1856](https://github.com/GetStream/stream-video-js/issues/1856)) ([b60bc7c](https://github.com/GetStream/stream-video-js/commit/b60bc7cd2dc2e09d52496d7b5cb593cac4b89485))
10
+
11
+ ### Bug Fixes
12
+
13
+ - restore calling state after unrecoverable join fail ([#1935](https://github.com/GetStream/stream-video-js/issues/1935)) ([8ab0168](https://github.com/GetStream/stream-video-js/commit/8ab01680d01cc47f9cf48078634358507f0c109d))
14
+ - send unifiedSessionId in the initial join request ([#1934](https://github.com/GetStream/stream-video-js/issues/1934)) ([e6a533d](https://github.com/GetStream/stream-video-js/commit/e6a533d7e926086ac5930ebfb4648dade449d15a))
15
+
16
+ ## [1.31.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.30.1...@stream-io/video-client-1.31.0) (2025-09-17)
17
+
18
+ ### Features
19
+
20
+ - introduce @stream-io/worker-timers ([94c962b](https://github.com/GetStream/stream-video-js/commit/94c962b2c5f731c152771b7803a59664fa925477))
21
+
22
+ ### Bug Fixes
23
+
24
+ - **video-filters:** prevent background tab throttling ([#1920](https://github.com/GetStream/stream-video-js/issues/1920)) ([f93d5cc](https://github.com/GetStream/stream-video-js/commit/f93d5cc5785957c7f181fcaf689ec366df9e646b))
25
+
5
26
  ## [1.30.1](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.30.0...@stream-io/video-client-1.30.1) (2025-09-16)
6
27
 
7
28
  ### Bug Fixes
@@ -7,6 +7,7 @@ import { TwirpFetchTransport, TwirpErrorCode } from '@protobuf-ts/twirp-transpor
7
7
  import { ReplaySubject, combineLatest, BehaviorSubject, shareReplay, map, distinctUntilChanged, startWith, takeWhile, distinctUntilKeyChanged, fromEventPattern, concatMap, merge, from, fromEvent, tap, debounceTime, pairwise, of } from 'rxjs';
8
8
  import { UAParser } from 'ua-parser-js';
9
9
  import { parse, write } from 'sdp-transform';
10
+ import { WorkerTimer } from '@stream-io/worker-timer';
10
11
 
11
12
  /* tslint:disable */
12
13
  /**
@@ -2948,6 +2949,12 @@ class JoinRequest$Type extends MessageType {
2948
2949
  super('stream.video.sfu.event.JoinRequest', [
2949
2950
  { no: 1, name: 'token', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
2950
2951
  { no: 2, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
2952
+ {
2953
+ no: 13,
2954
+ name: 'unified_session_id',
2955
+ kind: 'scalar',
2956
+ T: 9 /*ScalarType.STRING*/,
2957
+ },
2951
2958
  {
2952
2959
  no: 3,
2953
2960
  name: 'subscriber_sdp',
@@ -5757,7 +5764,7 @@ const getSdkVersion = (sdk) => {
5757
5764
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
5758
5765
  };
5759
5766
 
5760
- const version = "1.30.1";
5767
+ const version = "1.32.0";
5761
5768
  const [major, minor, patch] = version.split('.');
5762
5769
  let sdkInfo = {
5763
5770
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -7894,137 +7901,12 @@ function lazy(factory) {
7894
7901
  };
7895
7902
  }
7896
7903
 
7897
- // Do not modify this file manually. Instead, edit worker.ts
7898
- // and the run ./generate-timer-worker.sh
7899
- const timerWorker = {
7900
- src: `const timerIdMapping = new Map();
7901
- self.addEventListener('message', (event) => {
7902
- const request = event.data;
7903
- switch (request.type) {
7904
- case 'setTimeout':
7905
- case 'setInterval':
7906
- timerIdMapping.set(request.id, (request.type === 'setTimeout' ? setTimeout : setInterval)(() => {
7907
- tick(request.id);
7908
- if (request.type === 'setTimeout') {
7909
- timerIdMapping.delete(request.id);
7910
- }
7911
- }, request.timeout));
7912
- break;
7913
- case 'clearTimeout':
7914
- case 'clearInterval':
7915
- (request.type === 'clearTimeout' ? clearTimeout : clearInterval)(timerIdMapping.get(request.id));
7916
- timerIdMapping.delete(request.id);
7917
- break;
7918
- }
7919
- });
7920
- function tick(id) {
7921
- const message = { type: 'tick', id };
7922
- self.postMessage(message);
7923
- }`,
7924
- };
7925
-
7926
- class TimerWorker {
7927
- constructor() {
7928
- this.currentTimerId = 1;
7929
- this.callbacks = new Map();
7930
- this.fallback = false;
7931
- }
7932
- setup({ useTimerWorker = true } = {}) {
7933
- if (!useTimerWorker) {
7934
- this.fallback = true;
7935
- return;
7936
- }
7937
- try {
7938
- const source = timerWorker.src;
7939
- const blob = new Blob([source], {
7940
- type: 'application/javascript; charset=utf-8',
7941
- });
7942
- const script = URL.createObjectURL(blob);
7943
- this.worker = new Worker(script, { name: 'str-timer-worker' });
7944
- this.worker.addEventListener('message', (event) => {
7945
- const { type, id } = event.data;
7946
- if (type === 'tick') {
7947
- this.callbacks.get(id)?.();
7948
- }
7949
- });
7950
- }
7951
- catch (err) {
7952
- getLogger(['timer-worker'])('error', err);
7953
- this.fallback = true;
7954
- }
7955
- }
7956
- destroy() {
7957
- this.callbacks.clear();
7958
- this.worker?.terminate();
7959
- this.worker = undefined;
7960
- this.fallback = false;
7961
- }
7962
- get ready() {
7963
- return this.fallback || Boolean(this.worker);
7964
- }
7965
- setInterval(callback, timeout) {
7966
- return this.setTimer('setInterval', callback, timeout);
7967
- }
7968
- clearInterval(id) {
7969
- this.clearTimer('clearInterval', id);
7970
- }
7971
- setTimeout(callback, timeout) {
7972
- return this.setTimer('setTimeout', callback, timeout);
7973
- }
7974
- clearTimeout(id) {
7975
- this.clearTimer('clearTimeout', id);
7976
- }
7977
- setTimer(type, callback, timeout) {
7978
- if (!this.ready) {
7979
- this.setup();
7980
- }
7981
- if (this.fallback) {
7982
- return (type === 'setTimeout' ? setTimeout : setInterval)(callback, timeout);
7983
- }
7984
- const id = this.getTimerId();
7985
- this.callbacks.set(id, () => {
7986
- callback();
7987
- // Timeouts are one-off operations, so no need to keep callback reference
7988
- // after timer has fired
7989
- if (type === 'setTimeout') {
7990
- this.callbacks.delete(id);
7991
- }
7992
- });
7993
- this.sendMessage({ type, id, timeout });
7994
- return id;
7995
- }
7996
- clearTimer(type, id) {
7997
- if (!id) {
7998
- return;
7999
- }
8000
- if (!this.ready) {
8001
- this.setup();
8002
- }
8003
- if (this.fallback) {
8004
- (type === 'clearTimeout' ? clearTimeout : clearInterval)(id);
8005
- return;
8006
- }
8007
- this.callbacks.delete(id);
8008
- this.sendMessage({ type, id });
8009
- }
8010
- getTimerId() {
8011
- return this.currentTimerId++;
8012
- }
8013
- sendMessage(message) {
8014
- if (!this.worker) {
8015
- throw new Error("Cannot use timer worker before it's set up");
8016
- }
8017
- this.worker.postMessage(message);
8018
- }
8019
- }
8020
7904
  let timerWorkerEnabled = false;
8021
7905
  const enableTimerWorker = () => {
8022
7906
  timerWorkerEnabled = true;
8023
7907
  };
8024
7908
  const getTimers = lazy(() => {
8025
- const instance = new TimerWorker();
8026
- instance.setup({ useTimerWorker: timerWorkerEnabled });
8027
- return instance;
7909
+ return new WorkerTimer({ useWorker: timerWorkerEnabled });
8028
7910
  });
8029
7911
 
8030
7912
  /**
@@ -11993,7 +11875,6 @@ class Call {
11993
11875
  if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
11994
11876
  throw new Error(`Illegal State: call.join() shall be called only once`);
11995
11877
  }
11996
- this.state.setCallingState(CallingState.JOINING);
11997
11878
  // we will count the number of join failures per SFU.
11998
11879
  // once the number of failures reaches 2, we will piggyback on the `migrating_from`
11999
11880
  // field to force the coordinator to provide us another SFU
@@ -12022,8 +11903,6 @@ class Call {
12022
11903
  joinData.migrating_from = sfuId;
12023
11904
  }
12024
11905
  if (attempt === maxJoinRetries - 1) {
12025
- // restore the previous call state if the join-flow fails
12026
- this.state.setCallingState(callingState);
12027
11906
  throw err;
12028
11907
  }
12029
11908
  }
@@ -12083,6 +11962,7 @@ class Call {
12083
11962
  })
12084
11963
  : previousSfuClient;
12085
11964
  this.sfuClient = sfuClient;
11965
+ this.unifiedSessionId ?? (this.unifiedSessionId = sfuClient.sessionId);
12086
11966
  this.dynascaleManager.setSfuClient(sfuClient);
12087
11967
  const clientDetails = await getClientDetails();
12088
11968
  // we don't need to send JoinRequest if we are re-using an existing healthy SFU client
@@ -12106,6 +11986,7 @@ class Call {
12106
11986
  : [];
12107
11987
  try {
12108
11988
  const { callState, fastReconnectDeadlineSeconds, publishOptions } = await sfuClient.join({
11989
+ unifiedSessionId: this.unifiedSessionId,
12109
11990
  subscriberSdp,
12110
11991
  publisherSdp,
12111
11992
  clientDetails,
@@ -12151,6 +12032,7 @@ class Call {
12151
12032
  statsOptions,
12152
12033
  publishOptions: this.currentPublishOptions || [],
12153
12034
  closePreviousInstances: !performingMigration,
12035
+ unifiedSessionId: this.unifiedSessionId,
12154
12036
  });
12155
12037
  }
12156
12038
  // make sure we only track connection timing if we are not calling this method as part of a reconnection flow
@@ -12277,7 +12159,7 @@ class Call {
12277
12159
  * @internal
12278
12160
  */
12279
12161
  this.initPublisherAndSubscriber = (opts) => {
12280
- const { sfuClient, connectionConfig, clientDetails, statsOptions, publishOptions, closePreviousInstances, } = opts;
12162
+ const { sfuClient, connectionConfig, clientDetails, statsOptions, publishOptions, closePreviousInstances, unifiedSessionId, } = opts;
12281
12163
  const { enable_rtc_stats: enableTracing } = statsOptions;
12282
12164
  if (closePreviousInstances && this.subscriber) {
12283
12165
  this.subscriber.dispose();
@@ -12332,7 +12214,6 @@ class Call {
12332
12214
  this.tracer.setEnabled(enableTracing);
12333
12215
  this.sfuStatsReporter?.stop();
12334
12216
  if (statsOptions?.reporting_interval_ms > 0) {
12335
- this.unifiedSessionId ?? (this.unifiedSessionId = sfuClient.sessionId);
12336
12217
  this.sfuStatsReporter = new SfuStatsReporter(sfuClient, {
12337
12218
  clientDetails,
12338
12219
  options: statsOptions,
@@ -12342,7 +12223,7 @@ class Call {
12342
12223
  camera: this.camera,
12343
12224
  state: this.state,
12344
12225
  tracer: this.tracer,
12345
- unifiedSessionId: this.unifiedSessionId,
12226
+ unifiedSessionId,
12346
12227
  });
12347
12228
  this.sfuStatsReporter.start();
12348
12229
  }
@@ -14664,7 +14545,7 @@ class StreamClient {
14664
14545
  this.getUserAgent = () => {
14665
14546
  if (!this.cachedUserAgent) {
14666
14547
  const { clientAppIdentifier = {} } = this.options;
14667
- const { sdkName = 'js', sdkVersion = "1.30.1", ...extras } = clientAppIdentifier;
14548
+ const { sdkName = 'js', sdkVersion = "1.32.0", ...extras } = clientAppIdentifier;
14668
14549
  this.cachedUserAgent = [
14669
14550
  `stream-video-${sdkName}-v${sdkVersion}`,
14670
14551
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -14853,6 +14734,7 @@ class StreamVideoClient {
14853
14734
  this.effectsRegistered = false;
14854
14735
  this.eventHandlersToUnregister = [];
14855
14736
  this.connectionConcurrencyTag = Symbol('connectionConcurrencyTag');
14737
+ this.rejectCallWhenBusy = false;
14856
14738
  this.registerClientInstance = (apiKey, user) => {
14857
14739
  const instanceKey = getInstanceKey(apiKey, user);
14858
14740
  if (StreamVideoClient._instances.has(instanceKey)) {
@@ -14898,7 +14780,16 @@ class StreamVideoClient {
14898
14780
  let call = this.writeableStateStore.findCall(e.call.type, e.call.id);
14899
14781
  if (call) {
14900
14782
  if (ringing) {
14901
- await call.updateFromRingingEvent(e);
14783
+ if (this.shouldRejectCall(call.cid)) {
14784
+ this.logger('info', `Leaving call with busy reject reason ${call.cid} because user is busy`);
14785
+ // remove the instance from the state store
14786
+ await call.leave();
14787
+ // explicitly reject the call with busy reason as calling state was not ringing before and leave would not call it therefore
14788
+ await call.reject('busy');
14789
+ }
14790
+ else {
14791
+ await call.updateFromRingingEvent(e);
14792
+ }
14902
14793
  }
14903
14794
  else {
14904
14795
  call.state.updateFromCallResponse(e.call);
@@ -14913,11 +14804,19 @@ class StreamVideoClient {
14913
14804
  clientStore: this.writeableStateStore,
14914
14805
  ringing,
14915
14806
  });
14916
- call.state.updateFromCallResponse(e.call);
14917
14807
  if (ringing) {
14918
- await call.get();
14808
+ if (this.shouldRejectCall(call.cid)) {
14809
+ this.logger('info', `Rejecting call ${call.cid} because user is busy`);
14810
+ // call is not in the state store yet, so just reject api is enough
14811
+ await call.reject('busy');
14812
+ }
14813
+ else {
14814
+ await call.updateFromRingingEvent(e);
14815
+ await call.get();
14816
+ }
14919
14817
  }
14920
14818
  else {
14819
+ call.state.updateFromCallResponse(e.call);
14921
14820
  this.writeableStateStore.registerCall(call);
14922
14821
  this.logger('info', `New call created and registered: ${call.cid}`);
14923
14822
  }
@@ -15184,6 +15083,16 @@ class StreamVideoClient {
15184
15083
  this.connectAnonymousUser = async (user, tokenOrProvider) => {
15185
15084
  return withoutConcurrency(this.connectionConcurrencyTag, () => this.streamClient.connectAnonymousUser(user, tokenOrProvider));
15186
15085
  };
15086
+ this.shouldRejectCall = (currentCallId) => {
15087
+ if (!this.rejectCallWhenBusy)
15088
+ return false;
15089
+ const hasOngoingRingingCall = this.state.calls.some((c) => c.cid !== currentCallId &&
15090
+ c.ringing &&
15091
+ c.state.callingState !== CallingState.IDLE &&
15092
+ c.state.callingState !== CallingState.LEFT &&
15093
+ c.state.callingState !== CallingState.RECONNECTING_FAILED);
15094
+ return hasOngoingRingingCall;
15095
+ };
15187
15096
  const apiKey = typeof apiKeyOrArgs === 'string' ? apiKeyOrArgs : apiKeyOrArgs.apiKey;
15188
15097
  const clientOptions = typeof apiKeyOrArgs === 'string' ? opts : apiKeyOrArgs.options;
15189
15098
  if (clientOptions?.enableTimerWorker)
@@ -15191,6 +15100,7 @@ class StreamVideoClient {
15191
15100
  const rootLogger = clientOptions?.logger || logToConsole;
15192
15101
  setLogger(rootLogger, clientOptions?.logLevel || 'warn');
15193
15102
  this.logger = getLogger(['client']);
15103
+ this.rejectCallWhenBusy = clientOptions?.rejectCallWhenBusy ?? false;
15194
15104
  this.streamClient = createCoordinatorClient(apiKey, clientOptions);
15195
15105
  this.writeableStateStore = new StreamVideoWriteableStateStore();
15196
15106
  this.readOnlyStateStore = new StreamVideoReadOnlyStateStore(this.writeableStateStore);