@stream-io/video-client 1.55.2 → 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.2";
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);
@@ -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
  }
@@ -11503,6 +11557,12 @@ class DynascaleManager {
11503
11557
  let sourceNode = undefined;
11504
11558
  let gainNode = undefined;
11505
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);
11506
11566
  const isAudioTrack = trackType === 'audioTrack';
11507
11567
  const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
11508
11568
  const updateMediaStreamSubscription = participant$
@@ -11516,7 +11576,7 @@ class DynascaleManager {
11516
11576
  audioWatchdog?.dispose();
11517
11577
  audioWatchdog = undefined;
11518
11578
  if (!source) {
11519
- this.blockedAudioTracker.markBlocked(audioElement, false);
11579
+ clearBlockedAudio();
11520
11580
  return;
11521
11581
  }
11522
11582
  // Safari has a special quirk that prevents playing audio until the user
@@ -11544,7 +11604,7 @@ class DynascaleManager {
11544
11604
  this.tracer.trace('audioPlaybackError', e.message);
11545
11605
  if (e.name === 'NotAllowedError') {
11546
11606
  this.tracer.trace('audioPlaybackBlocked', null);
11547
- this.blockedAudioTracker.markBlocked(audioElement, true);
11607
+ this.blockedAudioTracker.markBlocked(audioElement, true, sessionId);
11548
11608
  }
11549
11609
  this.logger.warn(`Failed to play audio stream`, e);
11550
11610
  });
@@ -11577,7 +11637,8 @@ class DynascaleManager {
11577
11637
  });
11578
11638
  audioElement.autoplay = true;
