@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/dist/index.cjs.js CHANGED
@@ -4946,7 +4946,7 @@ const toIceCandidate = (iceTrickle) => {
4946
4946
  }
4947
4947
  };
4948
4948
 
4949
- const version = "1.55.1";
4949
+ const version = "1.56.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);
@@ -6301,6 +6302,12 @@ class CallState {
6301
6302
  // trigger re-sorting of participants
6302
6303
  this.setCurrentValue(this.participantsSubject, (ps) => ps);
6303
6304
  };
6305
+ /**
6306
+ * Returns the comparator currently used to sort the participants.
6307
+ */
6308
+ getSortParticipantsBy = () => {
6309
+ return this.sortParticipantsBy;
6310
+ };
6304
6311
  /**
6305
6312
  * Gets the current value of an observable, or undefined if the observable has
6306
6313
  * not emitted a value yet.
@@ -8485,6 +8492,7 @@ class BasePeerConnection {
8485
8492
  try {
8486
8493
  this.onPeerConnectionStateChange?.({
8487
8494
  peerType: this.peerType,
8495
+ iceConnectionState: this.pc.iceConnectionState,
8488
8496
  ...event,
8489
8497
  });
8490
8498
  }
@@ -8939,6 +8947,7 @@ class Publisher extends BasePeerConnection {
8939
8947
  clonedTracks = new Set();
8940
8948
  publishOptions;
8941
8949
  selfSubEnabled;
8950
+ transceiverLockKey = `pub.tx.${this.lock}`;
8942
8951
  /**
8943
8952
  * Constructs a new `Publisher` instance.
8944
8953
  */
@@ -8994,32 +9003,34 @@ class Publisher extends BasePeerConnection {
8994
9003
  * @param options the publish options to use.
8995
9004
  */
8996
9005
  publish = async (track, trackType, options = {}) => {
8997
- if (!this.publishOptions.some((o) => o.trackType === trackType)) {
8998
- throw new Error(`No publish options found for ${TrackType[trackType]}`);
8999
- }
9000
- for (const publishOption of this.publishOptions) {
9001
- if (publishOption.trackType !== trackType)
9002
- continue;
9003
- // create a clone of the track as otherwise the same trackId will
9004
- // appear in the SDP in multiple transceivers
9005
- const trackToPublish = this.cloneTrack(track);
9006
- const bundle = this.transceiverCache.get(publishOption);
9007
- if (!bundle) {
9008
- 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]}`);
9009
9010
  }
9010
- else {
9011
- const previousTrack = bundle.transceiver.sender.track;
9012
- await this.updateTransceiver(bundle, trackToPublish, options);
9013
- if (!isReactNative()) {
9014
- 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
+ }
9015
9025
  }
9016
9026
  }
9017
- }
9027
+ });
9018
9028
  };
9019
9029
  /**
9020
9030
  * Adds a new transceiver carrying the given track to the peer connection.
9021
9031
  */
9022
9032
  addTransceiver = async (track, publishOption, options) => {
9033
+ const trackType = publishOption.trackType;
9023
9034
  const encodings = isAudioTrackType(publishOption.trackType)
9024
9035
  ? computeAudioLayers(publishOption, options)
9025
9036
  : computeVideoLayers(track, publishOption);
@@ -9030,15 +9041,17 @@ class Publisher extends BasePeerConnection {
9030
9041
  direction: 'sendonly',
9031
9042
  sendEncodings,
9032
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`);
9033
9050
  const params = transceiver.sender.getParameters();
9034
9051
  params.degradationPreference =
9035
9052
  toRTCDegradationPreference(publishOption.degradationPreference) ??
9036
9053
  'maintain-framerate';
9037
9054
  await transceiver.sender.setParameters(params);
9038
- const trackType = publishOption.trackType;
9039
- this.logger.debug(`Added ${TrackType[trackType]} transceiver`);
9040
- this.transceiverCache.add({ publishOption, transceiver, options });
9041
- this.trackIdToTrackType.set(track.id, trackType);
9042
9055
  await this.negotiate();
9043
9056
  };
9044
9057
  /**
@@ -9095,33 +9108,35 @@ class Publisher extends BasePeerConnection {
9095
9108
  * Synchronizes the current Publisher state with the provided publish options.
9096
9109
  */
