@stream-io/video-client 1.28.0 → 1.29.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,23 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [1.29.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.28.1...@stream-io/video-client-1.29.0) (2025-09-09)
6
+
7
+ ### Features
8
+
9
+ - opt-out from optimistic updates ([#1904](https://github.com/GetStream/stream-video-js/issues/1904)) ([45dba34](https://github.com/GetStream/stream-video-js/commit/45dba34d38dc64f456e37b593e38e420426529f5))
10
+
11
+ ### Bug Fixes
12
+
13
+ - capabilities and call grants ([#1899](https://github.com/GetStream/stream-video-js/issues/1899)) ([5725dfa](https://github.com/GetStream/stream-video-js/commit/5725dfa29b1e5fdb6fe4e26825ce7b268664d2fa))
14
+ - graceful Axios request config overrides ([#1913](https://github.com/GetStream/stream-video-js/issues/1913)) ([a124099](https://github.com/GetStream/stream-video-js/commit/a124099f984a592750d66ac440ef6c27ae7a02d9))
15
+
16
+ ## [1.28.1](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.28.0...@stream-io/video-client-1.28.1) (2025-08-22)
17
+
18
+ ### Bug Fixes
19
+
20
+ - handle pre ended calls on ringing push arrival ([#1897](https://github.com/GetStream/stream-video-js/issues/1897)) ([935e375](https://github.com/GetStream/stream-video-js/commit/935e3756035639c651b3ac4469321a64b8576a0e))
21
+
5
22
  ## [1.28.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.27.5...@stream-io/video-client-1.28.0) (2025-08-21)
6
23
 
7
24
  ### Features
@@ -4,7 +4,7 @@ import { ServiceType, stackIntercept, RpcError } from '@protobuf-ts/runtime-rpc'
4
4
  import axios from 'axios';
5
5
  export { AxiosError } from 'axios';
6
6
  import { TwirpFetchTransport, TwirpErrorCode } from '@protobuf-ts/twirp-transport';
7
- import { ReplaySubject, combineLatest, BehaviorSubject, shareReplay, map, distinctUntilChanged, takeWhile, distinctUntilKeyChanged, fromEventPattern, startWith, concatMap, merge, from, fromEvent, tap, debounceTime, pairwise, of } from 'rxjs';
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
10
 
@@ -4649,6 +4649,7 @@ class CallState {
4649
4649
  // This happens when the participantJoined event hasn't been received yet.
4650
4650
  // We keep these tracks around until we can associate them with a participant.
4651
4651
  this.orphanedTracks = [];
4652
+ this.callGrantsSubject = new ReplaySubject(1);
4652
4653
  this.logger = getLogger(['CallState']);
4653
4654
  /**
4654
4655
  * A list of comparators that are used to sort the participants.
@@ -4776,6 +4777,15 @@ class CallState {
4776
4777
  this.setOwnCapabilities = (capabilities) => {
4777
4778
  return this.setCurrentValue(this.ownCapabilitiesSubject, capabilities);
4778
4779
  };
4780
+ /**
4781
+ * Sets the call grants (used for own capabilities).
4782
+ *
4783
+ * @internal
4784
+ * @param grants the grants to set.
4785
+ */
4786
+ this.setCallGrants = (grants) => {
4787
+ return this.setCurrentValue(this.callGrantsSubject, grants);
4788
+ };
4779
4789
  /**
4780
4790
  * Sets the backstage state.
4781
4791
  * @param backstage the backstage state.
@@ -5207,7 +5217,7 @@ class CallState {
5207
5217
  };
5208
5218
  this.updateOwnCapabilities = (event) => {
5209
5219
  if (event.user.id === this.localParticipant?.userId) {
5210
- this.setCurrentValue(this.ownCapabilitiesSubject, event.own_capabilities);
5220
+ this.setOwnCapabilities(event.own_capabilities);
5211
5221
  }
5212
5222
  };
5213
5223
  this.updateFromClosedCaptions = (event) => {
@@ -5276,25 +5286,53 @@ class CallState {
5276
5286
  const isShallowEqual = (a, b) => {
5277
5287
  if (a.length !== b.length)
5278
5288
  return false;
5279
- for (const item of a)
5289
+ for (const item of a) {
5280
5290
  if (!b.includes(item))
5281
5291
  return false;
5282
- for (const item of b)
5292
+ }
5293
+ for (const item of b) {
5283
5294
  if (!a.includes(item))
5284
5295
  return false;
5296
+ }
5285
5297
  return true;
5286
5298
  };
5287
5299
  /**
5288
5300
  * Creates an Observable from the given subject by piping to the
5289
5301
  * `distinctUntilChanged()` operator.
5290
5302
  */
5291
- const duc = (subject, comparator) => subject.asObservable().pipe(distinctUntilChanged(comparator));
5303
+ const duc = (subject, comparator) => subject.pipe(distinctUntilChanged(comparator));
5292
5304
  // primitive values should only emit once the value they hold changes
5293
5305
  this.anonymousParticipantCount$ = duc(this.anonymousParticipantCountSubject);
5294
5306
  this.blockedUserIds$ = duc(this.blockedUserIdsSubject, isShallowEqual);
5295
5307
  this.backstage$ = duc(this.backstageSubject);
5296
5308
  this.callingState$ = duc(this.callingStateSubject);
5297
- this.ownCapabilities$ = duc(this.ownCapabilitiesSubject, isShallowEqual);
5309
+ this.ownCapabilities$ = combineLatest([
5310
+ this.ownCapabilitiesSubject,
5311
+ this.callGrantsSubject.pipe(startWith(undefined)),
5312
+ ]).pipe(map(([capabilities, grants]) => {
5313
+ if (!grants)
5314
+ return capabilities;
5315
+ const { canPublishAudio, canPublishVideo, canScreenshare } = grants;
5316
+ const update = {
5317
+ [OwnCapability.SEND_AUDIO]: canPublishAudio,
5318
+ [OwnCapability.SEND_VIDEO]: canPublishVideo,
5319
+ [OwnCapability.SCREENSHARE]: canScreenshare,
5320
+ };
5321
+ const nextCapabilities = [...capabilities];
5322
+ for (const _capability in update) {
5323
+ const capability = _capability;
5324
+ const allowed = update[capability];
5325
+ // grants take precedence over capabilities, reconstruct the capabilities
5326
+ if (allowed && !nextCapabilities.includes(capability)) {
5327
+ nextCapabilities.push(capability);
5328
+ }
5329
+ else if (!allowed && nextCapabilities.includes(capability)) {
5330
+ const index = nextCapabilities.indexOf(capability);
5331
+ nextCapabilities.splice(index, 1);
5332
+ }
5333
+ }
5334
+ return nextCapabilities;
5335
+ }), distinctUntilChanged(isShallowEqual), shareReplay({ bufferSize: 1, refCount: true }));
5298
5336
  this.participantCount$ = duc(this.participantCountSubject);
5299
5337
  this.recording$ = duc(this.recordingSubject);
5300
5338
  this.transcribing$ = duc(this.transcribingSubject);
@@ -5653,7 +5691,7 @@ const getSdkVersion = (sdk) => {
5653
5691
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
5654
5692
  };
5655
5693
 
5656
- const version = "1.28.0";
5694
+ const version = "1.29.0";
5657
5695
  const [major, minor, patch] = version.split('.');
5658
5696
  let sdkInfo = {
5659
5697
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -8387,21 +8425,9 @@ const watchSfuCallEnded = (call) => {
8387
8425
  const watchCallGrantsUpdated = (state) => {
8388
8426
  return function onCallGrantsUpdated(event) {
8389
8427
  const { currentGrants } = event;
8390
- if (currentGrants) {
8391
- const { canPublishAudio, canPublishVideo, canScreenshare } = currentGrants;
8392
- const update = {
8393
- [OwnCapability.SEND_AUDIO]: canPublishAudio,
8394
- [OwnCapability.SEND_VIDEO]: canPublishVideo,
8395
- [OwnCapability.SCREENSHARE]: canScreenshare,
8396
- };
8397
- const nextCapabilities = state.ownCapabilities.filter((capability) => update[capability] !== false);
8398
- Object.entries(update).forEach(([capability, value]) => {
8399
- if (value && !nextCapabilities.includes(capability)) {
8400
- nextCapabilities.push(capability);
8401
- }
8402
- });
8403
- state.setOwnCapabilities(nextCapabilities);
8404
- }
8428
+ if (!currentGrants)
8429
+ return;
8430
+ state.setCallGrants(currentGrants);
8405
8431
  };
8406
8432
  };
8407
8433
 
@@ -8743,10 +8769,6 @@ const registerEventHandlers = (call, dispatcher) => {
8743
8769
  call.on('inboundStateNotification', watchInboundStateNotification(state)),
8744
8770
  handleRemoteSoftMute(call),
8745
8771
  ];
8746
- if (call.ringing) {
8747
- // these events are only relevant when the call is ringing
8748
- eventHandlers.push(registerRingingCallEventHandlers(call));
8749
- }
8750
8772
  return () => {
8751
8773
  eventHandlers.forEach((unsubscribe) => unsubscribe());
8752
8774
  };
@@ -10000,11 +10022,10 @@ class InputMediaDeviceManager {
10000
10022
  }
10001
10023
  });
10002
10024
  }
10003
- /**
10004
- * Stops or pauses the stream based on state.disableMode
10005
- * @param {boolean} [forceStop=false] when true, stops the tracks regardless of the state.disableMode
10006
- */
10007
- async disable(forceStop = false) {
10025
+ async disable(forceStopOrOptions) {
10026
+ const forceStop = typeof forceStopOrOptions === 'boolean'
10027
+ ? forceStopOrOptions
10028
+ : (forceStopOrOptions?.forceStop ?? false);
10008
10029
  this.state.prevStatus = this.state.optimisticStatus;
10009
10030
  if (!forceStop && this.state.optimisticStatus === 'disabled') {
10010
10031
  return;
@@ -11538,6 +11559,21 @@ class Call {
11538
11559
  });
11539
11560
  }
11540
11561
  }));
11562
+ if (this.ringing) {
11563
+ // if the call is ringing, we need to register the ringing call effects
11564
+ this.handleRingingCall();
11565
+ }
11566
+ else {
11567
+ // if the call is not ringing, we need to register the ringing call subscriptions
11568
+ // to handle the case when the call gets ringing flag after creation event
11569
+ this.leaveCallHooks.add(
11570
+ // "ringing" mode effects and event handlers
11571
+ createSubscription(this.ringingSubject, (isRinging) => {
11572
+ if (!isRinging)
11573
+ return;
11574
+ this.handleRingingCall();
11575
+ }));
11576
+ }
11541
11577
  this.leaveCallHooks.add(
11542
11578
  // cancel auto-drop when call is accepted or rejected
11543
11579
  createSubscription(this.state.session$, (session) => {
@@ -11559,53 +11595,49 @@ class Call {
11559
11595
  });
11560
11596
  }
11561
11597
  }));
11562
- this.leaveCallHooks.add(
11563
- // "ringing" mode effects and event handlers
11564
- createSubscription(this.ringingSubject, (isRinging) => {
11565
- if (!isRinging)
11566
- return;
11567
- const callSession = this.state.session;
11568
- const receiver_id = this.clientStore.connectedUser?.id;
11569
- const ended_at = callSession?.ended_at;
11570
- const created_by_id = this.state.createdBy?.id;
11571
- const rejected_by = callSession?.rejected_by;
11572
- const accepted_by = callSession?.accepted_by;
11573
- let leaveCallIdle = false;
11574
- if (ended_at) {
11575
- // call was ended before it was accepted or rejected so we should leave it to idle
11598
+ };
11599
+ this.handleRingingCall = () => {
11600
+ const callSession = this.state.session;
11601
+ const receiver_id = this.clientStore.connectedUser?.id;
11602
+ const ended_at = callSession?.ended_at;
11603
+ const created_by_id = this.state.createdBy?.id;
11604
+ const rejected_by = callSession?.rejected_by;
11605
+ const accepted_by = callSession?.accepted_by;
11606
+ let leaveCallIdle = false;
11607
+ if (ended_at) {
11608
+ // call was ended before it was accepted or rejected so we should leave it to idle
11609
+ leaveCallIdle = true;
11610
+ }
11611
+ else if (created_by_id && rejected_by) {
11612
+ if (rejected_by[created_by_id]) {
11613
+ // call was cancelled by the caller
11576
11614
  leaveCallIdle = true;
11577
11615
  }
11578
- else if (created_by_id && rejected_by) {
11579
- if (rejected_by[created_by_id]) {
11580
- // call was cancelled by the caller
11581
- leaveCallIdle = true;
11582
- }
11583
- }
11584
- else if (receiver_id && rejected_by) {
11585
- if (rejected_by[receiver_id]) {
11586
- // call was rejected by the receiver in some other device
11587
- leaveCallIdle = true;
11588
- }
11616
+ }
11617
+ else if (receiver_id && rejected_by) {
11618
+ if (rejected_by[receiver_id]) {
11619
+ // call was rejected by the receiver in some other device
11620
+ leaveCallIdle = true;
11589
11621
  }
11590
- else if (receiver_id && accepted_by) {
11591
- if (accepted_by[receiver_id]) {
11592
- // call was accepted by the receiver in some other device
11593
- leaveCallIdle = true;
11594
- }
11622
+ }
11623
+ else if (receiver_id && accepted_by) {
11624
+ if (accepted_by[receiver_id]) {
11625
+ // call was accepted by the receiver in some other device
11626
+ leaveCallIdle = true;
11595
11627
  }
11596
- if (leaveCallIdle) {
11597
- if (this.state.callingState !== CallingState.IDLE) {
11598
- this.state.setCallingState(CallingState.IDLE);
11599
- }
11628
+ }
11629
+ if (leaveCallIdle) {
11630
+ if (this.state.callingState !== CallingState.IDLE) {
11631
+ this.state.setCallingState(CallingState.IDLE);
11600
11632
  }
11601
- else {
11602
- if (this.state.callingState === CallingState.IDLE) {
11603
- this.state.setCallingState(CallingState.RINGING);
11604
- }
11605
- this.scheduleAutoDrop();
11606
- this.leaveCallHooks.add(registerRingingCallEventHandlers(this));
11633
+ }
11634
+ else {
11635
+ if (this.state.callingState === CallingState.IDLE) {
11636
+ this.state.setCallingState(CallingState.RINGING);
11607
11637
  }
11608
- }));
11638
+ this.scheduleAutoDrop();
11639
+ this.leaveCallHooks.add(registerRingingCallEventHandlers(this));
11640
+ }
11609
11641
  };
11610
11642
  this.handleOwnCapabilitiesUpdated = async (ownCapabilities) => {
11611
11643
  // update the permission context.
@@ -14562,7 +14594,7 @@ class StreamClient {
14562
14594
  this.getUserAgent = () => {
14563
14595
  if (!this.cachedUserAgent) {
14564
14596
  const { clientAppIdentifier = {} } = this.options;
14565
- const { sdkName = 'js', sdkVersion = "1.28.0", ...extras } = clientAppIdentifier;
14597
+ const { sdkName = 'js', sdkVersion = "1.29.0", ...extras } = clientAppIdentifier;
14566
14598
  this.cachedUserAgent = [
14567
14599
  `stream-video-${sdkName}-v${sdkVersion}`,
14568
14600
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -14584,12 +14616,14 @@ class StreamClient {
14584
14616
  'x-client-request-id': generateUUIDv4(),
14585
14617
  };
14586
14618
  }
14619
+ const { params: axiosConfigParams, headers: axiosConfigHeaders, ...axiosRequestConfig } = this.options.axiosRequestConfig || {};
14587
14620
  return {
14588
14621
  params: {
14589
14622
  user_id: this.userID,
14590
14623
  connection_id: this._getConnectionID(),
14591
14624
  api_key: this.key,
14592
14625
  ...options.params,
14626
+ ...axiosConfigParams,
14593
14627
  },
14594
14628
  headers: {
14595
14629
  ...authorization,
@@ -14598,9 +14632,10 @@ class StreamClient {
14598
14632
  : this.getAuthType(),
14599
14633
  'X-Stream-Client': this.getUserAgent(),
14600
14634
  ...options.headers,
14635
+ ...axiosConfigHeaders,
14601
14636
  },
14602
14637
  ...options.config,
14603
- ...this.options.axiosRequestConfig,
14638
+ ...axiosRequestConfig,
14604
14639
  };
14605
14640
  };
14606
14641
  this._getToken = () => {