@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.cjs.js CHANGED
@@ -4946,7 +4946,7 @@ const toIceCandidate = (iceTrickle) => {
4946
4946
  }
4947
4947
  };
4948
4948
 
4949
- const version = "1.55.2";
4949
+ const version = "1.57.0";
4950
4950
  const [major, minor, patch] = version.split('.');
4951
4951
  let sdkInfo = {
4952
4952
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -5275,6 +5275,23 @@ function wrapWithCancellation(tag, cb) {
5275
5275
  return { cb: wrapped, onContinued };
5276
5276
  }
5277
5277
 
5278
+ /**
5279
+ * Performs shallow comparison of two arrays.
5280
+ * Expects primitive values: [1, 2, 3] is equal to [2, 1, 3].
5281
+ */
5282
+ const isShallowArrayEqual = (a, b) => {
5283
+ if (a.length !== b.length)
5284
+ return false;
5285
+ for (const item of a) {
5286
+ if (!b.includes(item))
5287
+ return false;
5288
+ }
5289
+ for (const item of b) {
5290
+ if (!a.includes(item))
5291
+ return false;
5292
+ }
5293
+ return true;
5294
+ };
5278
5295
  /**
5279
5296
  * Checks if the provided update is a function patch.
5280
5297
  *
@@ -5387,6 +5404,7 @@ var rxUtils = /*#__PURE__*/Object.freeze({
5387
5404
  createSafeAsyncSubscription: createSafeAsyncSubscription,
5388
5405
  createSubscription: createSubscription,
5389
5406
  getCurrentValue: getCurrentValue,
5407
+ isShallowArrayEqual: isShallowArrayEqual,
5390
5408
  setCurrentValue: setCurrentValue,
5391
5409
  setCurrentValueAsync: setCurrentValueAsync,
5392
5410
  updateValue: updateValue
@@ -6127,23 +6145,6 @@ class CallState {
6127
6145
  this.endedBy$ = this.endedBySubject.asObservable();
6128
6146
  this.thumbnails$ = this.thumbnailsSubject.asObservable();
6129
6147
  this.closedCaptions$ = this.closedCaptionsSubject.asObservable();
6130
- /**
6131
- * Performs shallow comparison of two arrays.
6132
- * Expects primitive values: [1, 2, 3] is equal to [2, 1, 3].
6133
- */
6134
- const isShallowEqual = (a, b) => {
6135
- if (a.length !== b.length)
6136
- return false;
6137
- for (const item of a) {
6138
- if (!b.includes(item))
6139
- return false;
6140
- }
6141
- for (const item of b) {
6142
- if (!a.includes(item))
6143
- return false;
6144
- }
6145
- return true;
6146
- };
6147
6148
  /**
6148
6149
  * Creates an Observable from the given subject by piping to the
6149
6150
  * `distinctUntilChanged()` operator.
@@ -6151,7 +6152,7 @@ class CallState {
6151
6152
  const duc = (subject, comparator) => subject.pipe(rxjs.distinctUntilChanged(comparator));
6152
6153
  // primitive values should only emit once the value they hold changes
6153
6154
  this.anonymousParticipantCount$ = duc(this.anonymousParticipantCountSubject);
6154
- this.blockedUserIds$ = duc(this.blockedUserIdsSubject, isShallowEqual);
6155
+ this.blockedUserIds$ = duc(this.blockedUserIdsSubject, isShallowArrayEqual);
6155
6156
  this.backstage$ = duc(this.backstageSubject);
6156
6157
  this.callingState$ = duc(this.callingStateSubject);
6157
6158
  this.ownCapabilities$ = rxjs.combineLatest([
@@ -6180,7 +6181,7 @@ class CallState {
6180
6181
  }
6181
6182
  }
6182
6183
  return nextCapabilities;
6183
- }), rxjs.distinctUntilChanged(isShallowEqual), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
6184
+ }), rxjs.distinctUntilChanged(isShallowArrayEqual), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
6184
6185
  this.participantCount$ = duc(this.participantCountSubject);
6185
6186
  this.recording$ = duc(this.recordingSubject);
6186
6187
  this.individualRecording$ = duc(this.individualRecordingSubject);
@@ -8491,6 +8492,7 @@ class BasePeerConnection {
8491
8492
  try {
8492
8493
  this.onPeerConnectionStateChange?.({
8493
8494
  peerType: this.peerType,
8495
+ iceConnectionState: this.pc.iceConnectionState,
8494
8496
  ...event,
8495
8497
  });
8496
8498
  }
@@ -8945,6 +8947,7 @@ class Publisher extends BasePeerConnection {
8945
8947
  clonedTracks = new Set();
8946
8948
  publishOptions;
8947
8949
  selfSubEnabled;
8950
+ transceiverLockKey = `pub.tx.${this.lock}`;
8948
8951
  /**
8949
8952
  * Constructs a new `Publisher` instance.
8950
8953
  */
@@ -9000,32 +9003,34 @@ class Publisher extends BasePeerConnection {
9000
9003
  * @param options the publish options to use.
9001
9004
  */
9002
9005
  publish = async (track, trackType, options = {}) => {
9003
- if (!this.publishOptions.some((o) => o.trackType === trackType)) {
9004
- throw new Error(`No publish options found for ${TrackType[trackType]}`);
9005
- }
9006
- for (const publishOption of this.publishOptions) {
9007
- if (publishOption.trackType !== trackType)
9008
- continue;
9009
- // create a clone of the track as otherwise the same trackId will
9010
- // appear in the SDP in multiple transceivers
9011
- const trackToPublish = this.cloneTrack(track);
9012
- const bundle = this.transceiverCache.get(publishOption);
9013
- if (!bundle) {
9014
- await this.addTransceiver(trackToPublish, publishOption, options);
9006
+ return withoutConcurrency(this.transceiverLockKey, async () => {
9007
+ const publishOptions = this.publishOptions.filter((o) => o.trackType === trackType);
9008
+ if (!publishOptions.length) {
9009
+ throw new Error(`No publish options found for ${TrackType[trackType]}`);
9015
9010
  }
9016
- else {
9017
- const previousTrack = bundle.transceiver.sender.track;
9018
- await this.updateTransceiver(bundle, trackToPublish, options);
9019
- if (!isReactNative()) {
9020
- this.stopTrack(previousTrack);
9011
+ for (const publishOption of publishOptions) {
9012
+ // create a clone of the track as otherwise the same trackId will
9013
+ // appear in the SDP in multiple transceivers
9014
+ const trackToPublish = this.cloneTrack(track);
9015
+ const bundle = this.transceiverCache.get(publishOption);
9016
+ if (!bundle) {
9017
+ await this.addTransceiver(trackToPublish, publishOption, options);
9018
+ }
9019
+ else {
9020
+ const previousTrack = bundle.transceiver.sender.track;
9021
+ await this.updateTransceiver(bundle, trackToPublish, options);
9022
+ if (!isReactNative()) {
9023
+ this.stopTrack(previousTrack);
9024
+ }
9021
9025
  }
9022
9026
  }
9023
- }
9027
+ });
9024
9028
  };
9025
9029
  /**
9026
9030
  * Adds a new transceiver carrying the given track to the peer connection.
9027
9031
  */
9028
9032
  addTransceiver = async (track, publishOption, options) => {
9033
+ const trackType = publishOption.trackType;
9029
9034
  const encodings = isAudioTrackType(publishOption.trackType)
9030
9035
  ? computeAudioLayers(publishOption, options)
9031
9036
  : computeVideoLayers(track, publishOption);
@@ -9036,15 +9041,17 @@ class Publisher extends BasePeerConnection {
9036
9041
  direction: 'sendonly',
9037
9042
  sendEncodings,
9038
9043
  });
9044
+ // claim the cache entry in the same task that created the transceiver:
9045
+ // an await in between would let a concurrent caller add a second one
9046
+ // for the same publish option
9047
+ this.transceiverCache.add({ publishOption, transceiver, options });
9048
+ this.trackIdToTrackType.set(track.id, trackType);
9049
+ this.logger.debug(`Added ${TrackType[trackType]} transceiver`);
9039
9050
  const params = transceiver.sender.getParameters();
9040
9051
  params.degradationPreference =
9041
9052
  toRTCDegradationPreference(publishOption.degradationPreference) ??
9042
9053
  'maintain-framerate';
9043
9054
  await transceiver.sender.setParameters(params);
9044
- const trackType = publishOption.trackType;
9045
- this.logger.debug(`Added ${TrackType[trackType]} transceiver`);
9046
- this.transceiverCache.add({ publishOption, transceiver, options });
9047
- this.trackIdToTrackType.set(track.id, trackType);
9048
9055
  await this.negotiate();
9049
9056
  };
9050
9057
  /**
@@ -9101,33 +9108,35 @@ class Publisher extends BasePeerConnection {
9101
9108
  * Synchronizes the current Publisher state with the provided publish options.
9102
9109
  */
9103
9110
  syncPublishOptions = async () => {
9104
- // enable publishing with new options -> [av1, vp9]
9105
- for (const publishOption of this.publishOptions) {
9106
- const { trackType } = publishOption;
9107
- if (!this.isPublishing(trackType))
9108
- continue;
9109
- if (this.transceiverCache.has(publishOption))
9110
- continue;
9111
- const item = this.transceiverCache.find((i) => !!i.transceiver.sender.track &&
9112
- i.publishOption.trackType === trackType);
9113
- if (!item)
9114
- continue;
9115
- // take the track from the existing transceiver for the same track type,
9116
- // clone it and publish it with the new publish options
9117
- const track = this.cloneTrack(item.transceiver.sender.track);
9118
- await this.addTransceiver(track, publishOption, item.options);
9119
- }
9120
- // stop publishing with options not required anymore -> [vp9]
9121
- for (const item of this.transceiverCache.items()) {
9122
- const { publishOption, transceiver } = item;
9123
- const hasPublishOption = this.publishOptions.some((option) => option.id === publishOption.id &&
9124
- option.trackType === publishOption.trackType);
9125
- if (hasPublishOption)
9126
- continue;
9127
- // it is safe to stop the track here, it is a clone
9128
- this.stopTrack(transceiver.sender.track);
9129
- await this.updateTransceiver(item, null);
9130
- }
9111
+ return withoutConcurrency(this.transceiverLockKey, async () => {
9112
+ // enable publishing with new options -> [av1, vp9]
9113
+ for (const publishOption of this.publishOptions) {
9114
+ const { trackType } = publishOption;
9115
+ if (!this.isPublishing(trackType))
9116
+ continue;
9117
+ if (this.transceiverCache.has(publishOption))
9118
+ continue;
9119
+ const item = this.transceiverCache.find((i) => !!i.transceiver.sender.track &&
9120
+ i.publishOption.trackType === trackType);
9121
+ if (!item)
9122
+ continue;
9123
+ // take the track from the existing transceiver for the same track type,
9124
+ // clone it and publish it with the new publish options
9125
+ const track = this.cloneTrack(item.transceiver.sender.track);
9126
+ await this.addTransceiver(track, publishOption, item.options);
9127
+ }
9128
+ // stop publishing with options not required anymore -> [vp9]
9129
+ for (const item of this.transceiverCache.items()) {
9130
+ const { publishOption, transceiver } = item;
9131
+ const hasPublishOption = this.publishOptions.some((option) => option.id === publishOption.id &&
9132
+ option.trackType === publishOption.trackType);
9133
+ if (hasPublishOption)
9134
+ continue;
9135
+ // it is safe to stop the track here, it is a clone
9136
+ this.stopTrack(transceiver.sender.track);
9137
+ await this.updateTransceiver(item, null);
9138
+ }
9139
+ });
9131
9140
  };
9132
9141
  /**
9133
9142
  * Returns true if the given track type is currently being published to the SFU.
@@ -9295,12 +9304,23 @@ class Publisher extends BasePeerConnection {
9295
9304
  negotiate = async (options) => {
9296
9305
  return withoutConcurrency(`publisher.negotiate.${this.lock}`, async () => {
9297
9306
  const offer = await this.pc.createOffer(options);
9298
- const tracks = this.getAnnouncedTracks(offer.sdp);
9299
- if (!tracks.length)
9300
- throw new Error(`Can't negotiate without any tracks`);
9301
9307
  try {
9302
9308
  this.isIceRestarting = options?.iceRestart ?? false;
9303
9309
  await this.pc.setLocalDescription(offer);
9310
+ // the announced tracks have to be read after `setLocalDescription`.
9311
+ // `transceiver.mid` is null until the offer is applied, and reading the
9312
+ // tracks earlier makes `extractMid` guess the mid from the offer SDP.
9313
+ // That guess goes stale after a rolled-back negotiation, and a mid that
9314
+ // doesn't match the m-section carrying the track stops the SFU from
9315
+ // correlating the two: it then answers with every offered codec instead
9316
+ // of the announced one, and we publish the first codec of that list.
9317
+ // Per https://www.w3.org/TR/webrtc/#set-description, applying a local
9318
+ // offer associates the transceiver with its m-section and sets
9319
+ // `[[Mid]]` to `[[JsepMid]]`; a rollback disassociates a transceiver
9320
+ // that wasn't associated before and resets both slots to null.
9321
+ const tracks = this.getAnnouncedTracks(offer.sdp);
9322
+ if (!tracks.length)
9323
+ throw new Error(`Can't negotiate without any tracks`);
9304
9324
  const { sdp: baseSdp = '' } = offer;
9305
9325
  const { dangerouslyForceCodec, dangerouslySetStartBitrateFactor, fmtpLine, } = this.clientPublishOptions || {};
9306
9326
  let sdp = dangerouslyForceCodec
@@ -10925,34 +10945,67 @@ class AudioBindingsWatchdog {
10925
10945
  class BlockedAudioTracker {
10926
10946
  logger = videoLoggerSystem.getLogger('BlockedAudioTracker');
10927
10947
  tracer;
10928
- blockedElementsSubject = new rxjs.BehaviorSubject(new Set());
10948
+ blockedElementsSubject = new rxjs.BehaviorSubject([]);
10929
10949
  /**
10930
10950
  * Whether the browser's autoplay policy is blocking audio playback.
10931
10951
  * Will be `true` when at least one audio element is currently blocked.
10932
10952
  * Use {@link resumeAudio} within a user gesture to unblock.
10933
10953
  */
10934
- autoplayBlocked$ = this.blockedElementsSubject.pipe(rxjs.map((elements) => elements.size > 0), rxjs.distinctUntilChanged());
10954
+ autoplayBlocked$ = this.blockedElementsSubject.pipe(rxjs.map((elements) => elements.length > 0), rxjs.distinctUntilChanged());
10955
+ /**
10956
+ * The list of participant `sessionId`s whose audio element is currently
10957
+ * blocked by the browser's autoplay policy. Only some participants may be
10958
+ * blocked (e.g. one joined while a gesture was active and a later one was
10959
+ * not), so use this to render a per-participant affordance rather than a
10960
+ * call-wide one. Call {@link resumeAudio} within a user gesture to unblock
10961
+ * all of them.
10962
+ *
10963
+ * Session ids are registered together with the audio element.
10964
+ */
10965
+ blockedSessionIds$ = this.blockedElementsSubject.pipe(rxjs.map((elements) => {
10966
+ const ids = [];
10967
+ elements.forEach(({ sessionId }) => {
10968
+ if (sessionId && !ids.includes(sessionId))
10969
+ ids.push(sessionId);
10970
+ });
10971
+ return ids;
10972
+ }), rxjs.distinctUntilChanged(isShallowArrayEqual), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
10935
10973
  constructor(tracer) {
10936
10974
  this.tracer = tracer;
10937
10975
  }
10938
10976
  /**
10939
10977
  * Registers an audio element as blocked by the browser's autoplay policy.
10940
10978
  */
10941
- markBlocked = (audioElement, blocked) => {
10979
+ markBlocked = (audioElement, blocked, sessionId) => {
10942
10980
  setCurrentValue(this.blockedElementsSubject, (elements) => {
10943
- if (blocked)
10944
- elements.add(audioElement);
10945
- else
10946
- elements.delete(audioElement);
10947
- return elements;
10981
+ if (!blocked) {
10982
+ return elements.filter(({ element }) => element !== audioElement);
10983
+ }
10984
+ const existing = elements.find(({ element }) => element === audioElement);
10985
+ if (existing) {
10986
+ return elements.map((entry) => entry.element === audioElement
10987
+ ? { element: audioElement, sessionId: sessionId ?? entry.sessionId }
10988
+ : entry);
10989
+ }
10990
+ return [...elements, { element: audioElement, sessionId }];
10948
10991
  });
10949
10992
  };
10993
+ /**
10994
+ * Clears all tracked elements. The tracker stays usable afterwards.
10995
+ */
10996
+ reset = () => {
10997
+ if (this.blockedElementsSubject.getValue().length > 0) {
10998
+ this.blockedElementsSubject.next([]);
10999
+ }
11000
+ };
10950
11001
  /**
10951
11002
  * Returns whether the given audio element is currently flagged as blocked
10952
11003
  * by the browser's autoplay policy.
10953
11004
  */
10954
11005
  isBlocked = (audioElement) => {
10955
- return this.blockedElementsSubject.getValue().has(audioElement);
11006
+ return this.blockedElementsSubject
11007
+ .getValue()
11008
+ .some(({ element }) => element === audioElement);
10956
11009
  };
10957
11010
  /**
10958
11011
  * Plays all audio elements blocked by the browser's autoplay policy.
@@ -10961,17 +11014,18 @@ class BlockedAudioTracker {
10961
11014
  resumeAudio = async () => {
10962
11015
  this.tracer.trace('resumeAudio', null);
10963
11016
  await setCurrentValueAsync(this.blockedElementsSubject, async (elements) => {
10964
- await Promise.all(Array.from(elements, async (element) => {
11017
+ let next = elements;
11018
+ await Promise.all(elements.map(async ({ element }) => {
10965
11019
  try {
10966
11020
  if (element.srcObject)
10967
11021
  await timeboxed([element.play()], 2000);
10968
- elements.delete(element);
11022
+ next = next.filter((entry) => entry.element !== element);
10969
11023
  }
10970
11024
  catch (err) {
10971
11025
  this.logger.warn(`Can't resume audio for element`, element, err);
10972
11026
  }
10973
11027
  }));
10974
- return elements;
11028
+ return next;
10975
11029
  });
10976
11030
  };
10977
11031
  }
@@ -11162,8 +11216,10 @@ class MediaPlaybackWatchdog {
11162
11216
  tracer;
11163
11217
  controller = new AbortController();
11164
11218
  pendingTimer;
11219
+ settleTimer;
11165
11220
  attempt = 0;
11166
11221
  disposed = false;
11222
+ stopped = false;
11167
11223
  constructor(opts) {
11168
11224
  this.element = opts.element;
11169
11225
  this.kind = opts.kind;
@@ -11184,33 +11240,49 @@ class MediaPlaybackWatchdog {
11184
11240
  return;
11185
11241
  this.disposed = true;
11186
11242
  this.controller.abort();
11243
+ this.clearTimers();
11244
+ };
11245
+ clearTimers = () => {
11187
11246
  if (this.pendingTimer)
11188
11247
  clearTimeout(this.pendingTimer);
11189
11248
  this.pendingTimer = undefined;
11249
+ if (this.settleTimer)
11250
+ clearTimeout(this.settleTimer);
11251
+ this.settleTimer = undefined;
11190
11252
  };
11191
11253
  onPlaying = () => {
11192
- if (this.attempt > 0) {
11193
- this.tracer.trace('mediaPlayback.recover.success', {
11194
- kind: this.kind,
11195
- attempts: this.attempt,
11196
- });
11197
- }
11198
- this.attempt = 0;
11199
11254
  if (this.pendingTimer)
11200
11255
  clearTimeout(this.pendingTimer);
11201
11256
  this.pendingTimer = undefined;
11257
+ if (this.attempt === 0 || this.settleTimer)
11258
+ return;
11259
+ this.settleTimer ??= setTimeout(this.onSettled, 1000);
11260
+ };
11261
+ onSettled = () => {
11262
+ this.settleTimer = undefined;
11263
+ if (this.element.paused)
11264
+ return;
11265
+ this.tracer.trace('mediaPlayback.recover.success', {
11266
+ kind: this.kind,
11267
+ attempts: this.attempt,
11268
+ });
11269
+ this.attempt = 0;
11270
+ this.stopped = false;
11202
11271
  };
11203
11272
  onPauseOrSuspend = (event) => {
11204
- if (this.disposed)
11273
+ if (this.disposed || this.stopped)
11205
11274
  return;
11206
11275
  this.tracer.trace('mediaPlayback.paused', {
11207
11276
  kind: this.kind,
11208
11277
  reason: event.type,
11209
11278
  });
11279
+ if (this.settleTimer)
11280
+ clearTimeout(this.settleTimer);
11281
+ this.settleTimer = undefined;
11210
11282
  this.scheduleRecovery();
11211
11283
  };
11212
11284
  scheduleRecovery = () => {
11213
- if (this.disposed || this.pendingTimer)
11285
+ if (this.disposed || this.stopped || this.pendingTimer)
11214
11286
  return;
11215
11287
  const skipReason = this.computeSkipReason();
11216
11288
  if (skipReason) {
@@ -11220,6 +11292,15 @@ class MediaPlaybackWatchdog {
11220
11292
  });
11221
11293
  return;
11222
11294
  }
11295
+ if (this.attempt >= 10) {
11296
+ this.stopped = true;
11297
+ this.clearTimers();
11298
+ this.tracer.trace('mediaPlayback.recover.giveUp', {
11299
+ kind: this.kind,
11300
+ attempts: this.attempt,
11301
+ });
11302
+ return;
11303
+ }
11223
11304
  const delay = this.attempt === 0 ? 0 : retryInterval(this.attempt);
11224
11305
  this.pendingTimer = setTimeout(this.attemptPlay, delay);
11225
11306
  };
@@ -11255,13 +11336,6 @@ class MediaPlaybackWatchdog {
11255
11336
  if (this.disposed)
11256
11337
  return;
11257
11338
  this.logger.warn(`Failed to recover ${this.kind} playback`, err);
11258
- if (this.attempt >= 10) {
11259
- this.tracer.trace('mediaPlayback.recover.giveUp', {
11260
- kind: this.kind,
11261
- attempts: this.attempt,
11262
- });
11263
- return;
11264
- }
11265
11339
  this.scheduleRecovery();
11266
11340
  }
11267
11341
  };
@@ -11503,6 +11577,12 @@ class DynascaleManager {
11503
11577
  let sourceNode = undefined;
11504
11578
  let gainNode = undefined;
11505
11579
  let audioWatchdog = undefined;
11580
+ const clearBlockedAudio = () => {
11581
+ if (!this.blockedAudioTracker.isBlocked(audioElement))
11582
+ return;
11583
+ this.blockedAudioTracker.markBlocked(audioElement, false);
11584
+ };
11585
+ audioElement.addEventListener('playing', clearBlockedAudio);
11506
11586
  const isAudioTrack = trackType === 'audioTrack';
11507
11587
  const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
11508
11588
  const updateMediaStreamSubscription = participant$
@@ -11516,7 +11596,7 @@ class DynascaleManager {
11516
11596
  audioWatchdog?.dispose();
11517
11597
  audioWatchdog = undefined;
11518
11598
  if (!source) {
11519
- this.blockedAudioTracker.markBlocked(audioElement, false);
11599
+ clearBlockedAudio();
11520
11600
  return;
11521
11601
  }
11522
11602
  // Safari has a special quirk that prevents playing audio until the user
@@ -11544,7 +11624,7 @@ class DynascaleManager {
11544
11624
  this.tracer.trace('audioPlaybackError', e.message);
11545
11625
  if (e.name === 'NotAllowedError') {
11546
11626
  this.tracer.trace('audioPlaybackBlocked', null);
11547
- this.blockedAudioTracker.markBlocked(audioElement, true);
11627
+ this.blockedAudioTracker.markBlocked(audioElement, true, sessionId);
11548
11628
  }
11549
11629
  this.logger.warn(`Failed to play audio stream`, e);
11550
11630
  });
@@ -11577,7 +11657,8 @@ class DynascaleManager {
11577
11657
  });
11578
11658
  audioElement.autoplay = true;
11579
11659
  return () => {
11580
- this.blockedAudioTracker.markBlocked(audioElement, false);
11660
+ audioElement.removeEventListener('playing', clearBlockedAudio);
11661
+ clearBlockedAudio();
11581
11662
  sinkIdSubscription?.unsubscribe();
11582
11663
  volumeSubscription.unsubscribe();
11583
11664
  updateMediaStreamSubscription.unsubscribe();
@@ -14752,6 +14833,7 @@ class Call {
14752
14833
  this.screenShare = new ScreenShareManager(this);
14753
14834
  this.trackSubscriptionManager = new TrackSubscriptionManager(this.state, this.tracer);
14754
14835
  this.blockedAudioTracker = new BlockedAudioTracker(this.tracer);
14836
+ this.leaveCallHooks.add(this.blockedAudioTracker.reset);
14755
14837
  if (typeof document !== 'undefined') {
14756
14838
  this.audioBindingsWatchdog = new AudioBindingsWatchdog(this.state, this.tracer);
14757
14839
  this.viewportTracker = new ViewportTracker(this.state);
@@ -18448,11 +18530,11 @@ class StreamClient {
18448
18530
  return await this.wsConnection.connect(this.defaultWSTimeout);
18449
18531
  };
18450
18532
  getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
18451
- "1.55.2";
18533
+ "1.57.0";
18452
18534
  getUserAgent = () => {
18453
18535
  if (!this.cachedUserAgent) {
18454
18536
  const { clientAppIdentifier = {} } = this.options;
18455
- const { sdkName = 'js', sdkVersion = "1.55.2", ...extras } = clientAppIdentifier;
18537
+ const { sdkName = 'js', sdkVersion = "1.57.0", ...extras } = clientAppIdentifier;
18456
18538
  this.cachedUserAgent = [
18457
18539
  `stream-video-${sdkName}-v${sdkVersion}`,
18458
18540
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -18789,8 +18871,8 @@ class ClientEventReporter {
18789
18871
  applyErrorIfAbsent(this.wsPairs.get(cid), stageError);
18790
18872
  this.failCoordinator(cid);
18791
18873
  this.failWs(cid);
18792
- this.emitPeerConnectionFailure(cid, 'publish', code, reason, 'NOT_CONNECTED');
18793
- this.emitPeerConnectionFailure(cid, 'subscribe', code, reason, 'NOT_CONNECTED');
18874
+ this.emitPeerConnectionFailure(cid, 'publish', code, reason);
18875
+ this.emitPeerConnectionFailure(cid, 'subscribe', code, reason);
18794
18876
  }
18795
18877
  catch (err) {
18796
18878
  this.logger.warn('Failed to report abort', err);
@@ -18802,7 +18884,7 @@ class ClientEventReporter {
18802
18884
  if (this.wsPairs.get(cid))
18803
18885
  this.failWs(cid);
18804
18886
  for (const role of ['publish', 'subscribe']) {
18805
- this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt', 'NOT_CONNECTED');
18887
+ this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt');
18806
18888
  }
18807
18889
  };
18808
18890
  emitJoinInitiated = (cid) => {
@@ -18964,21 +19046,24 @@ class ClientEventReporter {
18964
19046
  };
18965
19047
  onPeerConnectionStateChange = (cid, event) => {
18966
19048
  const role = event.peerType === PeerType.SUBSCRIBER ? 'subscribe' : 'publish';
18967
- if (event.stateType === 'ice' && event.state === 'failed') {
18968
- this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed', 'FAILED');
19049
+ const pair = this.peerConnectionPairs.get(pcKey(cid, role));
19050
+ if (pair)
19051
+ pair.lastIceState = event.iceConnectionState;
19052
+ if (event.stateType === 'ice') {
19053
+ if (event.state === 'failed') {
19054
+ this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed');
19055
+ }
18969
19056
  return;
18970
19057
  }
18971
- if (event.stateType === 'peerConnection' && event.state === 'failed') {
18972
- this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed', 'CONNECTED');
19058
+ if (event.state === 'failed') {
19059
+ this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed');
18973
19060
  return;
18974
19061
  }
18975
- if (event.stateType !== 'peerConnection')
18976
- return;
18977
19062
  switch (event.state) {
18978
19063
  case 'connecting':
18979
- if (this.peerConnectionPairs.has(pcKey(cid, role)))
19064
+ if (pair)
18980
19065
  return;
18981
- this.openPeerConnectionPair(cid, role);
19066
+ this.openPeerConnectionPair(cid, role, event.iceConnectionState);
18982
19067
  break;
18983
19068
  case 'connected':
18984
19069
  this.emitPeerConnectionSuccess(cid, role);
@@ -18986,7 +19071,7 @@ class ClientEventReporter {
18986
19071
  break;
18987
19072
  }
18988
19073
  };
18989
- openPeerConnectionPair = (cid, role) => {
19074
+ openPeerConnectionPair = (cid, role, iceConnectionState) => {
18990
19075
  const key = pcKey(cid, role);
18991
19076
  const pair = {
18992
19077
  sid: generateUUIDv4(),
@@ -18996,6 +19081,7 @@ class ClientEventReporter {
18996
19081
  sfuId: this.getSfuId(cid),
18997
19082
  userSessionId: this.getUserSessionId(cid),
18998
19083
  wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
19084
+ lastIceState: iceConnectionState,
18999
19085
  };
19000
19086
  this.peerConnectionPairs.set(key, pair);
19001
19087
  this.sendForCall(cid, {
@@ -19031,7 +19117,7 @@ class ClientEventReporter {
19031
19117
  });
19032
19118
  this.peerConnectionPairs.delete(key);
19033
19119
  };
19034
- emitPeerConnectionFailure = (cid, role, code, reason, iceState) => {
19120
+ emitPeerConnectionFailure = (cid, role, code, reason) => {
19035
19121
  const key = pcKey(cid, role);
19036
19122
  const pair = this.peerConnectionPairs.get(key);
19037
19123
  if (!pair)
@@ -19049,7 +19135,7 @@ class ClientEventReporter {
19049
19135
  outcome: 'failure',
19050
19136
  retry_count_attempt: 0,
19051
19137
  elapsed_time: Date.now() - pair.startedAt,
19052
- ice_state: iceState,
19138
+ ice_state: toReportedIceState(pair.lastIceState),
19053
19139
  retry_failure_reason: reason,
19054
19140
  retry_failure_code: code,
19055
19141
  });
@@ -19115,6 +19201,17 @@ class ClientEventReporter {
19115
19201
  return false;
19116
19202
  };
19117
19203
  }
19204
+ const toReportedIceState = (state) => {
19205
+ switch (state) {
19206
+ case 'connected':
19207
+ case 'completed':
19208
+ return 'CONNECTED';
19209
+ case 'failed':
19210
+ return 'FAILED';
19211
+ default:
19212
+ return 'NOT_CONNECTED';
19213
+ }
19214
+ };
19118
19215
  const readPermissionStatus = (permission) => {
19119
19216
  const state = getCurrentValue(permission.asStateObservable());
19120
19217
  switch (state) {