9097
9110
  syncPublishOptions = async () => {
9098
- // enable publishing with new options -> [av1, vp9]
9099
- for (const publishOption of this.publishOptions) {
9100
- const { trackType } = publishOption;
9101
- if (!this.isPublishing(trackType))
9102
- continue;
9103
- if (this.transceiverCache.has(publishOption))
9104
- continue;
9105
- const item = this.transceiverCache.find((i) => !!i.transceiver.sender.track &&
9106
- i.publishOption.trackType === trackType);
9107
- if (!item)
9108
- continue;
9109
- // take the track from the existing transceiver for the same track type,
9110
- // clone it and publish it with the new publish options
9111
- const track = this.cloneTrack(item.transceiver.sender.track);
9112
- await this.addTransceiver(track, publishOption, item.options);
9113
- }
9114
- // stop publishing with options not required anymore -> [vp9]
9115
- for (const item of this.transceiverCache.items()) {
9116
- const { publishOption, transceiver } = item;
9117
- const hasPublishOption = this.publishOptions.some((option) => option.id === publishOption.id &&
9118
- option.trackType === publishOption.trackType);
9119
- if (hasPublishOption)
9120
- continue;
9121
- // it is safe to stop the track here, it is a clone
9122
- this.stopTrack(transceiver.sender.track);
9123
- await this.updateTransceiver(item, null);
9124
- }
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
+ });
9125
9140
  };
9126
9141
  /**
9127
9142
  * Returns true if the given track type is currently being published to the SFU.
@@ -9289,12 +9304,23 @@ class Publisher extends BasePeerConnection {
9289
9304
  negotiate = async (options) => {
9290
9305
  return withoutConcurrency(`publisher.negotiate.${this.lock}`, async () => {
9291
9306
  const offer = await this.pc.createOffer(options);
9292
- const tracks = this.getAnnouncedTracks(offer.sdp);
9293
- if (!tracks.length)
9294
- throw new Error(`Can't negotiate without any tracks`);
9295
9307
  try {
9296
9308
  this.isIceRestarting = options?.iceRestart ?? false;
9297
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`);
9298
9324
  const { sdp: baseSdp = '' } = offer;
9299
9325
  const { dangerouslyForceCodec, dangerouslySetStartBitrateFactor, fmtpLine, } = this.clientPublishOptions || {};
9300
9326
  let sdp = dangerouslyForceCodec
@@ -10919,34 +10945,67 @@ class AudioBindingsWatchdog {
10919
10945
  class BlockedAudioTracker {
10920
10946
  logger = videoLoggerSystem.getLogger('BlockedAudioTracker');
10921
10947
  tracer;
10922
- blockedElementsSubject = new rxjs.BehaviorSubject(new Set());
10948
+ blockedElementsSubject = new rxjs.BehaviorSubject([]);
10923
10949
  /**
10924
10950
  * Whether the browser's autoplay policy is blocking audio playback.
10925
10951
  * Will be `true` when at least one audio element is currently blocked.
10926
10952
  * Use {@link resumeAudio} within a user gesture to unblock.
10927
10953
  */
10928
- 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 }));
10929
10973
  constructor(tracer) {
10930
10974
  this.tracer = tracer;
10931
10975
  }
10932
10976
  /**
10933
10977
  * Registers an audio element as blocked by the browser's autoplay policy.
10934
10978
  */
10935
- markBlocked = (audioElement, blocked) => {
10979
+ markBlocked = (audioElement, blocked, sessionId) => {
10936
10980
  setCurrentValue(this.blockedElementsSubject, (elements) => {
10937
- if (blocked)
10938
- elements.add(audioElement);
10939
- else
10940
- elements.delete(audioElement);
10941
- 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 }];
10942
10991
  });
10943
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
+ };
10944
11001
  /**
10945
11002
  * Returns whether the given audio element is currently flagged as blocked
10946
11003
  * by the browser's autoplay policy.
10947
11004
  */
10948
11005
  isBlocked = (audioElement) => {
10949
- return this.blockedElementsSubject.getValue().has(audioElement);
11006
+ return this.blockedElementsSubject
11007
+ .getValue()
11008
+ .some(({ element }) => element === audioElement);
10950
11009
  };
