@stream-io/video-client 1.39.2 → 1.40.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,19 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [1.40.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.39.3...@stream-io/video-client-1.40.0) (2026-01-09)
6
+
7
+ ### Features
8
+
9
+ - Call Stats Map ([#2025](https://github.com/GetStream/stream-video-js/issues/2025)) ([6c784f0](https://github.com/GetStream/stream-video-js/commit/6c784f0acacce3d23d0f589ff423d6a0d04c1e95))
10
+
11
+ ## [1.39.3](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.39.2...@stream-io/video-client-1.39.3) (2025-12-30)
12
+
13
+ ### Bug Fixes
14
+
15
+ - adjusted shouldRejectCall implementation ([#2072](https://github.com/GetStream/stream-video-js/issues/2072)) ([2107e3d](https://github.com/GetStream/stream-video-js/commit/2107e3db65309664a7797cacae054aeb7a371f4a))
16
+ - **rpc:** Reliable SFU request timeouts ([#2066](https://github.com/GetStream/stream-video-js/issues/2066)) ([f842b74](https://github.com/GetStream/stream-video-js/commit/f842b74109af02c8454f5ff4f6618baac650ed4e))
17
+
5
18
  ## [1.39.2](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.39.1...@stream-io/video-client-1.39.2) (2025-12-23)
6
19
 
7
20
  - upgrade stream dependencies ([#2065](https://github.com/GetStream/stream-video-js/issues/2065)) ([04ca858](https://github.com/GetStream/stream-video-js/commit/04ca858517072f861c1ddae0876f0b425ca658e2))
@@ -3716,7 +3716,35 @@ const withHeaders = (headers) => {
3716
3716
  },
3717
3717
  };
3718
3718
  };
3719
- const withRequestLogger = (logger, level) => {
3719
+ const TIMEOUT_SYMBOL = '@@stream-io/timeout';
3720
+ const withTimeout = (timeoutMs, trace) => {
3721
+ const scheduleTimeout = (methodName) => {
3722
+ const controller = new AbortController();
3723
+ // aborts with specially crafted error that can be reliably recognized by
3724
+ // @protobuf-ts/twirp-transport and our internal retry logic.
3725
+ // https://github.com/timostamm/protobuf-ts/blob/657e64e80009e503e94f608fda423fbcbf4fb5a7/packages/twirp-transport/src/twirp-transport.ts#L102-L107
3726
+ const timeoutId = setTimeout(() => {
3727
+ trace?.(`${methodName}Timeout`, [timeoutMs]);
3728
+ const error = new Error(TIMEOUT_SYMBOL);
3729
+ error.name = 'AbortError';
3730
+ controller.abort(error);
3731
+ }, timeoutMs);
3732
+ return [controller.signal, () => clearTimeout(timeoutId)];
3733
+ };
3734
+ return {
3735
+ interceptUnary(next, method, input, options) {
3736
+ // respect external abort signals if provided
3737
+ if (options.abort)
3738
+ return next(method, input, options);
3739
+ // set up a custom abort signal for the RPC call
3740
+ const [signal, cancel] = scheduleTimeout(method.name);
3741
+ const invocation = next(method, input, { ...options, abort: signal });
3742
+ invocation.then(cancel, cancel);
3743
+ return invocation;
3744
+ },
3745
+ };
3746
+ };
3747
+ const withRequestLogger = (logger, level = 'trace') => {
3720
3748
  return {
3721
3749
  interceptUnary: (next, method, input, options) => {
3722
3750
  const invocation = next(method, input, options);
@@ -3735,18 +3763,21 @@ const withRequestTracer = (trace) => {
3735
3763
  const traceError = (name, input, err) => trace(`${name}OnFailure`, [err, input]);
3736
3764
  return {
3737
3765
  interceptUnary(next, method, input, options) {
3738
- const name = method.name;
3739
- if (exclusions.has(name))
3766
+ const methodName = method.name;
3767
+ if (exclusions.has(methodName))
3740
3768
  return next(method, input, options);
3741
- trace(name, input);
3769
+ const { invocationMeta: { attempt = 0 } = {} } = options;
3770
+ const traceName = attempt === 0 ? methodName : `${methodName}(${attempt})`;
3771
+ trace(traceName, input);
3742
3772
  const unaryCall = next(method, input, options);
3743
3773
  unaryCall.then((invocation) => {
3744
3774
  const response = invocation.response;
3745
3775
  if (response.error)
3746
- traceError(name, input, response.error);
3747
- if (responseInclusions.has(name))
3748
- trace(`${name}Response`, response);
3749
- }, (error) => traceError(name, input, error));
3776
+ traceError(traceName, input, response.error);
3777
+ if (responseInclusions.has(methodName)) {
3778
+ trace(`${traceName}Response`, response);
3779
+ }
3780
+ }, (error) => traceError(methodName, input, error));
3750
3781
  return unaryCall;
3751
3782
  },
3752
3783
  };
@@ -3898,22 +3929,26 @@ const videoLoggerSystem = scopedLogger.createLoggerSystem();
3898
3929
  *
3899
3930
  * @param rpc the closure around the RPC call to execute.
3900
3931
  * @param signal the signal to abort the RPC call and retries loop.
3932
+ * @param maxRetries the maximum number of retries to perform. Defaults to `Number.POSITIVE_INFINITY`.
3901
3933
  */
3902
- const retryable = async (rpc, signal) => {
3934
+ const retryable = async (rpc, signal, maxRetries = Number.POSITIVE_INFINITY) => {
3903
3935
  let attempt = 0;
3904
3936
  let result = undefined;
3905
3937
  do {
3906
3938
  if (attempt > 0)
3907
3939
  await sleep(retryInterval(attempt));
3908
3940
  try {
3909
- result = await rpc();
3941
+ result = await rpc({ attempt });
3910
3942
  }
3911
3943
  catch (err) {
3912
3944
  const isRequestCancelled = err instanceof RpcError &&
3945
+ err.message !== TIMEOUT_SYMBOL &&
3913
3946
  err.code === TwirpErrorCode[TwirpErrorCode.cancelled];
3914
3947
  const isAborted = signal?.aborted ?? false;
3915
3948
  if (isRequestCancelled || isAborted)
3916
3949
  throw err;
3950
+ if (attempt + 1 >= maxRetries)
3951
+ throw err;
3917
3952
  videoLoggerSystem
3918
3953
  .getLogger('sfu-client', { tags: ['rpc'] })
3919
3954
  .debug(`rpc failed (${attempt})`, err);
@@ -6025,7 +6060,7 @@ const getSdkVersion = (sdk) => {
6025
6060
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
6026
6061
  };
6027
6062
 
6028
- const version = "1.39.2";
6063
+ const version = "1.40.0";
6029
6064
  const [major, minor, patch] = version.split('.');
6030
6065
  let sdkInfo = {
6031
6066
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -8263,7 +8298,7 @@ class StreamSfuClient {
8263
8298
  /**
8264
8299
  * Constructs a new SFU client.
8265
8300
  */
8266
- constructor({ dispatcher, credentials, sessionId, cid, tag, joinResponseTimeout = 5000, onSignalClose, streamClient, enableTracing, }) {
8301
+ constructor({ dispatcher, credentials, sessionId, cid, tag, joinResponseTimeout = 5000, rpcRequestTimeout = 5000, onSignalClose, streamClient, enableTracing, }) {
8267
8302
  /**
8268
8303
  * A buffer for ICE Candidates that are received before
8269
8304
  * the Publisher and Subscriber Peer Connections are ready to handle them.
@@ -8392,27 +8427,27 @@ class StreamSfuClient {
8392
8427
  };
8393
8428
  this.updateSubscriptions = async (tracks) => {
8394
8429
  await this.joinTask;
8395
- return retryable(() => this.rpc.updateSubscriptions({ sessionId: this.sessionId, tracks }), this.abortController.signal);
8430
+ return retryable((invocationMeta) => this.rpc.updateSubscriptions({ sessionId: this.sessionId, tracks }, { invocationMeta }), this.abortController.signal);
8396
8431
  };
8397
8432
  this.setPublisher = async (data) => {
8398
8433
  await this.joinTask;
8399
- return retryable(() => this.rpc.setPublisher({ ...data, sessionId: this.sessionId }), this.abortController.signal);
8434
+ return retryable((invocationMeta) => this.rpc.setPublisher({ ...data, sessionId: this.sessionId }, { invocationMeta }), this.abortController.signal, 3);
8400
8435
  };
8401
8436
  this.sendAnswer = async (data) => {
8402
8437
  await this.joinTask;
8403
- return retryable(() => this.rpc.sendAnswer({ ...data, sessionId: this.sessionId }), this.abortController.signal);
8438
+ return retryable((invocationMeta) => this.rpc.sendAnswer({ ...data, sessionId: this.sessionId }, { invocationMeta }), this.abortController.signal);
8404
8439
  };
8405
8440
  this.iceTrickle = async (data) => {
8406
8441
  await this.joinTask;
8407
- return retryable(() => this.rpc.iceTrickle({ ...data, sessionId: this.sessionId }), this.abortController.signal);
8442
+ return retryable((invocationMeta) => this.rpc.iceTrickle({ ...data, sessionId: this.sessionId }, { invocationMeta }), this.abortController.signal);
8408
8443
  };
8409
8444
  this.iceRestart = async (data) => {
8410
8445
  await this.joinTask;
8411
- return retryable(() => this.rpc.iceRestart({ ...data, sessionId: this.sessionId }), this.abortController.signal);
8446
+ return retryable((invocationMeta) => this.rpc.iceRestart({ ...data, sessionId: this.sessionId }, { invocationMeta }), this.abortController.signal);
8412
8447
  };
8413
8448
  this.updateMuteStates = async (muteStates) => {
8414
8449
  await this.joinTask;
8415
- return retryable(() => this.rpc.updateMuteStates({ muteStates, sessionId: this.sessionId }), this.abortController.signal);
8450
+ return retryable((invocationMeta) => this.rpc.updateMuteStates({ muteStates, sessionId: this.sessionId }, { invocationMeta }), this.abortController.signal);
8416
8451
  };
8417
8452
  this.sendStats = async (stats) => {
8418
8453
  await this.joinTask;
@@ -8421,11 +8456,11 @@ class StreamSfuClient {
8421
8456
  };
8422
8457
  this.startNoiseCancellation = async () => {
8423
8458
  await this.joinTask;
8424
- return retryable(() => this.rpc.startNoiseCancellation({ sessionId: this.sessionId }), this.abortController.signal);
8459
+ return retryable((invocationMeta) => this.rpc.startNoiseCancellation({ sessionId: this.sessionId }, { invocationMeta }), this.abortController.signal);
8425
8460
  };
8426
8461
  this.stopNoiseCancellation = async () => {
8427
8462
  await this.joinTask;
8428
- return retryable(() => this.rpc.stopNoiseCancellation({ sessionId: this.sessionId }), this.abortController.signal);
8463
+ return retryable((invocationMeta) => this.rpc.stopNoiseCancellation({ sessionId: this.sessionId }, { invocationMeta }), this.abortController.signal);
8429
8464
  };
8430
8465
  this.enterMigration = async (opts = {}) => {
8431
8466
  this.isLeaving = true;
@@ -8550,8 +8585,8 @@ class StreamSfuClient {
8550
8585
  interceptors: [
8551
8586
  withHeaders({ Authorization: `Bearer ${token}` }),
8552
8587
  this.tracer && withRequestTracer(this.tracer.trace),
8553
- this.logger.getLogLevel() === 'trace' &&
8554
- withRequestLogger(this.logger, 'trace'),
8588
+ this.logger.getLogLevel() === 'trace' && withRequestLogger(this.logger),
8589
+ withTimeout(rpcRequestTimeout, this.tracer?.trace),
8555
8590
  ].filter((v) => !!v),
8556
8591
  });
8557
8592
  // Special handling for the ICETrickle kind of events.
@@ -12361,12 +12396,14 @@ class Call {
12361
12396
  *
12362
12397
  * @returns a promise which resolves once the call join-flow has finished.
12363
12398
  */
12364
- this.join = async ({ maxJoinRetries = 3, ...data } = {}) => {
12399
+ this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, ...data } = {}) => {
12365
12400
  await this.setup();
12366
12401
  const callingState = this.state.callingState;
12367
12402
  if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
12368
12403
  throw new Error(`Illegal State: call.join() shall be called only once`);
12369
12404
  }
12405
+ this.joinResponseTimeout = joinResponseTimeout;
12406
+ this.rpcRequestTimeout = rpcRequestTimeout;
12370
12407
  // we will count the number of join failures per SFU.
12371
12408
  // once the number of failures reaches 2, we will piggyback on the `migrating_from`
12372
12409
  // field to force the coordinator to provide us another SFU
@@ -12447,6 +12484,8 @@ class Call {
12447
12484
  credentials: this.credentials,
12448
12485
  streamClient: this.streamClient,
12449
12486
  enableTracing: statsOptions.enable_rtc_stats,
12487
+ joinResponseTimeout: this.joinResponseTimeout,
12488
+ rpcRequestTimeout: this.rpcRequestTimeout,
12450
12489
  // a new session_id is necessary for the REJOIN strategy.
12451
12490
  // we use the previous session_id if available
12452
12491
  sessionId: performingRejoin ? undefined : previousSessionId,
@@ -13715,6 +13754,15 @@ class Call {
13715
13754
  },
13716
13755
  });
13717
13756
  };
13757
+ /**
13758
+ * Retrieves the call stats for the current call session in a format suitable
13759
+ * for displaying in map-like UIs.
13760
+ */
13761
+ this.getCallStatsMap = async (params = {}, callSessionId = this.state.session?.id) => {
13762
+ if (!callSessionId)
13763
+ throw new Error('callSessionId is required');
13764
+ return this.streamClient.get(`${this.streamClient.baseURL}/call_stats/${this.type}/${this.id}/${callSessionId}/map`, params);
13765
+ };
13718
13766
  /**
13719
13767
  * Sends a custom event to all call participants.
13720
13768
  *
@@ -15042,7 +15090,7 @@ class StreamClient {
15042
15090
  this.getUserAgent = () => {
15043
15091
  if (!this.cachedUserAgent) {
15044
15092
  const { clientAppIdentifier = {} } = this.options;
15045
- const { sdkName = 'js', sdkVersion = "1.39.2", ...extras } = clientAppIdentifier;
15093
+ const { sdkName = 'js', sdkVersion = "1.40.0", ...extras } = clientAppIdentifier;
15046
15094
  this.cachedUserAgent = [
15047
15095
  `stream-video-${sdkName}-v${sdkVersion}`,
15048
15096
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -15600,7 +15648,6 @@ class StreamVideoClient {
15600
15648
  return false;
15601
15649
  return this.state.calls.some((c) => c.cid !== currentCallId &&
15602
15650
  c.ringing &&
15603
- !c.isCreatedByMe &&
15604
15651
  c.state.callingState !== CallingState.IDLE &&
15605
15652
  c.state.callingState !== CallingState.LEFT &&
15606
15653
  c.state.callingState !== CallingState.RECONNECTING_FAILED);