@stream-io/video-client 1.55.2 → 1.57.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/index.browser.es.js +216 -119
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +216 -119
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +216 -119
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/helpers/BlockedAudioTracker.d.ts +16 -1
  9. package/dist/src/helpers/MediaPlaybackWatchdog.d.ts +4 -0
  10. package/dist/src/reporting/ClientEventReporter.d.ts +1 -0
  11. package/dist/src/rtc/Publisher.d.ts +1 -0
  12. package/dist/src/rtc/types.d.ts +9 -2
  13. package/dist/src/store/rxUtils.d.ts +5 -0
  14. package/package.json +11 -11
  15. package/src/Call.ts +1 -0
  16. package/src/devices/__tests__/MicrophoneManagerRN.test.ts +1 -2
  17. package/src/devices/devicePersistence.ts +1 -2
  18. package/src/helpers/BlockedAudioTracker.ts +71 -13
  19. package/src/helpers/DynascaleManager.ts +14 -3
  20. package/src/helpers/MediaPlaybackWatchdog.ts +35 -16
  21. package/src/helpers/__tests__/BlockedAudioTracker.test.ts +54 -6
  22. package/src/helpers/__tests__/DynascaleManager.test.ts +45 -0
  23. package/src/helpers/__tests__/MediaPlaybackWatchdog.test.ts +137 -8
  24. package/src/helpers/__tests__/ViewportTracker.test.ts +1 -2
  25. package/src/reporting/ClientEventReporter.ts +39 -39
  26. package/src/reporting/__tests__/ClientEventReporter.test.ts +159 -44
  27. package/src/rtc/BasePeerConnection.ts +1 -0
  28. package/src/rtc/Publisher.ts +78 -58
  29. package/src/rtc/__tests__/Publisher.test.ts +297 -0
  30. package/src/rtc/types.ts +14 -13
  31. package/src/stats/rtc/types.ts +8 -2
  32. package/src/store/CallState.ts +6 -19
  33. package/src/store/rxUtils.ts +15 -0
  34. package/src/types.ts +1 -4
package/dist/index.es.js CHANGED
@@ -4927,7 +4927,7 @@ const toIceCandidate = (iceTrickle) => {
4927
4927
  }
4928
4928
  };
4929
4929
 
4930
- const version = "1.55.2";
4930
+ const version = "1.57.0";
4931
4931
  const [major, minor, patch] = version.split('.');