10951
11010
  /**
10952
11011
  * Plays all audio elements blocked by the browser's autoplay policy.
@@ -10955,17 +11014,18 @@ class BlockedAudioTracker {
10955
11014
  resumeAudio = async () => {
10956
11015
  this.tracer.trace('resumeAudio', null);
10957
11016
  await setCurrentValueAsync(this.blockedElementsSubject, async (elements) => {
10958
- await Promise.all(Array.from(elements, async (element) => {
11017
+ let next = elements;
11018
+ await Promise.all(elements.map(async ({ element }) => {
10959
11019
  try {
10960
11020
  if (element.srcObject)
10961
11021
  await timeboxed([element.play()], 2000);
10962
- elements.delete(element);
11022
+ next = next.filter((entry) => entry.element !== element);
10963
11023
  }
10964
11024
  catch (err) {
10965
11025
  this.logger.warn(`Can't resume audio for element`, element, err);
10966
11026
  }
10967
11027
  }));
10968
- return elements;
11028
+ return next;
10969
11029
  });
10970
11030
  };
10971
11031
  }
@@ -11497,6 +11557,12 @@ class DynascaleManager {
11497
11557
  let sourceNode = undefined;
11498
11558
  let gainNode = undefined;
11499
11559
  let audioWatchdog = undefined;
11560
+ const clearBlockedAudio = () => {
11561
+ if (!this.blockedAudioTracker.isBlocked(audioElement))
11562
+ return;
11563
+ this.blockedAudioTracker.markBlocked(audioElement, false);
11564
+ };
11565
+ audioElement.addEventListener('playing', clearBlockedAudio);
11500
11566
  const isAudioTrack = trackType === 'audioTrack';
11501
11567
  const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
11502
11568
  const updateMediaStreamSubscription = participant$
@@ -11510,7 +11576,7 @@ class DynascaleManager {
11510
11576
  audioWatchdog?.dispose();
11511
11577
  audioWatchdog = undefined;
11512
11578
  if (!source) {
11513
- this.blockedAudioTracker.markBlocked(audioElement, false);
11579
+ clearBlockedAudio();
11514
11580
  return;
11515
11581
  }
11516
11582
  // Safari has a special quirk that prevents playing audio until the user
@@ -11538,7 +11604,7 @@ class DynascaleManager {
11538
11604
  this.tracer.trace('audioPlaybackError', e.message);
11539
11605
  if (e.name === 'NotAllowedError') {
11540
11606
  this.tracer.trace('audioPlaybackBlocked', null);
11541
- this.blockedAudioTracker.markBlocked(audioElement, true);
11607
+ this.blockedAudioTracker.markBlocked(audioElement, true, sessionId);
11542
11608
  }
11543
11609
  this.logger.warn(`Failed to play audio stream`, e);
11544
11610
  });
@@ -11571,7 +11637,8 @@ class DynascaleManager {
11571
11637
  });
11572
11638
  audioElement.autoplay = true;
11573
11639
  return () => {
11574
- this.blockedAudioTracker.markBlocked(audioElement, false);
11640
+ audioElement.removeEventListener('playing', clearBlockedAudio);
11641
+ clearBlockedAudio();
11575
11642
  sinkIdSubscription?.unsubscribe();
11576
11643
  volumeSubscription.unsubscribe();
11577
11644
  updateMediaStreamSubscription.unsubscribe();
@@ -14746,6 +14813,7 @@ class Call {
14746
14813
  this.screenShare = new ScreenShareManager(this);
14747
14814
  this.trackSubscriptionManager = new TrackSubscriptionManager(this.state, this.tracer);
14748
14815
  this.blockedAudioTracker = new BlockedAudioTracker(this.tracer);
14816
+ this.leaveCallHooks.add(this.blockedAudioTracker.reset);
14749
14817
  if (typeof document !== 'undefined') {
14750
14818
  this.audioBindingsWatchdog = new AudioBindingsWatchdog(this.state, this.tracer);
14751
14819
  this.viewportTracker = new ViewportTracker(this.state);
@@ -16369,6 +16437,12 @@ class Call {
16369
16437
  setSortParticipantsBy = (criteria) => {
16370
16438
  return this.state.setSortParticipantsBy(criteria);
16371
16439
  };
16440
+ /**
16441
+ * Returns the comparator currently used to sort the participants.
16442
+ */
16443
+ getSortParticipantsBy = () => {
16444
+ return this.state.getSortParticipantsBy();
16445
+ };
16372
16446
  /**
16373
16447
  * Sends a reaction to the other call participants.
16374
16448
  *
@@ -18436,11 +18510,11 @@ class StreamClient {
18436
18510
  return await this.wsConnection.connect(this.defaultWSTimeout);
18437
18511
  };
18438
18512
  getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
18439
- "1.55.1";
18513
+ "1.56.0";
18440
18514
  getUserAgent = () => {
18441
18515
  if (!this.cachedUserAgent) {
18442
18516
  const { clientAppIdentifier = {} } = this.options;
18443
- const { sdkName = 'js', sdkVersion = "1.55.1", ...extras } = clientAppIdentifier;
18517
+ const { sdkName = 'js', sdkVersion = "1.56.0", ...extras } = clientAppIdentifier;
18444
18518
  this.cachedUserAgent = [
18445
18519
  `stream-video-${sdkName}-v${sdkVersion}`,
18446
18520
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -18777,8 +18851,8 @@ class ClientEventReporter {
18777
18851
  applyErrorIfAbsent(this.wsPairs.get(cid), stageError);
18778
18852
  this.failCoordinator(cid);
18779
18853
  this.failWs(cid);
18780
- this.emitPeerConnectionFailure(cid, 'publish', code, reason, 'NOT_CONNECTED');
18781
- this.emitPeerConnectionFailure(cid, 'subscribe', code, reason, 'NOT_CONNECTED');
18854
+ this.emitPeerConnectionFailure(cid, 'publish', code, reason);
18855
+ this.emitPeerConnectionFailure(cid, 'subscribe', code, reason);
18782
18856
  }
18783
18857
  catch (err) {
18784
18858
  this.logger.warn('Failed to report abort', err);
@@ -18790,7 +18864,7 @@ class ClientEventReporter {
18790
18864
  if (this.wsPairs.get(cid))
18791
18865
  this.failWs(cid);
18792
18866
  for (const role of ['publish', 'subscribe']) {
18793
- this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt', 'NOT_CONNECTED');
18867
+ this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt');
18794
18868
  }
18795
18869
  };
18796
18870
  emitJoinInitiated = (cid) => {
@@ -18952,21 +19026,24 @@ class ClientEventReporter {
18952
19026
  };
18953
19027
  onPeerConnectionStateChange = (cid, event) => {
18954
19028
  const role = event.peerType === PeerType.SUBSCRIBER ? 'subscribe' : 'publish';
18955
- if (event.stateType === 'ice' && event.state === 'failed') {
18956
- this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed', 'FAILED');
19029
+ const pair = this.peerConnectionPairs.get(pcKey(cid, role));
19030
+ if (pair)
19031
+ pair.lastIceState = event.iceConnectionState;
19032
+ if (event.stateType === 'ice') {
19033
+ if (event.state === 'failed') {
19034
+ this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed');
19035
+ }
18957
19036
  return;
18958
19037
  }
18959
- if (event.stateType === 'peerConnection' && event.state === 'failed') {
18960
- this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed', 'CONNECTED');
19038
+ if (event.state === 'failed') {
19039
+ this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed');
18961
19040
  return;
18962
19041
  }
18963
- if (event.stateType !== 'peerConnection')
18964
- return;
18965
19042
  switch (event.state) {
18966
19043
  case 'connecting':
18967
- if (this.peerConnectionPairs.has(pcKey(cid, role)))
19044
+ if (pair)
18968
19045
  return;
18969
- this.openPeerConnectionPair(cid, role);
19046
+ this.openPeerConnectionPair(cid, role, event.iceConnectionState);
18970
19047
  break;
18971
19048
  case 'connected':
18972
19049
  this.emitPeerConnectionSuccess(cid, role);
@@ -18974,7 +19051,7 @@ class ClientEventReporter {
18974
19051
  break;
18975
19052
  }
18976
19053
  };
18977
- openPeerConnectionPair = (cid, role) => {
19054
+ openPeerConnectionPair = (cid, role, iceConnectionState) => {
18978
19055
  const key = pcKey(cid, role);
18979
19056
  const pair = {
18980
19057
  sid: generateUUIDv4(),
@@ -18984,6 +19061,7 @@ class ClientEventReporter {
18984
19061
  sfuId: this.getSfuId(cid),
18985
19062
  userSessionId: this.getUserSessionId(cid),
18986
19063
  wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
19064
+ lastIceState: iceConnectionState,
18987
19065
  };
18988
19066
  this.peerConnectionPairs.set(key, pair);
18989
19067
  this.sendForCall(cid, {
@@ -19019,7 +19097,7 @@ class ClientEventReporter {
19019
19097
  });
19020
19098
  this.peerConnectionPairs.delete(key);
19021
19099
  };
19022
- emitPeerConnectionFailure = (cid, role, code, reason, iceState) => {
19100
+ emitPeerConnectionFailure = (cid, role, code, reason) => {
19023
19101
  const key = pcKey(cid, role);
19024
19102
  const pair = this.peerConnectionPairs.get(key);
19025
19103
  if (!pair)
@@ -19037,7 +19115,7 @@ class ClientEventReporter {
19037
19115
  outcome: 'failure',
19038
19116
  retry_count_attempt: 0,
19039
19117
  elapsed_time: Date.now() - pair.startedAt,
19040
- ice_state: iceState,
19118
+ ice_state: toReportedIceState(pair.lastIceState),
19041
19119
  retry_failure_reason: reason,
19042
19120
  retry_failure_code: code,
19043
19121
  });
@@ -19103,6 +19181,17 @@ class ClientEventReporter {
19103
19181
  return false;
19104
19182
  };
19105
19183
  }
19184
+ const toReportedIceState = (state) => {
19185
+ switch (state) {
19186
+ case 'connected':
19187
+ case 'completed':
19188
+ return 'CONNECTED';
19189
+ case 'failed':
19190
+ return 'FAILED';
19191
+ default:
19192
+ return 'NOT_CONNECTED';
19193
+ }
19194
+ };
19106
19195
  const readPermissionStatus = (permission) => {
19107
19196
  const state = getCurrentValue(permission.asStateObservable());
19108
19197
  switch (state) {