@stream-io/video-client 1.55.1 → 1.56.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,24 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [1.56.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.55.2...@stream-io/video-client-1.56.0) (2026-07-30)
6
+
7
+ ### Features
8
+
9
+ - **react-sdk:** show blocked audio overlay per participant ([#2341](https://github.com/GetStream/stream-video-js/issues/2341)) ([4fe5c40](https://github.com/GetStream/stream-video-js/commit/4fe5c40aaed1829125bff49deec39a008c7abec5))
10
+
11
+ ### Bug Fixes
12
+
13
+ - **client:** fix stale mid in SetPublisher after PeerConnection rollback ([#2344](https://github.com/GetStream/stream-video-js/issues/2344)) ([9c3e201](https://github.com/GetStream/stream-video-js/commit/9c3e201c9a1cf129e182da961b1d73652508b9f2))
14
+ - **client:** never announce two tracks for the same publish option ([#2352](https://github.com/GetStream/stream-video-js/issues/2352)) ([b9a0c6e](https://github.com/GetStream/stream-video-js/commit/b9a0c6e709805e8658e74a68dc6e84459353db37))
15
+ - **client:** preserve ICE state in peer connection failure reports ([#2340](https://github.com/GetStream/stream-video-js/issues/2340)) ([2ce42a2](https://github.com/GetStream/stream-video-js/commit/2ce42a229eecfc60ae8f6783672c517b94b6ab42))
16
+
17
+ ## [1.55.2](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.55.1...@stream-io/video-client-1.55.2) (2026-07-17)
18
+
19
+ ### Bug Fixes
20
+
21
+ - use speakerLayoutSortPreset in RN spotlight view ([#2332](https://github.com/GetStream/stream-video-js/issues/2332)) ([910a5de](https://github.com/GetStream/stream-video-js/commit/910a5de6160cc34de99ea1615ef40bb034c57078))
22
+
5
23
  ## [1.55.1](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.55.0...@stream-io/video-client-1.55.1) (2026-07-02)
6
24
 
7
25
  ### Bug Fixes
@@ -4926,7 +4926,7 @@ const toIceCandidate = (iceTrickle) => {
4926
4926
  }
4927
4927
  };
4928
4928
 
4929
- const version = "1.55.1";
4929
+ const version = "1.56.0";
4930
4930
  const [major, minor, patch] = version.split('.');
4931
4931
  let sdkInfo = {
4932
4932
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -5255,6 +5255,23 @@ function wrapWithCancellation(tag, cb) {
5255
5255
  return { cb: wrapped, onContinued };
5256
5256
  }
5257
5257
 
5258
+ /**
5259
+ * Performs shallow comparison of two arrays.
5260
+ * Expects primitive values: [1, 2, 3] is equal to [2, 1, 3].
5261
+ */
5262
+ const isShallowArrayEqual = (a, b) => {
5263
+ if (a.length !== b.length)
5264
+ return false;
5265
+ for (const item of a) {
5266
+ if (!b.includes(item))
5267
+ return false;
5268
+ }
5269
+ for (const item of b) {
5270
+ if (!a.includes(item))
5271
+ return false;
5272
+ }
5273
+ return true;
5274
+ };
5258
5275
  /**
5259
5276
  * Checks if the provided update is a function patch.
5260
5277
  *
@@ -5367,6 +5384,7 @@ var rxUtils = /*#__PURE__*/Object.freeze({
5367
5384
  createSafeAsyncSubscription: createSafeAsyncSubscription,
5368
5385
  createSubscription: createSubscription,
5369
5386
  getCurrentValue: getCurrentValue,
5387
+ isShallowArrayEqual: isShallowArrayEqual,
5370
5388
  setCurrentValue: setCurrentValue,
5371
5389
  setCurrentValueAsync: setCurrentValueAsync,
5372
5390
  updateValue: updateValue
@@ -6107,23 +6125,6 @@ class CallState {
6107
6125
  this.endedBy$ = this.endedBySubject.asObservable();
6108
6126
  this.thumbnails$ = this.thumbnailsSubject.asObservable();
6109
6127
  this.closedCaptions$ = this.closedCaptionsSubject.asObservable();
6110
- /**
6111
- * Performs shallow comparison of two arrays.
6112
- * Expects primitive values: [1, 2, 3] is equal to [2, 1, 3].
6113
- */
6114
- const isShallowEqual = (a, b) => {
6115
- if (a.length !== b.length)
6116
- return false;
6117
- for (const item of a) {
6118
- if (!b.includes(item))
6119
- return false;
6120
- }
6121
- for (const item of b) {
6122
- if (!a.includes(item))
6123
- return false;
6124
- }
6125
- return true;
6126
- };
6127
6128
  /**
6128
6129
  * Creates an Observable from the given subject by piping to the
6129
6130
  * `distinctUntilChanged()` operator.
@@ -6131,7 +6132,7 @@ class CallState {
6131
6132
  const duc = (subject, comparator) => subject.pipe(distinctUntilChanged(comparator));
6132
6133
  // primitive values should only emit once the value they hold changes
6133
6134
  this.anonymousParticipantCount$ = duc(this.anonymousParticipantCountSubject);
6134
- this.blockedUserIds$ = duc(this.blockedUserIdsSubject, isShallowEqual);
6135
+ this.blockedUserIds$ = duc(this.blockedUserIdsSubject, isShallowArrayEqual);
6135
6136
  this.backstage$ = duc(this.backstageSubject);
6136
6137
  this.callingState$ = duc(this.callingStateSubject);
6137
6138
  this.ownCapabilities$ = combineLatest([
@@ -6160,7 +6161,7 @@ class CallState {
6160
6161
  }
6161
6162
  }
6162
6163
  return nextCapabilities;
6163
- }), distinctUntilChanged(isShallowEqual), shareReplay({ bufferSize: 1, refCount: true }));
6164
+ }), distinctUntilChanged(isShallowArrayEqual), shareReplay({ bufferSize: 1, refCount: true }));
6164
6165
  this.participantCount$ = duc(this.participantCountSubject);
6165
6166
  this.recording$ = duc(this.recordingSubject);
6166
6167
  this.individualRecording$ = duc(this.individualRecordingSubject);
@@ -6281,6 +6282,12 @@ class CallState {
6281
6282
  // trigger re-sorting of participants
6282
6283
  this.setCurrentValue(this.participantsSubject, (ps) => ps);
6283
6284
  };
6285
+ /**
6286
+ * Returns the comparator currently used to sort the participants.
6287
+ */
6288
+ getSortParticipantsBy = () => {
6289
+ return this.sortParticipantsBy;
6290
+ };
6284
6291
  /**
6285
6292
  * Gets the current value of an observable, or undefined if the observable has
6286
6293
  * not emitted a value yet.
@@ -8465,6 +8472,7 @@ class BasePeerConnection {
8465
8472
  try {
8466
8473
  this.onPeerConnectionStateChange?.({
8467
8474
  peerType: this.peerType,
8475
+ iceConnectionState: this.pc.iceConnectionState,
8468
8476
  ...event,
8469
8477
  });
8470
8478
  }
@@ -8919,6 +8927,7 @@ class Publisher extends BasePeerConnection {
8919
8927
  clonedTracks = new Set();
8920
8928
  publishOptions;
8921
8929
  selfSubEnabled;
8930
+ transceiverLockKey = `pub.tx.${this.lock}`;
8922
8931
  /**
8923
8932
  * Constructs a new `Publisher` instance.
8924
8933
  */
@@ -8974,32 +8983,34 @@ class Publisher extends BasePeerConnection {
8974
8983
  * @param options the publish options to use.
8975
8984
  */
8976
8985
  publish = async (track, trackType, options = {}) => {
8977
- if (!this.publishOptions.some((o) => o.trackType === trackType)) {
8978
- throw new Error(`No publish options found for ${TrackType[trackType]}`);
8979
- }
8980
- for (const publishOption of this.publishOptions) {
8981
- if (publishOption.trackType !== trackType)
8982
- continue;
8983
- // create a clone of the track as otherwise the same trackId will
8984
- // appear in the SDP in multiple transceivers
8985
- const trackToPublish = this.cloneTrack(track);
8986
- const bundle = this.transceiverCache.get(publishOption);
8987
- if (!bundle) {
8988
- await this.addTransceiver(trackToPublish, publishOption, options);
8986
+ return withoutConcurrency(this.transceiverLockKey, async () => {
8987
+ const publishOptions = this.publishOptions.filter((o) => o.trackType === trackType);
8988
+ if (!publishOptions.length) {
8989
+ throw new Error(`No publish options found for ${TrackType[trackType]}`);
8989
8990
  }
8990
- else {
8991
- const previousTrack = bundle.transceiver.sender.track;
8992
- await this.updateTransceiver(bundle, trackToPublish, options);
8993
- if (!isReactNative()) {
8994
- this.stopTrack(previousTrack);
8991
+ for (const publishOption of publishOptions) {
8992
+ // create a clone of the track as otherwise the same trackId will
8993
+ // appear in the SDP in multiple transceivers
8994
+ const trackToPublish = this.cloneTrack(track);
8995
+ const bundle = this.transceiverCache.get(publishOption);
8996
+ if (!bundle) {
8997
+ await this.addTransceiver(trackToPublish, publishOption, options);
8998
+ }
8999
+ else {
9000
+ const previousTrack = bundle.transceiver.sender.track;
9001
+ await this.updateTransceiver(bundle, trackToPublish, options);
9002
+ if (!isReactNative()) {
9003
+ this.stopTrack(previousTrack);
9004
+ }
8995
9005
  }
8996
9006
  }
8997
- }
9007
+ });
8998
9008
  };
8999
9009
  /**
9000
9010
  * Adds a new transceiver carrying the given track to the peer connection.
9001
9011
  */
9002
9012
  addTransceiver = async (track, publishOption, options) => {
9013
+ const trackType = publishOption.trackType;
9003
9014
  const encodings = isAudioTrackType(publishOption.trackType)
9004
9015
  ? computeAudioLayers(publishOption, options)
9005
9016
  : computeVideoLayers(track, publishOption);
@@ -9010,15 +9021,17 @@ class Publisher extends BasePeerConnection {
9010
9021
  direction: 'sendonly',
9011
9022
  sendEncodings,
9012
9023
  });
9024
+ // claim the cache entry in the same task that created the transceiver:
9025
+ // an await in between would let a concurrent caller add a second one
9026
+ // for the same publish option
9027
+ this.transceiverCache.add({ publishOption, transceiver, options });
9028
+ this.trackIdToTrackType.set(track.id, trackType);
9029
+ this.logger.debug(`Added ${TrackType[trackType]} transceiver`);
9013
9030
  const params = transceiver.sender.getParameters();
9014
9031
  params.degradationPreference =
9015
9032
  toRTCDegradationPreference(publishOption.degradationPreference) ??
9016
9033
  'maintain-framerate';
9017
9034
  await transceiver.sender.setParameters(params);
9018
- const trackType = publishOption.trackType;
9019
- this.logger.debug(`Added ${TrackType[trackType]} transceiver`);
9020
- this.transceiverCache.add({ publishOption, transceiver, options });
9021
- this.trackIdToTrackType.set(track.id, trackType);
9022
9035
  await this.negotiate();
9023
9036
  };
9024
9037
  /**
@@ -9075,33 +9088,35 @@ class Publisher extends BasePeerConnection {
9075
9088
  * Synchronizes the current Publisher state with the provided publish options.
9076
9089
  */
9077
9090
  syncPublishOptions = async () => {
9078
- // enable publishing with new options -> [av1, vp9]
9079
- for (const publishOption of this.publishOptions) {
9080
- const { trackType } = publishOption;
9081
- if (!this.isPublishing(trackType))
9082
- continue;
9083
- if (this.transceiverCache.has(publishOption))
9084
- continue;
9085
- const item = this.transceiverCache.find((i) => !!i.transceiver.sender.track &&
9086
- i.publishOption.trackType === trackType);
9087
- if (!item)
9088
- continue;
9089
- // take the track from the existing transceiver for the same track type,
9090
- // clone it and publish it with the new publish options
9091
- const track = this.cloneTrack(item.transceiver.sender.track);
9092
- await this.addTransceiver(track, publishOption, item.options);
9093
- }
9094
- // stop publishing with options not required anymore -> [vp9]
9095
- for (const item of this.transceiverCache.items()) {
9096
- const { publishOption, transceiver } = item;
9097
- const hasPublishOption = this.publishOptions.some((option) => option.id === publishOption.id &&
9098
- option.trackType === publishOption.trackType);
9099
- if (hasPublishOption)
9100
- continue;
9101
- // it is safe to stop the track here, it is a clone
9102
- this.stopTrack(transceiver.sender.track);
9103
- await this.updateTransceiver(item, null);
9104
- }
9091
+ return withoutConcurrency(this.transceiverLockKey, async () => {
9092
+ // enable publishing with new options -> [av1, vp9]
9093
+ for (const publishOption of this.publishOptions) {
9094
+ const { trackType } = publishOption;
9095
+ if (!this.isPublishing(trackType))
9096
+ continue;
9097
+ if (this.transceiverCache.has(publishOption))
9098
+ continue;
9099
+ const item = this.transceiverCache.find((i) => !!i.transceiver.sender.track &&
9100
+ i.publishOption.trackType === trackType);
9101
+ if (!item)
9102
+ continue;
9103
+ // take the track from the existing transceiver for the same track type,
9104
+ // clone it and publish it with the new publish options
9105
+ const track = this.cloneTrack(item.transceiver.sender.track);
9106
+ await this.addTransceiver(track, publishOption, item.options);
9107
+ }
9108
+ // stop publishing with options not required anymore -> [vp9]
9109
+ for (const item of this.transceiverCache.items()) {
9110
+ const { publishOption, transceiver } = item;
9111
+ const hasPublishOption = this.publishOptions.some((option) => option.id === publishOption.id &&
9112
+ option.trackType === publishOption.trackType);
9113
+ if (hasPublishOption)
9114
+ continue;
9115
+ // it is safe to stop the track here, it is a clone
9116
+ this.stopTrack(transceiver.sender.track);
9117
+ await this.updateTransceiver(item, null);
9118
+ }
9119
+ });
9105
9120
  };
9106
9121
  /**
9107
9122
  * Returns true if the given track type is currently being published to the SFU.
@@ -9269,12 +9284,23 @@ class Publisher extends BasePeerConnection {
9269
9284
  negotiate = async (options) => {
9270
9285
  return withoutConcurrency(`publisher.negotiate.${this.lock}`, async () => {
9271
9286
  const offer = await this.pc.createOffer(options);
9272
- const tracks = this.getAnnouncedTracks(offer.sdp);
9273
- if (!tracks.length)
9274
- throw new Error(`Can't negotiate without any tracks`);
9275
9287
  try {
9276
9288
  this.isIceRestarting = options?.iceRestart ?? false;
9277
9289
  await this.pc.setLocalDescription(offer);
9290
+ // the announced tracks have to be read after `setLocalDescription`.
9291
+ // `transceiver.mid` is null until the offer is applied, and reading the
9292
+ // tracks earlier makes `extractMid` guess the mid from the offer SDP.
9293
+ // That guess goes stale after a rolled-back negotiation, and a mid that
9294
+ // doesn't match the m-section carrying the track stops the SFU from
9295
+ // correlating the two: it then answers with every offered codec instead
9296
+ // of the announced one, and we publish the first codec of that list.
9297
+ // Per https://www.w3.org/TR/webrtc/#set-description, applying a local
9298
+ // offer associates the transceiver with its m-section and sets
9299
+ // `[[Mid]]` to `[[JsepMid]]`; a rollback disassociates a transceiver
9300
+ // that wasn't associated before and resets both slots to null.
9301
+ const tracks = this.getAnnouncedTracks(offer.sdp);
9302
+ if (!tracks.length)
9303
+ throw new Error(`Can't negotiate without any tracks`);
9278
9304
  const { sdp: baseSdp = '' } = offer;
9279
9305
  const { dangerouslyForceCodec, dangerouslySetStartBitrateFactor, fmtpLine, } = this.clientPublishOptions || {};
9280
9306
  let sdp = dangerouslyForceCodec
@@ -10899,34 +10925,67 @@ class AudioBindingsWatchdog {
10899
10925
  class BlockedAudioTracker {
10900
10926
  logger = videoLoggerSystem.getLogger('BlockedAudioTracker');
10901
10927
  tracer;
10902
- blockedElementsSubject = new BehaviorSubject(new Set());
10928
+ blockedElementsSubject = new BehaviorSubject([]);
10903
10929
  /**
10904
10930
  * Whether the browser's autoplay policy is blocking audio playback.
10905
10931
  * Will be `true` when at least one audio element is currently blocked.
10906
10932
  * Use {@link resumeAudio} within a user gesture to unblock.
10907
10933
  */
10908
- autoplayBlocked$ = this.blockedElementsSubject.pipe(map((elements) => elements.size > 0), distinctUntilChanged());
10934
+ autoplayBlocked$ = this.blockedElementsSubject.pipe(map((elements) => elements.length > 0), distinctUntilChanged());
10935
+ /**
10936
+ * The list of participant `sessionId`s whose audio element is currently
10937
+ * blocked by the browser's autoplay policy. Only some participants may be
10938
+ * blocked (e.g. one joined while a gesture was active and a later one was
10939
+ * not), so use this to render a per-participant affordance rather than a
10940
+ * call-wide one. Call {@link resumeAudio} within a user gesture to unblock
10941
+ * all of them.
10942
+ *
10943
+ * Session ids are registered together with the audio element.
10944
+ */
10945
+ blockedSessionIds$ = this.blockedElementsSubject.pipe(map((elements) => {
10946
+ const ids = [];
10947
+ elements.forEach(({ sessionId }) => {
10948
+ if (sessionId && !ids.includes(sessionId))
10949
+ ids.push(sessionId);
10950
+ });
10951
+ return ids;
10952
+ }), distinctUntilChanged(isShallowArrayEqual), shareReplay({ bufferSize: 1, refCount: true }));
10909
10953
  constructor(tracer) {
10910
10954
  this.tracer = tracer;
10911
10955
  }
10912
10956
  /**
10913
10957
  * Registers an audio element as blocked by the browser's autoplay policy.
10914
10958
  */
10915
- markBlocked = (audioElement, blocked) => {
10959
+ markBlocked = (audioElement, blocked, sessionId) => {
10916
10960
  setCurrentValue(this.blockedElementsSubject, (elements) => {
10917
- if (blocked)
10918
- elements.add(audioElement);
10919
- else
10920
- elements.delete(audioElement);
10921
- return elements;
10961
+ if (!blocked) {
10962
+ return elements.filter(({ element }) => element !== audioElement);
10963
+ }
10964
+ const existing = elements.find(({ element }) => element === audioElement);
10965
+ if (existing) {
10966
+ return elements.map((entry) => entry.element === audioElement
10967
+ ? { element: audioElement, sessionId: sessionId ?? entry.sessionId }
10968
+ : entry);
10969
+ }
10970
+ return [...elements, { element: audioElement, sessionId }];
10922
10971
  });
10923
10972
  };
10973
+ /**
10974
+ * Clears all tracked elements. The tracker stays usable afterwards.
10975
+ */
10976
+ reset = () => {
10977
+ if (this.blockedElementsSubject.getValue().length > 0) {
10978
+ this.blockedElementsSubject.next([]);
10979
+ }
10980
+ };
10924
10981
  /**
10925
10982
  * Returns whether the given audio element is currently flagged as blocked
10926
10983
  * by the browser's autoplay policy.
10927
10984
  */
10928
10985
  isBlocked = (audioElement) => {
10929
- return this.blockedElementsSubject.getValue().has(audioElement);
10986
+ return this.blockedElementsSubject
10987
+ .getValue()
10988
+ .some(({ element }) => element === audioElement);
10930
10989
  };
10931
10990
  /**
10932
10991
  * Plays all audio elements blocked by the browser's autoplay policy.
@@ -10935,17 +10994,18 @@ class BlockedAudioTracker {
10935
10994
  resumeAudio = async () => {
10936
10995
  this.tracer.trace('resumeAudio', null);
10937
10996
  await setCurrentValueAsync(this.blockedElementsSubject, async (elements) => {
10938
- await Promise.all(Array.from(elements, async (element) => {
10997
+ let next = elements;
10998
+ await Promise.all(elements.map(async ({ element }) => {
10939
10999
  try {
10940
11000
  if (element.srcObject)
10941
11001
  await timeboxed([element.play()], 2000);
10942
- elements.delete(element);
11002
+ next = next.filter((entry) => entry.element !== element);
10943
11003
  }
10944
11004
  catch (err) {
10945
11005
  this.logger.warn(`Can't resume audio for element`, element, err);
10946
11006
  }
10947
11007
  }));
10948
- return elements;
11008
+ return next;
10949
11009
  });
10950
11010
  };
10951
11011
  }
@@ -11477,6 +11537,12 @@ class DynascaleManager {
11477
11537
  let sourceNode = undefined;
11478
11538
  let gainNode = undefined;
11479
11539
  let audioWatchdog = undefined;
11540
+ const clearBlockedAudio = () => {
11541
+ if (!this.blockedAudioTracker.isBlocked(audioElement))
11542
+ return;
11543
+ this.blockedAudioTracker.markBlocked(audioElement, false);
11544
+ };
11545
+ audioElement.addEventListener('playing', clearBlockedAudio);
11480
11546
  const isAudioTrack = trackType === 'audioTrack';
11481
11547
  const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
11482
11548
  const updateMediaStreamSubscription = participant$
@@ -11490,7 +11556,7 @@ class DynascaleManager {
11490
11556
  audioWatchdog?.dispose();
11491
11557
  audioWatchdog = undefined;
11492
11558
  if (!source) {
11493
- this.blockedAudioTracker.markBlocked(audioElement, false);
11559
+ clearBlockedAudio();
11494
11560
  return;
11495
11561
  }
11496
11562
  // Safari has a special quirk that prevents playing audio until the user
@@ -11518,7 +11584,7 @@ class DynascaleManager {
11518
11584
  this.tracer.trace('audioPlaybackError', e.message);
11519
11585
  if (e.name === 'NotAllowedError') {
11520
11586
  this.tracer.trace('audioPlaybackBlocked', null);
11521
- this.blockedAudioTracker.markBlocked(audioElement, true);
11587
+ this.blockedAudioTracker.markBlocked(audioElement, true, sessionId);
11522
11588
  }
11523
11589
  this.logger.warn(`Failed to play audio stream`, e);
11524
11590
  });
@@ -11551,7 +11617,8 @@ class DynascaleManager {
11551
11617
  });
11552
11618
  audioElement.autoplay = true;
11553
11619
  return () => {
11554
- this.blockedAudioTracker.markBlocked(audioElement, false);
11620
+ audioElement.removeEventListener('playing', clearBlockedAudio);
11621
+ clearBlockedAudio();
11555
11622
  sinkIdSubscription?.unsubscribe();
11556
11623
  volumeSubscription.unsubscribe();
11557
11624
  updateMediaStreamSubscription.unsubscribe();
@@ -14726,6 +14793,7 @@ class Call {
14726
14793
  this.screenShare = new ScreenShareManager(this);
14727
14794
  this.trackSubscriptionManager = new TrackSubscriptionManager(this.state, this.tracer);
14728
14795
  this.blockedAudioTracker = new BlockedAudioTracker(this.tracer);
14796
+ this.leaveCallHooks.add(this.blockedAudioTracker.reset);
14729
14797
  if (typeof document !== 'undefined') {
14730
14798
  this.audioBindingsWatchdog = new AudioBindingsWatchdog(this.state, this.tracer);
14731
14799
  this.viewportTracker = new ViewportTracker(this.state);
@@ -16349,6 +16417,12 @@ class Call {
16349
16417
  setSortParticipantsBy = (criteria) => {
16350
16418
  return this.state.setSortParticipantsBy(criteria);
16351
16419
  };
16420
+ /**
16421
+ * Returns the comparator currently used to sort the participants.
16422
+ */
16423
+ getSortParticipantsBy = () => {
16424
+ return this.state.getSortParticipantsBy();
16425
+ };
16352
16426
  /**
16353
16427
  * Sends a reaction to the other call participants.
16354
16428
  *
@@ -18418,11 +18492,11 @@ class StreamClient {
18418
18492
  return await this.wsConnection.connect(this.defaultWSTimeout);
18419
18493
  };
18420
18494
  getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
18421
- "1.55.1";
18495
+ "1.56.0";
18422
18496
  getUserAgent = () => {
18423
18497
  if (!this.cachedUserAgent) {
18424
18498
  const { clientAppIdentifier = {} } = this.options;
18425
- const { sdkName = 'js', sdkVersion = "1.55.1", ...extras } = clientAppIdentifier;
18499
+ const { sdkName = 'js', sdkVersion = "1.56.0", ...extras } = clientAppIdentifier;
18426
18500
  this.cachedUserAgent = [
18427
18501
  `stream-video-${sdkName}-v${sdkVersion}`,
18428
18502
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -18759,8 +18833,8 @@ class ClientEventReporter {
18759
18833
  applyErrorIfAbsent(this.wsPairs.get(cid), stageError);
18760
18834
  this.failCoordinator(cid);
18761
18835
  this.failWs(cid);
18762
- this.emitPeerConnectionFailure(cid, 'publish', code, reason, 'NOT_CONNECTED');
18763
- this.emitPeerConnectionFailure(cid, 'subscribe', code, reason, 'NOT_CONNECTED');
18836
+ this.emitPeerConnectionFailure(cid, 'publish', code, reason);
18837
+ this.emitPeerConnectionFailure(cid, 'subscribe', code, reason);
18764
18838
  }
18765
18839
  catch (err) {
18766
18840
  this.logger.warn('Failed to report abort', err);
@@ -18772,7 +18846,7 @@ class ClientEventReporter {
18772
18846
  if (this.wsPairs.get(cid))
18773
18847
  this.failWs(cid);
18774
18848
  for (const role of ['publish', 'subscribe']) {
18775
- this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt', 'NOT_CONNECTED');
18849
+ this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt');
18776
18850
  }
18777
18851
  };
18778
18852
  emitJoinInitiated = (cid) => {
@@ -18934,21 +19008,24 @@ class ClientEventReporter {
18934
19008
  };
18935
19009
  onPeerConnectionStateChange = (cid, event) => {
18936
19010
  const role = event.peerType === PeerType.SUBSCRIBER ? 'subscribe' : 'publish';
18937
- if (event.stateType === 'ice' && event.state === 'failed') {
18938
- this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed', 'FAILED');
19011
+ const pair = this.peerConnectionPairs.get(pcKey(cid, role));
19012
+ if (pair)
19013
+ pair.lastIceState = event.iceConnectionState;
19014
+ if (event.stateType === 'ice') {
19015
+ if (event.state === 'failed') {
19016
+ this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed');
19017
+ }
18939
19018
  return;
18940
19019
  }
18941
- if (event.stateType === 'peerConnection' && event.state === 'failed') {
18942
- this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed', 'CONNECTED');
19020
+ if (event.state === 'failed') {
19021
+ this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed');
18943
19022
  return;
18944
19023
  }
18945
- if (event.stateType !== 'peerConnection')
18946
- return;
18947
19024
  switch (event.state) {
18948
19025
  case 'connecting':
18949
- if (this.peerConnectionPairs.has(pcKey(cid, role)))
19026
+ if (pair)
18950
19027
  return;
18951
- this.openPeerConnectionPair(cid, role);
19028
+ this.openPeerConnectionPair(cid, role, event.iceConnectionState);
18952
19029
  break;
18953
19030
  case 'connected':
18954
19031
  this.emitPeerConnectionSuccess(cid, role);
@@ -18956,7 +19033,7 @@ class ClientEventReporter {
18956
19033
  break;
18957
19034
  }
18958
19035
  };
18959
- openPeerConnectionPair = (cid, role) => {
19036
+ openPeerConnectionPair = (cid, role, iceConnectionState) => {
18960
19037
  const key = pcKey(cid, role);
18961
19038
  const pair = {
18962
19039
  sid: generateUUIDv4(),
@@ -18966,6 +19043,7 @@ class ClientEventReporter {
18966
19043
  sfuId: this.getSfuId(cid),
18967
19044
  userSessionId: this.getUserSessionId(cid),
18968
19045
  wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
19046
+ lastIceState: iceConnectionState,
18969
19047
  };
18970
19048
  this.peerConnectionPairs.set(key, pair);
18971
19049
  this.sendForCall(cid, {
@@ -19001,7 +19079,7 @@ class ClientEventReporter {
19001
19079
  });
19002
19080
  this.peerConnectionPairs.delete(key);
19003
19081
  };
19004
- emitPeerConnectionFailure = (cid, role, code, reason, iceState) => {
19082
+ emitPeerConnectionFailure = (cid, role, code, reason) => {
19005
19083
  const key = pcKey(cid, role);
19006
19084
  const pair = this.peerConnectionPairs.get(key);
19007
19085
  if (!pair)
@@ -19019,7 +19097,7 @@ class ClientEventReporter {
19019
19097
  outcome: 'failure',
19020
19098
  retry_count_attempt: 0,
19021
19099
  elapsed_time: Date.now() - pair.startedAt,
19022
- ice_state: iceState,
19100
+ ice_state: toReportedIceState(pair.lastIceState),
19023
19101
  retry_failure_reason: reason,
19024
19102
  retry_failure_code: code,
19025
19103
  });
@@ -19085,6 +19163,17 @@ class ClientEventReporter {
19085
19163
  return false;
19086
19164
  };
19087
19165
  }
19166
+ const toReportedIceState = (state) => {
19167
+ switch (state) {
19168
+ case 'connected':
19169
+ case 'completed':
19170
+ return 'CONNECTED';
19171
+ case 'failed':
19172
+ return 'FAILED';
19173
+ default:
19174
+ return 'NOT_CONNECTED';
19175
+ }
19176
+ };
19088
19177
  const readPermissionStatus = (permission) => {
19089
19178
  const state = getCurrentValue(permission.asStateObservable());
19090
19179
  switch (state) {