4932
4932
  let sdkInfo = {
4933
4933
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -5256,6 +5256,23 @@ function wrapWithCancellation(tag, cb) {
5256
5256
  return { cb: wrapped, onContinued };
5257
5257
  }
5258
5258
 
5259
+ /**
5260
+ * Performs shallow comparison of two arrays.
5261
+ * Expects primitive values: [1, 2, 3] is equal to [2, 1, 3].
5262
+ */
5263
+ const isShallowArrayEqual = (a, b) => {
5264
+ if (a.length !== b.length)
5265
+ return false;
5266
+ for (const item of a) {
5267
+ if (!b.includes(item))
5268
+ return false;
5269
+ }
5270
+ for (const item of b) {
5271
+ if (!a.includes(item))
5272
+ return false;
5273
+ }
5274
+ return true;
5275
+ };
5259
5276
  /**
5260
5277
  * Checks if the provided update is a function patch.
5261
5278
  *
@@ -5368,6 +5385,7 @@ var rxUtils = /*#__PURE__*/Object.freeze({
5368
5385
  createSafeAsyncSubscription: createSafeAsyncSubscription,
5369
5386
  createSubscription: createSubscription,
5370
5387
  getCurrentValue: getCurrentValue,
5388
+ isShallowArrayEqual: isShallowArrayEqual,
5371
5389
  setCurrentValue: setCurrentValue,
5372
5390
  setCurrentValueAsync: setCurrentValueAsync,
5373
5391
  updateValue: updateValue
@@ -6108,23 +6126,6 @@ class CallState {
6108
6126
  this.endedBy$ = this.endedBySubject.asObservable();
6109
6127
  this.thumbnails$ = this.thumbnailsSubject.asObservable();
6110
6128
  this.closedCaptions$ = this.closedCaptionsSubject.asObservable();
6111
- /**
6112
- * Performs shallow comparison of two arrays.
6113
- * Expects primitive values: [1, 2, 3] is equal to [2, 1, 3].
6114
- */
6115
- const isShallowEqual = (a, b) => {
6116
- if (a.length !== b.length)
6117
- return false;
6118
- for (const item of a) {
6119
- if (!b.includes(item))
6120
- return false;
6121
- }
6122
- for (const item of b) {
6123
- if (!a.includes(item))
6124
- return false;
6125
- }
6126
- return true;
6127
- };
6128
6129
  /**
6129
6130
  * Creates an Observable from the given subject by piping to the
6130
6131
  * `distinctUntilChanged()` operator.
@@ -6132,7 +6133,7 @@ class CallState {
6132
6133
  const duc = (subject, comparator) => subject.pipe(distinctUntilChanged(comparator));
6133
6134
  // primitive values should only emit once the value they hold changes
6134
6135
  this.anonymousParticipantCount$ = duc(this.anonymousParticipantCountSubject);
6135
- this.blockedUserIds$ = duc(this.blockedUserIdsSubject, isShallowEqual);
6136
+ this.blockedUserIds$ = duc(this.blockedUserIdsSubject, isShallowArrayEqual);
6136
6137
  this.backstage$ = duc(this.backstageSubject);
6137
6138
  this.callingState$ = duc(this.callingStateSubject);
6138
6139
  this.ownCapabilities$ = combineLatest([
@@ -6161,7 +6162,7 @@ class CallState {
6161
6162
  }
6162
6163
  }
6163
6164
  return nextCapabilities;
6164
- }), distinctUntilChanged(isShallowEqual), shareReplay({ bufferSize: 1, refCount: true }));
6165
+ }), distinctUntilChanged(isShallowArrayEqual), shareReplay({ bufferSize: 1, refCount: true }));
6165
6166
  this.participantCount$ = duc(this.participantCountSubject);
6166
6167
  this.recording$ = duc(this.recordingSubject);
6167
6168
  this.individualRecording$ = duc(this.individualRecordingSubject);
@@ -8472,6 +8473,7 @@ class BasePeerConnection {
8472
8473
  try {
8473
8474
  this.onPeerConnectionStateChange?.({
8474
8475
  peerType: this.peerType,
8476
+ iceConnectionState: this.pc.iceConnectionState,
8475
8477
  ...event,
8476
8478
  });
8477
8479
  }
@@ -8926,6 +8928,7 @@ class Publisher extends BasePeerConnection {
8926
8928
  clonedTracks = new Set();
8927
8929
  publishOptions;
8928
8930
  selfSubEnabled;
8931
+ transceiverLockKey = `pub.tx.${this.lock}`;
8929
8932
  /**
8930
8933
  * Constructs a new `Publisher` instance.
8931
8934
  */
@@ -8981,32 +8984,34 @@ class Publisher extends BasePeerConnection {
8981
8984
  * @param options the publish options to use.
8982
8985
  */
8983
8986
  publish = async (track, trackType, options = {}) => {
8984
- if (!this.publishOptions.some((o) => o.trackType === trackType)) {
8985
- throw new Error(`No publish options found for ${TrackType[trackType]}`);
8986
- }
8987
- for (const publishOption of this.publishOptions) {
8988
- if (publishOption.trackType !== trackType)
8989
- continue;
8990
- // create a clone of the track as otherwise the same trackId will
8991
- // appear in the SDP in multiple transceivers
8992
- const trackToPublish = this.cloneTrack(track);
8993
- const bundle = this.transceiverCache.get(publishOption);
8994
- if (!bundle) {
8995
- await this.addTransceiver(trackToPublish, publishOption, options);
8987
+ return withoutConcurrency(this.transceiverLockKey, async () => {
8988
+ const publishOptions = this.publishOptions.filter((o) => o.trackType === trackType);
8989
+ if (!publishOptions.length) {
8990
+ throw new Error(`No publish options found for ${TrackType[trackType]}`);
8996
8991
  }
8997
- else {
8998
- const previousTrack = bundle.transceiver.sender.track;
8999
- await this.updateTransceiver(bundle, trackToPublish, options);
9000
- if (!isReactNative()) {
9001
- this.stopTrack(previousTrack);
8992
+ for (const publishOption of publishOptions) {
8993
+ // create a clone of the track as otherwise the same trackId will
8994
+ // appear in the SDP in multiple transceivers
8995
+ const trackToPublish = this.cloneTrack(track);
8996
+ const bundle = this.transceiverCache.get(publishOption);
8997
+ if (!bundle) {
8998
+ await this.addTransceiver(trackToPublish, publishOption, options);
8999
+ }
9000
+ else {
9001
+ const previousTrack = bundle.transceiver.sender.track;
9002
+ await this.updateTransceiver(bundle, trackToPublish, options);
9003
+ if (!isReactNative()) {
9004
+ this.stopTrack(previousTrack);
9005
+ }
9002
9006
  }
9003
9007
  }
9004
- }
9008
+ });
9005
9009
  };
9006
9010
  /**
9007
9011
  * Adds a new transceiver carrying the given track to the peer connection.
9008
9012
  */
9009
9013
  addTransceiver = async (track, publishOption, options) => {
9014
+ const trackType = publishOption.trackType;
9010
9015
  const encodings = isAudioTrackType(publishOption.trackType)
9011
9016
  ? computeAudioLayers(publishOption, options)
9012
9017
  : computeVideoLayers(track, publishOption);
@@ -9017,15 +9022,17 @@ class Publisher extends BasePeerConnection {
9017
9022
  direction: 'sendonly',
9018
9023
  sendEncodings,
9019
9024
  });
9025
+ // claim the cache entry in the same task that created the transceiver:
9026
+ // an await in between would let a concurrent caller add a second one
9027
+ // for the same publish option
9028
+ this.transceiverCache.add({ publishOption, transceiver, options });
9029
+ this.trackIdToTrackType.set(track.id, trackType);
9030
+ this.logger.debug(`Added ${TrackType[trackType]} transceiver`);
9020
9031
  const params = transceiver.sender.getParameters();
9021
9032
  params.degradationPreference =
9022
9033
  toRTCDegradationPreference(publishOption.degradationPreference) ??
9023
9034
  'maintain-framerate';
9024
9035
  await transceiver.sender.setParameters(params);
9025
- const trackType = publishOption.trackType;
9026
- this.logger.debug(`Added ${TrackType[trackType]} transceiver`);
9027
- this.transceiverCache.add({ publishOption, transceiver, options });
9028
- this.trackIdToTrackType.set(track.id, trackType);
9029
9036
  await this.negotiate();
9030
9037
  };
9031
9038
  /**
@@ -9082,33 +9089,35 @@ class Publisher extends BasePeerConnection {
9082
9089
  * Synchronizes the current Publisher state with the provided publish options.
9083
9090
  */
9084
9091
  syncPublishOptions = async () => {
9085
- // enable publishing with new options -> [av1, vp9]
9086
- for (const publishOption of this.publishOptions) {
9087
- const { trackType } = publishOption;
9088
- if (!this.isPublishing(trackType))
9089
- continue;
9090
- if (this.transceiverCache.has(publishOption))
9091
- continue;
9092
- const item = this.transceiverCache.find((i) => !!i.transceiver.sender.track &&
9093
- i.publishOption.trackType === trackType);
9094
- if (!item)
9095
- continue;
9096
- // take the track from the existing transceiver for the same track type,
9097
- // clone it and publish it with the new publish options
9098
- const track = this.cloneTrack(item.transceiver.sender.track);
9099
- await this.addTransceiver(track, publishOption, item.options);
9100
- }
9101
- // stop publishing with options not required anymore -> [vp9]
9102
- for (const item of this.transceiverCache.items()) {
9103
- const { publishOption, transceiver } = item;
9104
- const hasPublishOption = this.publishOptions.some((option) => option.id === publishOption.id &&
9105
- option.trackType === publishOption.trackType);
9106
- if (hasPublishOption)
9107
- continue;
9108
- // it is safe to stop the track here, it is a clone
9109
- this.stopTrack(transceiver.sender.track);
9110
- await this.updateTransceiver(item, null);
9111
- }
9092
+ return withoutConcurrency(this.transceiverLockKey, async () => {
9093
+ // enable publishing with new options -> [av1, vp9]
9094
+ for (const publishOption of this.publishOptions) {
9095
+ const { trackType } = publishOption;
9096
+ if (!this.isPublishing(trackType))
9097
+ continue;
9098
+ if (this.transceiverCache.has(publishOption))
9099
+ continue;
9100
+ const item = this.transceiverCache.find((i) => !!i.transceiver.sender.track &&
9101
+ i.publishOption.trackType === trackType);
9102
+ if (!item)
9103
+ continue;
9104
+ // take the track from the existing transceiver for the same track type,
9105
+ // clone it and publish it with the new publish options
9106
+ const track = this.cloneTrack(item.transceiver.sender.track);
9107
+ await this.addTransceiver(track, publishOption, item.options);
9108
+ }
9109
+ // stop publishing with options not required anymore -> [vp9]
9110
+ for (const item of this.transceiverCache.items()) {
9111
+ const { publishOption, transceiver } = item;
9112
+ const hasPublishOption = this.publishOptions.some((option) => option.id === publishOption.id &&
9113
+ option.trackType === publishOption.trackType);
9114
+ if (hasPublishOption)
9115
+ continue;
9116
+ // it is safe to stop the track here, it is a clone
9117
+ this.stopTrack(transceiver.sender.track);
9118
+ await this.updateTransceiver(item, null);
9119
+ }
9120
+ });
9112
9121
  };
9113
9122
  /**
9114
9123
  * Returns true if the given track type is currently being published to the SFU.
@@ -9276,12 +9285,23 @@ class Publisher extends BasePeerConnection {
9276
9285
  negotiate = async (options) => {
9277
9286
  return withoutConcurrency(`publisher.negotiate.${this.lock}`, async () => {
9278
9287
  const offer = await this.pc.createOffer(options);
9279
- const tracks = this.getAnnouncedTracks(offer.sdp);
9280
- if (!tracks.length)
9281
- throw new Error(`Can't negotiate without any tracks`);
9282
9288
  try {
9283
9289
  this.isIceRestarting = options?.iceRestart ?? false;
9284
9290
  await this.pc.setLocalDescription(offer);
9291
+ // the announced tracks have to be read after `setLocalDescription`.
9292
+ // `transceiver.mid` is null until the offer is applied, and reading the
9293
+ // tracks earlier makes `extractMid` guess the mid from the offer SDP.
9294
+ // That guess goes stale after a rolled-back negotiation, and a mid that
9295
+ // doesn't match the m-section carrying the track stops the SFU from
9296
+ // correlating the two: it then answers with every offered codec instead
9297
+ // of the announced one, and we publish the first codec of that list.
9298
+ // Per https://www.w3.org/TR/webrtc/#set-description, applying a local
9299
+ // offer associates the transceiver with its m-section and sets
9300
+ // `[[Mid]]` to `[[JsepMid]]`; a rollback disassociates a transceiver
9301
+ // that wasn't associated before and resets both slots to null.
9302
+ const tracks = this.getAnnouncedTracks(offer.sdp);
9303
+ if (!tracks.length)
9304
+ throw new Error(`Can't negotiate without any tracks`);
9285
9305
  const { sdp: baseSdp = '' } = offer;
9286
9306
  const { dangerouslyForceCodec, dangerouslySetStartBitrateFactor, fmtpLine, } = this.clientPublishOptions || {};
9287
9307
  let sdp = dangerouslyForceCodec
@@ -10906,34 +10926,67 @@ class AudioBindingsWatchdog {
10906
10926
  class BlockedAudioTracker {
10907
10927
  logger = videoLoggerSystem.getLogger('BlockedAudioTracker');
10908
10928
  tracer;
10909
- blockedElementsSubject = new BehaviorSubject(new Set());
10929
+ blockedElementsSubject = new BehaviorSubject([]);
10910
10930
  /**
10911
10931
  * Whether the browser's autoplay policy is blocking audio playback.
10912
10932
  * Will be `true` when at least one audio element is currently blocked.
10913
10933
  * Use {@link resumeAudio} within a user gesture to unblock.
10914
10934
  */
10915
- autoplayBlocked$ = this.blockedElementsSubject.pipe(map((elements) => elements.size > 0), distinctUntilChanged());
10935
+ autoplayBlocked$ = this.blockedElementsSubject.pipe(map((elements) => elements.length > 0), distinctUntilChanged());
10936
+ /**
10937
+ * The list of participant `sessionId`s whose audio element is currently
10938
+ * blocked by the browser's autoplay policy. Only some participants may be
10939
+ * blocked (e.g. one joined while a gesture was active and a later one was
10940
+ * not), so use this to render a per-participant affordance rather than a
10941
+ * call-wide one. Call {@link resumeAudio} within a user gesture to unblock
10942
+ * all of them.
10943
+ *
10944
+ * Session ids are registered together with the audio element.
10945
+ */
10946
+ blockedSessionIds$ = this.blockedElementsSubject.pipe(map((elements) => {
10947
+ const ids = [];
10948
+ elements.forEach(({ sessionId }) => {
10949
+ if (sessionId && !ids.includes(sessionId))
10950
+ ids.push(sessionId);
10951
+ });
10952
+ return ids;
10953
+ }), distinctUntilChanged(isShallowArrayEqual), shareReplay({ bufferSize: 1, refCount: true }));
10916
10954
  constructor(tracer) {
10917
10955
  this.tracer = tracer;
10918
10956
  }
10919
10957
  /**
10920
10958
  * Registers an audio element as blocked by the browser's autoplay policy.
10921
10959
  */
10922
- markBlocked = (audioElement, blocked) => {
10960
+ markBlocked = (audioElement, blocked, sessionId) => {
10923
10961
  setCurrentValue(this.blockedElementsSubject, (elements) => {
10924
- if (blocked)
10925
- elements.add(audioElement);
10926
- else
10927
- elements.delete(audioElement);
10928
- return elements;
10962
+ if (!blocked) {
10963
+ return elements.filter(({ element }) => element !== audioElement);
10964
+ }
10965
+ const existing = elements.find(({ element }) => element === audioElement);
10966
+ if (existing) {
10967
+ return elements.map((entry) => entry.element === audioElement
10968
+ ? { element: audioElement, sessionId: sessionId ?? entry.sessionId }
10969
+ : entry);
10970
+ }
10971
+ return [...elements, { element: audioElement, sessionId }];
10929
10972
  });
10930
10973
  };
10974
+ /**
10975
+ * Clears all tracked elements. The tracker stays usable afterwards.
10976
+ */
10977
+ reset = () => {
10978
+ if (this.blockedElementsSubject.getValue().length > 0) {
10979
+ this.blockedElementsSubject.next([]);
10980
+ }
10981
+ };
10931
10982
  /**
10932
10983
  * Returns whether the given audio element is currently flagged as blocked
10933
10984
  * by the browser's autoplay policy.
10934
10985
  */
10935
10986
  isBlocked = (audioElement) => {
10936
- return this.blockedElementsSubject.getValue().has(audioElement);
10987
+ return this.blockedElementsSubject
10988
+ .getValue()
10989
+ .some(({ element }) => element === audioElement);
10937
10990
  };
10938
10991
  /**
10939
10992
  * Plays all audio elements blocked by the browser's autoplay policy.
@@ -10942,17 +10995,18 @@ class BlockedAudioTracker {
10942
10995
  resumeAudio = async () => {
10943
10996
  this.tracer.trace('resumeAudio', null);
10944
10997
  await setCurrentValueAsync(this.blockedElementsSubject, async (elements) => {
10945
- await Promise.all(Array.from(elements, async (element) => {
10998
+ let next = elements;
10999
+ await Promise.all(elements.map(async ({ element }) => {
10946
11000
  try {
10947
11001
  if (element.srcObject)
10948
11002
  await timeboxed([element.play()], 2000);
10949
- elements.delete(element);
11003
+ next = next.filter((entry) => entry.element !== element);
10950
11004
  }
10951
11005
  catch (err) {
10952
11006
  this.logger.warn(`Can't resume audio for element`, element, err);
10953
11007
  }
10954
11008
  }));
10955
- return elements;
11009
+ return next;
10956
11010
  });
10957
11011
  };
10958
11012
  }
@@ -11143,8 +11197,10 @@ class MediaPlaybackWatchdog {
11143
11197
  tracer;
11144
11198
  controller = new AbortController();
11145
11199
  pendingTimer;
11200
+ settleTimer;
11146
11201
  attempt = 0;
11147
11202
  disposed = false;
11203
+ stopped = false;
11148
11204
  constructor(opts) {
11149
11205
  this.element = opts.element;
11150
11206
  this.kind = opts.kind;
@@ -11165,33 +11221,49 @@ class MediaPlaybackWatchdog {
11165
11221
  return;
11166
11222
  this.disposed = true;
11167
11223
  this.controller.abort();
11224
+ this.clearTimers();
11225
+ };
11226
+ clearTimers = () => {
11168
11227
  if (this.pendingTimer)
11169
11228
  clearTimeout(this.pendingTimer);
11170
11229
  this.pendingTimer = undefined;
11230
+ if (this.settleTimer)
11231
+ clearTimeout(this.settleTimer);
11232
+ this.settleTimer = undefined;
11171
11233
  };
11172
11234
  onPlaying = () => {
11173
- if (this.attempt > 0) {
11174
- this.tracer.trace('mediaPlayback.recover.success', {
11175
- kind: this.kind,
11176
- attempts: this.attempt,
11177
- });
11178
- }
11179
- this.attempt = 0;
11180
11235
  if (this.pendingTimer)
11181
11236
  clearTimeout(this.pendingTimer);
11182
11237
  this.pendingTimer = undefined;
11238
+ if (this.attempt === 0 || this.settleTimer)
11239
+ return;
11240
+ this.settleTimer ??= setTimeout(this.onSettled, 1000);
11241
+ };
11242
+ onSettled = () => {
11243
+ this.settleTimer = undefined;
11244
+ if (this.element.paused)
11245
+ return;
11246
+ this.tracer.trace('mediaPlayback.recover.success', {
11247
+ kind: this.kind,
11248
+ attempts: this.attempt,
11249
+ });
11250
+ this.attempt = 0;
11251
+ this.stopped = false;
11183
11252
  };
11184
11253
  onPauseOrSuspend = (event) => {
11185
- if (this.disposed)
11254
+ if (this.disposed || this.stopped)
11186
11255
  return;
11187
11256
  this.tracer.trace('mediaPlayback.paused', {
11188
11257
  kind: this.kind,
11189
11258
  reason: event.type,
11190
11259
  });
11260
+ if (this.settleTimer)
11261
+ clearTimeout(this.settleTimer);
11262
+ this.settleTimer = undefined;
11191
11263
  this.scheduleRecovery();
11192
11264
  };
11193
11265
  scheduleRecovery = () => {
11194
- if (this.disposed || this.pendingTimer)
11266
+ if (this.disposed || this.stopped || this.pendingTimer)
11195
11267
  return;
11196
11268
  const skipReason = this.computeSkipReason();
11197
11269
  if (skipReason) {
@@ -11201,6 +11273,15 @@ class MediaPlaybackWatchdog {
11201
11273
  });
11202
11274
  return;
11203
11275
  }
11276
+ if (this.attempt >= 10) {
11277
+ this.stopped = true;
11278
+ this.clearTimers();
11279
+ this.tracer.trace('mediaPlayback.recover.giveUp', {
11280
+ kind: this.kind,
11281
+ attempts: this.attempt,
11282
+ });
11283
+ return;
11284
+ }
11204
11285
  const delay = this.attempt === 0 ? 0 : retryInterval(this.attempt);
11205
11286
  this.pendingTimer = setTimeout(this.attemptPlay, delay);
11206
11287
  };
@@ -11236,13 +11317,6 @@ class MediaPlaybackWatchdog {
11236
11317
  if (this.disposed)
11237
11318
  return;
11238
11319
  this.logger.warn(`Failed to recover ${this.kind} playback`, err);
11239
- if (this.attempt >= 10) {
11240
- this.tracer.trace('mediaPlayback.recover.giveUp', {
11241
- kind: this.kind,
11242
- attempts: this.attempt,
11243
- });
11244
- return;
11245
- }
11246
11320
  this.scheduleRecovery();
11247
11321
  }
11248
11322
  };
@@ -11484,6 +11558,12 @@ class DynascaleManager {
11484
11558
  let sourceNode = undefined;
11485
11559
  let gainNode = undefined;
11486
11560
  let audioWatchdog = undefined;
11561
+ const clearBlockedAudio = () => {
11562
+ if (!this.blockedAudioTracker.isBlocked(audioElement))
11563
+ return;
11564
+ this.blockedAudioTracker.markBlocked(audioElement, false);
11565
+ };
11566
+ audioElement.addEventListener('playing', clearBlockedAudio);
11487
11567
  const isAudioTrack = trackType === 'audioTrack';
11488
11568
  const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
11489
11569
  const updateMediaStreamSubscription = participant$
@@ -11497,7 +11577,7 @@ class DynascaleManager {
11497
11577
  audioWatchdog?.dispose();
11498
11578
  audioWatchdog = undefined;
11499
11579
  if (!source) {
11500
- this.blockedAudioTracker.markBlocked(audioElement, false);
11580
+ clearBlockedAudio();
11501
11581
  return;
11502
11582
  }
11503
11583
  // Safari has a special quirk that prevents playing audio until the user
@@ -11525,7 +11605,7 @@ class DynascaleManager {
11525
11605
  this.tracer.trace('audioPlaybackError', e.message);
11526
11606
  if (e.name === 'NotAllowedError') {
11527
11607
  this.tracer.trace('audioPlaybackBlocked', null);
11528
- this.blockedAudioTracker.markBlocked(audioElement, true);
11608
+ this.blockedAudioTracker.markBlocked(audioElement, true, sessionId);
11529
11609
  }
11530
11610
  this.logger.warn(`Failed to play audio stream`, e);
11531
11611
  });
@@ -11558,7 +11638,8 @@ class DynascaleManager {
11558
11638
  });
11559
11639
  audioElement.autoplay = true;
11560
11640
  return () => {
11561
- this.blockedAudioTracker.markBlocked(audioElement, false);
11641
+ audioElement.removeEventListener('playing', clearBlockedAudio);
11642
+ clearBlockedAudio();
11562
11643
  sinkIdSubscription?.unsubscribe();
11563
11644
  volumeSubscription.unsubscribe();
11564
11645
  updateMediaStreamSubscription.unsubscribe();
@@ -14733,6 +14814,7 @@ class Call {
14733
14814
  this.screenShare = new ScreenShareManager(this);
14734
14815
  this.trackSubscriptionManager = new TrackSubscriptionManager(this.state, this.tracer);
14735
14816
  this.blockedAudioTracker = new BlockedAudioTracker(this.tracer);
14817
+ this.leaveCallHooks.add(this.blockedAudioTracker.reset);
14736
14818
  if (typeof document !== 'undefined') {
14737
14819
  this.audioBindingsWatchdog = new AudioBindingsWatchdog(this.state, this.tracer);
14738
14820
  this.viewportTracker = new ViewportTracker(this.state);
@@ -18429,11 +18511,11 @@ class StreamClient {
18429
18511
  return await this.wsConnection.connect(this.defaultWSTimeout);
18430
18512
  };
18431
18513
  getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
18432
- "1.55.2";
18514
+ "1.57.0";
18433
18515
  getUserAgent = () => {
18434
18516
  if (!this.cachedUserAgent) {
18435
18517
  const { clientAppIdentifier = {} } = this.options;
18436
- const { sdkName = 'js', sdkVersion = "1.55.2", ...extras } = clientAppIdentifier;
18518
+ const { sdkName = 'js', sdkVersion = "1.57.0", ...extras } = clientAppIdentifier;
18437
18519
  this.cachedUserAgent = [
18438
18520
  `stream-video-${sdkName}-v${sdkVersion}`,
18439
18521
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -18770,8 +18852,8 @@ class ClientEventReporter {
18770
18852
  applyErrorIfAbsent(this.wsPairs.get(cid), stageError);
18771
18853
  this.failCoordinator(cid);
18772
18854
  this.failWs(cid);
18773
- this.emitPeerConnectionFailure(cid, 'publish', code, reason, 'NOT_CONNECTED');
18774
- this.emitPeerConnectionFailure(cid, 'subscribe', code, reason, 'NOT_CONNECTED');
18855
+ this.emitPeerConnectionFailure(cid, 'publish', code, reason);
18856
+ this.emitPeerConnectionFailure(cid, 'subscribe', code, reason);
18775
18857
  }
18776
18858
  catch (err) {
18777
18859
  this.logger.warn('Failed to report abort', err);
@@ -18783,7 +18865,7 @@ class ClientEventReporter {
18783
18865
  if (this.wsPairs.get(cid))
18784
18866
  this.failWs(cid);
18785
18867
  for (const role of ['publish', 'subscribe']) {
18786
- this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt', 'NOT_CONNECTED');
18868
+ this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt');
18787
18869
  }
18788
18870
  };
18789
18871
  emitJoinInitiated = (cid) => {
@@ -18945,21 +19027,24 @@ class ClientEventReporter {
18945
19027
  };
18946
19028
  onPeerConnectionStateChange = (cid, event) => {
18947
19029
  const role = event.peerType === PeerType.SUBSCRIBER ? 'subscribe' : 'publish';
18948
- if (event.stateType === 'ice' && event.state === 'failed') {
18949
- this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed', 'FAILED');
19030
+ const pair = this.peerConnectionPairs.get(pcKey(cid, role));
19031
+ if (pair)
19032
+ pair.lastIceState = event.iceConnectionState;
19033
+ if (event.stateType === 'ice') {
19034
+ if (event.state === 'failed') {
19035
+ this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed');
19036
+ }
18950
19037
  return;
18951
19038
  }
18952
- if (event.stateType === 'peerConnection' && event.state === 'failed') {
18953
- this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed', 'CONNECTED');
19039
+ if (event.state === 'failed') {
19040
+ this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed');
18954
19041
  return;
18955
19042
  }
18956
- if (event.stateType !== 'peerConnection')
18957
- return;
18958
19043
  switch (event.state) {
18959
19044
  case 'connecting':
18960
- if (this.peerConnectionPairs.has(pcKey(cid, role)))
19045
+ if (pair)
18961
19046
  return;
18962
- this.openPeerConnectionPair(cid, role);
19047
+ this.openPeerConnectionPair(cid, role, event.iceConnectionState);
18963
19048
  break;
18964
19049
  case 'connected':
18965
19050
  this.emitPeerConnectionSuccess(cid, role);
@@ -18967,7 +19052,7 @@ class ClientEventReporter {
18967
19052
  break;
18968
19053
  }
18969
19054
  };
18970
- openPeerConnectionPair = (cid, role) => {
19055
+ openPeerConnectionPair = (cid, role, iceConnectionState) => {
18971
19056
  const key = pcKey(cid, role);
18972
19057
  const pair = {
18973
19058
  sid: generateUUIDv4(),
@@ -18977,6 +19062,7 @@ class ClientEventReporter {
18977
19062
  sfuId: this.getSfuId(cid),
18978
19063
  userSessionId: this.getUserSessionId(cid),
18979
19064
  wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
19065
+ lastIceState: iceConnectionState,
18980
19066
  };
18981
19067
  this.peerConnectionPairs.set(key, pair);
18982
19068
  this.sendForCall(cid, {
@@ -19012,7 +19098,7 @@ class ClientEventReporter {
19012
19098
  });
19013
19099
  this.peerConnectionPairs.delete(key);
19014
19100
  };
19015
- emitPeerConnectionFailure = (cid, role, code, reason, iceState) => {
19101
+ emitPeerConnectionFailure = (cid, role, code, reason) => {
19016
19102
  const key = pcKey(cid, role);
19017
19103
  const pair = this.peerConnectionPairs.get(key);
19018
19104
  if (!pair)
@@ -19030,7 +19116,7 @@ class ClientEventReporter {
19030
19116
  outcome: 'failure',
19031
19117
  retry_count_attempt: 0,
19032
19118
  elapsed_time: Date.now() - pair.startedAt,
19033
- ice_state: iceState,
19119
+ ice_state: toReportedIceState(pair.lastIceState),
19034
19120
  retry_failure_reason: reason,
19035
19121
  retry_failure_code: code,
19036
19122
  });
@@ -19096,6 +19182,17 @@ class ClientEventReporter {
19096
19182
  return false;
19097
19183
  };
19098
19184
  }
19185
+ const toReportedIceState = (state) => {
19186
+ switch (state) {
19187
+ case 'connected':
19188
+ case 'completed':
19189
+ return 'CONNECTED';
19190
+ case 'failed':
19191
+ return 'FAILED';
19192
+ default:
19193
+ return 'NOT_CONNECTED';
19194
+ }
19195
+ };
19099
19196
  const readPermissionStatus = (permission) => {
19100
19197
  const state = getCurrentValue(permission.asStateObservable());
19101
19198
  switch (state) {