11579
11639
  return () => {
11580
- this.blockedAudioTracker.markBlocked(audioElement, false);
11640
+ audioElement.removeEventListener('playing', clearBlockedAudio);
11641
+ clearBlockedAudio();
11581
11642
  sinkIdSubscription?.unsubscribe();
11582
11643
  volumeSubscription.unsubscribe();
11583
11644
  updateMediaStreamSubscription.unsubscribe();
@@ -14752,6 +14813,7 @@ class Call {
14752
14813
  this.screenShare = new ScreenShareManager(this);
14753
14814
  this.trackSubscriptionManager = new TrackSubscriptionManager(this.state, this.tracer);
14754
14815
  this.blockedAudioTracker = new BlockedAudioTracker(this.tracer);
14816
+ this.leaveCallHooks.add(this.blockedAudioTracker.reset);
14755
14817
  if (typeof document !== 'undefined') {
14756
14818
  this.audioBindingsWatchdog = new AudioBindingsWatchdog(this.state, this.tracer);
14757
14819
  this.viewportTracker = new ViewportTracker(this.state);
@@ -18448,11 +18510,11 @@ class StreamClient {
18448
18510
  return await this.wsConnection.connect(this.defaultWSTimeout);
18449
18511
  };
18450
18512
  getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
18451
- "1.55.2";
18513
+ "1.56.0";
18452
18514
  getUserAgent = () => {
18453
18515
  if (!this.cachedUserAgent) {
18454
18516
  const { clientAppIdentifier = {} } = this.options;
18455
- const { sdkName = 'js', sdkVersion = "1.55.2", ...extras } = clientAppIdentifier;
18517
+ const { sdkName = 'js', sdkVersion = "1.56.0", ...extras } = clientAppIdentifier;
18456
18518
  this.cachedUserAgent = [
18457
18519
  `stream-video-${sdkName}-v${sdkVersion}`,
18458
18520
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -18789,8 +18851,8 @@ class ClientEventReporter {
18789
18851
  applyErrorIfAbsent(this.wsPairs.get(cid), stageError);
18790
18852
  this.failCoordinator(cid);
18791
18853
  this.failWs(cid);
18792
- this.emitPeerConnectionFailure(cid, 'publish', code, reason, 'NOT_CONNECTED');
18793
- this.emitPeerConnectionFailure(cid, 'subscribe', code, reason, 'NOT_CONNECTED');
18854
+ this.emitPeerConnectionFailure(cid, 'publish', code, reason);
18855
+ this.emitPeerConnectionFailure(cid, 'subscribe', code, reason);
18794
18856
  }
18795
18857
  catch (err) {
18796
18858
  this.logger.warn('Failed to report abort', err);
@@ -18802,7 +18864,7 @@ class ClientEventReporter {
18802
18864
  if (this.wsPairs.get(cid))
18803
18865
  this.failWs(cid);
18804
18866
  for (const role of ['publish', 'subscribe']) {
18805
- 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');
18806
18868
  }
18807
18869
  };
18808
18870
  emitJoinInitiated = (cid) => {
@@ -18964,21 +19026,24 @@ class ClientEventReporter {
18964
19026
  };
18965
19027
  onPeerConnectionStateChange = (cid, event) => {
18966
19028
  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');
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
+ }
18969
19036
  return;
18970
19037
  }
18971
- if (event.stateType === 'peerConnection' && event.state === 'failed') {
18972
- 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');
18973
19040
  return;
18974
19041
  }
18975
- if (event.stateType !== 'peerConnection')
18976
- return;
18977
19042
  switch (event.state) {
18978
19043
  case 'connecting':
18979
- if (this.peerConnectionPairs.has(pcKey(cid, role)))
19044
+ if (pair)
18980
19045
  return;
18981
- this.openPeerConnectionPair(cid, role);
19046
+ this.openPeerConnectionPair(cid, role, event.iceConnectionState);
18982
19047
  break;
18983
19048
  case 'connected':
18984
19049
  this.emitPeerConnectionSuccess(cid, role);
@@ -18986,7 +19051,7 @@ class ClientEventReporter {
18986
19051
  break;
18987
19052
  }
18988
19053
  };
18989
- openPeerConnectionPair = (cid, role) => {
19054
+ openPeerConnectionPair = (cid, role, iceConnectionState) => {
18990
19055
  const key = pcKey(cid, role);
18991
19056
  const pair = {
18992
19057
  sid: generateUUIDv4(),
@@ -18996,6 +19061,7 @@ class ClientEventReporter {
18996
19061
  sfuId: this.getSfuId(cid),
18997
19062
  userSessionId: this.getUserSessionId(cid),
18998
19063
  wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
19064
+ lastIceState: iceConnectionState,
18999
19065
  };
19000
19066
  this.peerConnectionPairs.set(key, pair);
19001
19067
  this.sendForCall(cid, {
@@ -19031,7 +19097,7 @@ class ClientEventReporter {
19031
19097
  });
19032
19098
  this.peerConnectionPairs.delete(key);
19033
19099
  };
19034
- emitPeerConnectionFailure = (cid, role, code, reason, iceState) => {
19100
+ emitPeerConnectionFailure = (cid, role, code, reason) => {
19035
19101
  const key = pcKey(cid, role);
19036
19102
  const pair = this.peerConnectionPairs.get(key);
19037
19103
  if (!pair)
@@ -19049,7 +19115,7 @@ class ClientEventReporter {
19049
19115
  outcome: 'failure',
19050
19116
  retry_count_attempt: 0,
19051
19117
  elapsed_time: Date.now() - pair.startedAt,
19052
- ice_state: iceState,
19118
+ ice_state: toReportedIceState(pair.lastIceState),
19053
19119
  retry_failure_reason: reason,
19054
19120
  retry_failure_code: code,
19055
19121
  });
@@ -19115,6 +19181,17 @@ class ClientEventReporter {
19115
19181
  return false;
19116
19182
  };
19117
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
+ };
19118
19195
  const readPermissionStatus = (permission) => {
19119
19196
  const state = getCurrentValue(permission.asStateObservable());
19120
19197
  switch (state) {