@stream-io/video-client 1.52.1-beta.0 → 1.53.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 (67) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/index.browser.es.js +801 -123
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +801 -122
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +801 -123
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +6 -14
  9. package/dist/src/StreamVideoClient.d.ts +2 -0
  10. package/dist/src/coordinator/connection/client.d.ts +1 -0
  11. package/dist/src/errors/SfuTimeoutError.d.ts +8 -0
  12. package/dist/src/errors/index.d.ts +1 -0
  13. package/dist/src/gen/google/protobuf/struct.d.ts +1 -3
  14. package/dist/src/gen/google/protobuf/timestamp.d.ts +1 -3
  15. package/dist/src/gen/video/sfu/event/events.d.ts +1 -22
  16. package/dist/src/gen/video/sfu/models/models.d.ts +0 -4
  17. package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +2 -23
  18. package/dist/src/helpers/firstVideoFrame.d.ts +7 -0
  19. package/dist/src/reporting/ClientEventReporter.d.ts +84 -0
  20. package/dist/src/reporting/index.d.ts +1 -0
  21. package/dist/src/rtc/BasePeerConnection.d.ts +5 -2
  22. package/dist/src/rtc/Publisher.d.ts +1 -4
  23. package/dist/src/rtc/Subscriber.d.ts +0 -7
  24. package/dist/src/rtc/types.d.ts +24 -1
  25. package/dist/src/types.d.ts +5 -0
  26. package/package.json +1 -1
  27. package/src/Call.ts +185 -106
  28. package/src/StreamSfuClient.ts +3 -3
  29. package/src/StreamVideoClient.ts +18 -3
  30. package/src/__tests__/Call.autodrop.test.ts +4 -1
  31. package/src/__tests__/Call.lifecycle.test.ts +4 -1
  32. package/src/__tests__/Call.publishing.test.ts +4 -1
  33. package/src/__tests__/Call.test.ts +23 -0
  34. package/src/coordinator/connection/client.ts +5 -0
  35. package/src/devices/__tests__/CameraManager.test.ts +10 -1
  36. package/src/devices/__tests__/DeviceManager.test.ts +10 -1
  37. package/src/devices/__tests__/DeviceManagerFilters.test.ts +4 -1
  38. package/src/devices/__tests__/MicrophoneManager.test.ts +10 -1
  39. package/src/devices/__tests__/MicrophoneManagerRN.test.ts +4 -1
  40. package/src/devices/__tests__/ScreenShareManager.test.ts +4 -1
  41. package/src/devices/__tests__/SpeakerManager.test.ts +13 -4
  42. package/src/errors/SfuTimeoutError.ts +7 -0
  43. package/src/errors/index.ts +1 -0
  44. package/src/events/__tests__/call.test.ts +2 -0
  45. package/src/events/__tests__/mutes.test.ts +4 -1
  46. package/src/events/call.ts +8 -0
  47. package/src/gen/google/protobuf/struct.ts +12 -7
  48. package/src/gen/google/protobuf/timestamp.ts +7 -6
  49. package/src/gen/video/sfu/event/events.ts +25 -23
  50. package/src/gen/video/sfu/models/models.ts +1 -11
  51. package/src/gen/video/sfu/signal_rpc/signal.client.ts +29 -25
  52. package/src/gen/video/sfu/signal_rpc/signal.ts +0 -1
  53. package/src/helpers/__tests__/AudioBindingsWatchdog.test.ts +6 -3
  54. package/src/helpers/__tests__/DynascaleManager.test.ts +6 -3
  55. package/src/helpers/__tests__/ViewportTracker.test.ts +6 -3
  56. package/src/helpers/__tests__/firstVideoFrame.test.ts +91 -0
  57. package/src/helpers/client-details.ts +1 -1
  58. package/src/helpers/firstVideoFrame.ts +38 -0
  59. package/src/reporting/ClientEventReporter.ts +859 -0
  60. package/src/reporting/__tests__/ClientEventReporter.test.ts +620 -0
  61. package/src/reporting/index.ts +1 -0
  62. package/src/rtc/BasePeerConnection.ts +30 -0
  63. package/src/rtc/Publisher.ts +0 -4
  64. package/src/rtc/Subscriber.ts +2 -28
  65. package/src/rtc/__tests__/Call.reconnect.test.ts +3 -0
  66. package/src/rtc/types.ts +34 -0
  67. package/src/types.ts +6 -0
package/dist/index.es.js CHANGED
@@ -508,7 +508,6 @@ class ErrorFromResponse extends Error {
508
508
  }
509
509
  }
510
510
 
511
- /* eslint-disable */
512
511
  // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
513
512
  // @generated from protobuf file "google/protobuf/struct.proto" (package "google.protobuf", syntax proto3)
514
513
  // tslint:disable
@@ -774,7 +773,6 @@ class ListValue$Type extends MessageType {
774
773
  */
775
774
  const ListValue = new ListValue$Type();
776
775
 
777
- /* eslint-disable */
778
776
  // @generated by protobuf-ts 2.10.0 with parameter long_type_string,client_generic,server_none,eslint_disable,optimize_code_size
779
777
  // @generated from protobuf file "google/protobuf/timestamp.proto" (package "google.protobuf", syntax proto3)
780
778
  // tslint:disable
@@ -1841,12 +1839,6 @@ class TrackInfo$Type extends MessageType {
1841
1839
  kind: 'scalar',
1842
1840
  T: 5 /*ScalarType.INT32*/,
1843
1841
  },
1844
- {
1845
- no: 13,
1846
- name: 'self_sub_audio_video',
1847
- kind: 'scalar',
1848
- T: 8 /*ScalarType.BOOL*/,
1849
- },
1850
1842
  ]);
1851
1843
  }
1852
1844
  }
@@ -6649,7 +6641,7 @@ const getSdkVersion = (sdk) => {
6649
6641
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
6650
6642
  };
6651
6643
 
6652
- const version = "1.52.1-beta.0";
6644
+ const version = "1.53.0";
6653
6645
  const [major, minor, patch] = version.split('.');
6654
6646
  let sdkInfo = {
6655
6647
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -6793,7 +6785,7 @@ const getClientDetails = async () => {
6793
6785
  .join(' '),
6794
6786
  version: '',
6795
6787
  },
6796
- webrtcVersion: webRtcInfo?.version || '',
6788
+ webrtcVersion: browserVersion,
6797
6789
  };
6798
6790
  };
6799
6791
 
@@ -7743,7 +7735,7 @@ class BasePeerConnection {
7743
7735
  /**
7744
7736
  * Constructs a new `BasePeerConnection` instance.
7745
7737
  */
7746
- constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
7738
+ constructor(peerType, { sfuClient, connectionConfig, state, dispatcher, onReconnectionNeeded, onIceConnected, onPeerConnectionStateChange, onRemoteTrackUnmute, tag, enableTracing, clientPublishOptions, iceRestartDelay = 2500, statsTimestampDriftThresholdMs = 0, }) {
7747
7739
  this.iceHasEverConnected = false;
7748
7740
  this.isIceRestarting = false;
7749
7741
  this.isDisposed = false;
@@ -7897,6 +7889,10 @@ class BasePeerConnection {
7897
7889
  this.onConnectionStateChange = async () => {
7898
7890
  const state = this.pc.connectionState;
7899
7891
  this.logger.debug(`Connection state changed`, state);
7892
+ this.fireOnPeerConnectionStateChange({
7893
+ stateType: 'peerConnection',
7894
+ state,
7895
+ });
7900
7896
  if (this.tracer && (state === 'connected' || state === 'failed')) {
7901
7897
  try {
7902
7898
  const stats = await this.stats.get();
@@ -7919,8 +7915,20 @@ class BasePeerConnection {
7919
7915
  this.onIceConnectionStateChange = () => {
7920
7916
  const state = this.pc.iceConnectionState;
7921
7917
  this.logger.debug(`ICE connection state changed`, state);
7918
+ this.fireOnPeerConnectionStateChange({ stateType: 'ice', state });
7922
7919
  this.handleConnectionStateUpdate(state);
7923
7920
  };
7921
+ this.fireOnPeerConnectionStateChange = (event) => {
7922
+ try {
7923
+ this.onPeerConnectionStateChange?.({
7924
+ peerType: this.peerType,
7925
+ ...event,
7926
+ });
7927
+ }
7928
+ catch (err) {
7929
+ this.logger.warn('onPeerConnectionStateChange listener threw', err);
7930
+ }
7931
+ };
7924
7932
  this.handleConnectionStateUpdate = (state) => {
7925
7933
  const { callingState } = this.state;
7926
7934
  if (callingState === CallingState.OFFLINE)
@@ -8035,6 +8043,8 @@ class BasePeerConnection {
8035
8043
  this.tag = tag;
8036
8044
  this.onReconnectionNeeded = onReconnectionNeeded;
8037
8045
  this.onIceConnected = onIceConnected;
8046
+ this.onPeerConnectionStateChange = onPeerConnectionStateChange;
8047
+ this.onRemoteTrackUnmute = onRemoteTrackUnmute;
8038
8048
  this.logger = videoLoggerSystem.getLogger(peerType === PeerType.SUBSCRIBER ? 'Subscriber' : 'Publisher', { tags: [tag] });
8039
8049
  this.pc = this.createPeerConnection(connectionConfig);
8040
8050
  this.stats = new StatsTracer(this.pc, peerType, this.trackIdToTrackType, statsTimestampDriftThresholdMs);
@@ -8057,6 +8067,8 @@ class BasePeerConnection {
8057
8067
  this.preConnectStuckTimeout = undefined;
8058
8068
  this.onReconnectionNeeded = undefined;
8059
8069
  this.onIceConnected = undefined;
8070
+ this.onPeerConnectionStateChange = undefined;
8071
+ this.onRemoteTrackUnmute = undefined;
8060
8072
  this.isDisposed = true;
8061
8073
  this.detachEventHandlers();
8062
8074
  this.pc.close();
@@ -8417,7 +8429,7 @@ class Publisher extends BasePeerConnection {
8417
8429
  /**
8418
8430
  * Constructs a new `Publisher` instance.
8419
8431
  */
8420
- constructor(baseOptions, publishOptions, opts = {}) {
8432
+ constructor(baseOptions, publishOptions) {
8421
8433
  super(PeerType.PUBLISHER_UNSPECIFIED, baseOptions);
8422
8434
  this.transceiverCache = new TransceiverCache();
8423
8435
  this.clonedTracks = new Set();
@@ -8838,7 +8850,6 @@ class Publisher extends BasePeerConnection {
8838
8850
  muted: !isTrackLive,
8839
8851
  codec: publishOption.codec,
8840
8852
  publishOptionId: publishOption.id,
8841
- selfSubAudioVideo: this.selfSubEnabled,
8842
8853
  };
8843
8854
  };
8844
8855
  this.cloneTrack = (track) => {
@@ -8919,7 +8930,6 @@ class Publisher extends BasePeerConnection {
8919
8930
  });
8920
8931
  };
8921
8932
  this.publishOptions = publishOptions;
8922
- this.selfSubEnabled = opts.selfSubEnabled ?? false;
8923
8933
  this.on('iceRestart', (iceRestart) => {
8924
8934
  if (iceRestart.peerType !== PeerType.PUBLISHER_UNSPECIFIED)
8925
8935
  return;
@@ -9001,13 +9011,6 @@ class Subscriber extends BasePeerConnection {
9001
9011
  */
9002
9012
  constructor(opts) {
9003
9013
  super(PeerType.SUBSCRIBER, opts);
9004
- /**
9005
- * Remote streams received from the SFU. For a self-sub case
9006
- * we need to be able to distinguish between the local capture stream.
9007
- * The map will never contain local streams so we can safely use it to
9008
- * check if the stream is remote and dispose it when needed.
9009
- */
9010
- this.trackedStreams = new WeakSet();
9011
9014
  /**
9012
9015
  * Restarts the ICE connection and renegotiates with the SFU.
9013
9016
  */
@@ -9042,7 +9045,6 @@ class Subscriber extends BasePeerConnection {
9042
9045
  // example: `e3f6aaf8-b03d-4911-be36-83f47d37a76a:TRACK_TYPE_VIDEO`
9043
9046
  const [trackId, rawTrackType] = primaryStream.id.split(':');
9044
9047
  const participantToUpdate = this.state.participants.find((p) => p.trackLookupPrefix === trackId);
9045
- const isSelfSub = !!participantToUpdate?.isLocalParticipant;
9046
9048
  this.logger.debug(`[onTrack]: Got remote ${rawTrackType} track for userId: ${participantToUpdate?.userId}`, track.id, track);
9047
9049
  const trackType = toTrackType(rawTrackType);
9048
9050
  if (!trackType) {
@@ -9056,6 +9058,7 @@ class Subscriber extends BasePeerConnection {
9056
9058
  track.addEventListener('unmute', () => {
9057
9059
  this.logger.info(`[onTrack]: Track unmuted: ${trackDebugInfo}`);
9058
9060
  this.setRemoteTrackInterrupted(trackId, trackType, false);
9061
+ this.onRemoteTrackUnmute?.(trackType, track.id);
9059
9062
  });
9060
9063
  track.addEventListener('ended', () => {
9061
9064
  this.logger.info(`[onTrack]: Track ended: ${trackDebugInfo}`);
@@ -9066,9 +9069,6 @@ class Subscriber extends BasePeerConnection {
9066
9069
  this.setRemoteTrackInterrupted(trackId, trackType, true);
9067
9070
  }
9068
9071
  this.trackIdToTrackType.set(track.id, trackType);
9069
- if (isSelfSub) {
9070
- this.trackedStreams.add(primaryStream);
9071
- }
9072
9072
  if (!participantToUpdate) {
9073
9073
  this.logger.warn(`[onTrack]: Received track for unknown participant: ${trackId}`, e);
9074
9074
  this.state.registerOrphanedTrack({
@@ -9084,12 +9084,6 @@ class Subscriber extends BasePeerConnection {
9084
9084
  this.logger.error(`Unknown track type: ${rawTrackType}`);
9085
9085
  return;
9086
9086
  }
9087
- // Self-sub loopback audio routes to the speaker by default, which
9088
- // would echo the local user's voice. Default-mute here; consumers
9089
- // (the loopback recording hook) re-enable explicitly when needed.
9090
- if (isSelfSub && e.track.kind === 'audio') {
9091
- e.track.enabled = false;
9092
- }
9093
9087
  // get the previous stream to dispose it later
9094
9088
  // usually this happens during migration, when the stream is replaced
9095
9089
  // with a new one but the old one is still in the state
@@ -9098,12 +9092,8 @@ class Subscriber extends BasePeerConnection {
9098
9092
  this.state.updateParticipant(participantToUpdate.sessionId, {
9099
9093
  [streamKindProp]: primaryStream,
9100
9094
  });
9095
+ // now, dispose the previous stream if it exists
9101
9096
  if (previousStream) {
9102
- if (isSelfSub && !this.trackedStreams.has(previousStream)) {
9103
- // this is the local capture stream, we don't want to dispose it
9104
- this.logger.debug(`[onTrack]: Skipping cleanup of previous ${e.track.kind} stream for userId: ${participantToUpdate.userId} because it is not tracked`);
9105
- return;
9106
- }
9107
9097
  this.logger.info(`[onTrack]: Cleaning up previous remote ${track.kind} tracks for userId: ${participantToUpdate.userId}`);
9108
9098
  previousStream.getTracks().forEach((t) => {
9109
9099
  t.stop();
@@ -9308,6 +9298,15 @@ class SfuJoinError extends Error {
9308
9298
  }
9309
9299
  }
9310
9300
 
9301
+ /**
9302
+ * An error thrown when a client-side SFU deadline (e.g., waiting for the
9303
+ * signaling WS to open or for the `joinResponse` to arrive) fires before
9304
+ * the awaited operation resolves. Allows consumers (e.g., the client event
9305
+ * reporter) to classify timeouts without relying on message wording.
9306
+ */
9307
+ class SfuTimeoutError extends Error {
9308
+ }
9309
+
9311
9310
  /**
9312
9311
  * Creates a fresh `joinResponseTask` with a no-op rejection handler attached
9313
9312
  * to the underlying promise. The handler marks the rejection path as handled
@@ -9413,7 +9412,7 @@ class StreamSfuClient {
9413
9412
  timeoutId = setTimeout(() => {
9414
9413
  const message = `SFU WS connection failed to open after ${this.joinResponseTimeout}ms`;
9415
9414
  this.tracer?.trace('signal.timeout', message);
9416
- reject(new Error(message));
9415
+ reject(new SfuTimeoutError(message));
9417
9416
  }, this.joinResponseTimeout);
9418
9417
  }),
9419
9418
  ]));
@@ -9583,7 +9582,7 @@ class StreamSfuClient {
9583
9582
  cleanupJoinSubscriptions();
9584
9583
  const message = `Waiting for "joinResponse" has timed out after ${this.joinResponseTimeout}ms`;
9585
9584
  this.tracer?.trace('joinRequestTimeout', message);
9586
- current.reject(new Error(message));
9585
+ current.reject(new SfuTimeoutError(message));
9587
9586
  }, this.joinResponseTimeout);
9588
9587
  const joinRequest = SfuRequest.create({
9589
9588
  requestPayload: {
@@ -9800,6 +9799,10 @@ const watchCallEnded = (call) => {
9800
9799
  const { callingState } = call.state;
9801
9800
  if (callingState !== CallingState.IDLE &&
9802
9801
  callingState !== CallingState.LEFT) {
9802
+ call.clientEventReporter.abort(call.cid, {
9803
+ code: 'BACKEND_LEAVE',
9804
+ reason: 'call.ended event received',
9805
+ });
9803
9806
  call
9804
9807
  .leave({ message: 'call.ended event received', reject: false })
9805
9808
  .catch((err) => {
@@ -9829,6 +9832,10 @@ const watchSfuCallEnded = (call) => {
9829
9832
  call.state.setEndedAt(new Date());
9830
9833
  const reason = CallEndedReason[e.reason];
9831
9834
  globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
9835
+ call.clientEventReporter.abort(call.cid, {
9836
+ code: 'BACKEND_LEAVE',
9837
+ reason: `callEnded received: ${reason}`,
9838
+ });
9832
9839
  await call.leave({ message: `callEnded received: ${reason}` });
9833
9840
  }
9834
9841
  catch (err) {
@@ -10981,6 +10988,40 @@ class DynascaleManager {
10981
10988
  }
10982
10989
  }
10983
10990
 
10991
+ /**
10992
+ * Invokes `onFirstFrame` once when the video element renders a frame.
10993
+ *
10994
+ * Uses `requestVideoFrameCallback` when available, falling back to `loadeddata`
10995
+ * for browsers that don't support it.
10996
+ */
10997
+ const createFirstVideoFrameDetector = (videoElement, onFirstFrame) => {
10998
+ let done = false;
10999
+ const notify = () => {
11000
+ if (done)
11001
+ return;
11002
+ done = true;
11003
+ onFirstFrame();
11004
+ };
11005
+ if (typeof videoElement.requestVideoFrameCallback === 'function') {
11006
+ const handle = videoElement.requestVideoFrameCallback(notify);
11007
+ return () => {
11008
+ done = true;
11009
+ videoElement.cancelVideoFrameCallback(handle);
11010
+ };
11011
+ }
11012
+ if (videoElement.readyState >= videoElement.HAVE_CURRENT_DATA) {
11013
+ queueMicrotask(notify);
11014
+ return () => {
11015
+ done = true;
11016
+ };
11017
+ }
11018
+ videoElement.addEventListener('loadeddata', notify, { once: true });
11019
+ return () => {
11020
+ done = true;
11021
+ videoElement.removeEventListener('loadeddata', notify);
11022
+ };
11023
+ };
11024
+
10984
11025
  const DEFAULT_THRESHOLD = 0.35;
10985
11026
  const DEFAULT_VIEWPORT_VISIBILITY_STATE = {
10986
11027
  videoTrack: VisibilityState.UNKNOWN,
@@ -13824,7 +13865,7 @@ class Call {
13824
13865
  * Use the [`StreamVideoClient.call`](./StreamVideoClient.md/#call)
13825
13866
  * method to construct a `Call` instance.
13826
13867
  */
13827
- constructor({ type, id, streamClient, members, ownCapabilities, sortParticipantsBy, clientStore, ringing = false, watching = false, }) {
13868
+ constructor({ type, id, streamClient, clientEventReporter, members, ownCapabilities, sortParticipantsBy, clientStore, ringing = false, watching = false, }) {
13828
13869
  /**
13829
13870
  * The state of this call.
13830
13871
  */
@@ -13861,7 +13902,6 @@ class Call {
13861
13902
  // maintain the order of publishing tracks to restore them after a reconnection
13862
13903
  // it shouldn't contain duplicates
13863
13904
  this.trackPublishOrder = [];
13864
- this.selfSubEnabled = false;
13865
13905
  this.hasJoinedOnce = false;
13866
13906
  this.deviceSettingsAppliedOnce = false;
13867
13907
  this.initialized = false;
@@ -14152,9 +14192,14 @@ class Call {
14152
14192
  this.sfuStatsReporter = undefined;
14153
14193
  this.lastStatsOptions = undefined;
14154
14194
  await this.subscriber?.dispose();
14195
+ this.clientEventReporter.abort(this.cid, {
14196
+ code: 'CLIENT_ABORTED',
14197
+ reason: leaveReason,
14198
+ });
14155
14199
  this.subscriber = undefined;
14156
14200
  await this.publisher?.dispose();
14157
14201
  this.publisher = undefined;
14202
+ this.clientEventReporter.unregisterCall(this.cid);
14158
14203
  await this.sfuClient?.leaveAndClose(leaveReason);
14159
14204
  this.sfuClient = undefined;
14160
14205
  this.trackSubscriptionManager.setSfuClient(undefined);
@@ -14206,30 +14251,6 @@ class Call {
14206
14251
  await Promise.all(stopOnLeavePromises);
14207
14252
  });
14208
14253
  };
14209
- /**
14210
- * The largest video publish dimension across the current publish options.
14211
- *
14212
- * @internal
14213
- */
14214
- this.getMaxVideoPublishDimension = () => {
14215
- if (!this.currentPublishOptions)
14216
- return undefined;
14217
- let maxDimension;
14218
- let maxArea = 0;
14219
- for (const opt of this.currentPublishOptions) {
14220
- if (opt.trackType !== TrackType.VIDEO)
14221
- continue;
14222
- const dim = opt.videoDimension;
14223
- if (!dim || !dim.width || !dim.height)
14224
- continue;
14225
- const area = dim.width * dim.height;
14226
- if (area > maxArea) {
14227
- maxDimension = dim;
14228
- maxArea = area;
14229
- }
14230
- }
14231
- return maxDimension;
14232
- };
14233
14254
  /**
14234
14255
  * Update from the call response from the "call.ring" event
14235
14256
  * @internal
@@ -14376,7 +14397,7 @@ class Call {
14376
14397
  *
14377
14398
  * @returns a promise which resolves once the call join-flow has finished.
14378
14399
  */
14379
- this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, selfSubEnabled = false, ...data } = {}) => {
14400
+ this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, ...data } = {}) => {
14380
14401
  const callingState = this.state.callingState;
14381
14402
  if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
14382
14403
  throw new Error(`Illegal State: call.join() shall be called only once`);
@@ -14384,15 +14405,19 @@ class Call {
14384
14405
  if (data?.ring) {
14385
14406
  this.ringingSubject.next(true);
14386
14407
  }
14387
- // we need this to be set before the callingx.joinCall() is
14388
- // called to avoid registering the test call in the CallKit/Telecom
14389
- this.selfSubEnabled = selfSubEnabled;
14390
14408
  const callingX = globalThis.streamRNVideoSDK?.callingX;
14391
14409
  if (callingX) {
14392
14410
  // for Android/iOS, we need to start the call in the callingx library as soon as possible
14393
14411
  await callingX.joinCall(this, this.clientStore.calls);
14394
14412
  }
14395
14413
  await this.setup();
14414
+ this.clientEventReporter.registerCall(this.cid, {
14415
+ callType: this.type,
14416
+ callId: this.id,
14417
+ getCallSessionId: () => this.state.session?.id ?? '',
14418
+ getSfuId: () => this.credentials?.server.edge_name ?? '',
14419
+ getUserSessionId: () => this.sfuClient?.sessionId ?? '',
14420
+ });
14396
14421
  this.joinResponseTimeout = joinResponseTimeout;
14397
14422
  this.rpcRequestTimeout = rpcRequestTimeout;
14398
14423
  // we will count the number of join failures per SFU.
@@ -14402,39 +14427,42 @@ class Call {
14402
14427
  const joinData = data;
14403
14428
  maxJoinRetries = Math.max(maxJoinRetries, 1);
14404
14429
  try {
14405
- for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
14406
- try {
14407
- this.logger.trace(`Joining call (${attempt})`, this.cid);
14408
- await this.doJoin(data);
14409
- delete joinData.migrating_from;
14410
- delete joinData.migrating_from_list;
14411
- break;
14412
- }
14413
- catch (err) {
14414
- this.logger.warn(`Failed to join call (${attempt})`, this.cid);
14415
- if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
14416
- (err instanceof SfuJoinError && err.unrecoverable)) {
14417
- // if the error is unrecoverable, we should not retry as that signals
14418
- // that connectivity is good, but the coordinator doesn't allow the user
14419
- // to join the call due to some reason (e.g., ended call, expired token...)
14420
- throw err;
14421
- }
14422
- // immediately switch to a different SFU in case of recoverable join error
14423
- const switchSfu = err instanceof SfuJoinError &&
14424
- SfuJoinError.isJoinErrorCode(err.errorEvent);
14425
- const sfuId = this.credentials?.server.edge_name || '';
14426
- const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
14427
- sfuJoinFailures.set(sfuId, failures);
14428
- if (switchSfu || failures >= 2) {
14429
- joinData.migrating_from = sfuId;
14430
- joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
14430
+ await this.clientEventReporter.withJoinLifecycle(this.cid, 'first-attempt', async () => {
14431
+ for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
14432
+ try {
14433
+ this.logger.trace(`Joining call (${attempt})`, this.cid);
14434
+ await this.doJoin(data);
14435
+ delete joinData.migrating_from;
14436
+ delete joinData.migrating_from_list;
14437
+ return;
14431
14438
  }
14432
- if (attempt === maxJoinRetries - 1) {
14433
- throw err;
14439
+ catch (err) {
14440
+ this.logger.warn(`Failed to join call (${attempt})`, this.cid);
14441
+ if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
14442
+ (err instanceof SfuJoinError && err.unrecoverable)) {
14443
+ throw err;
14444
+ }
14445
+ const switchSfu = err instanceof SfuJoinError &&
14446
+ SfuJoinError.isJoinErrorCode(err.errorEvent);
14447
+ const sfuId = this.credentials?.server.edge_name;
14448
+ if (sfuId) {
14449
+ const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
14450
+ sfuJoinFailures.set(sfuId, failures);
14451
+ if (switchSfu || failures >= 2) {
14452
+ joinData.migrating_from = sfuId;
14453
+ joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
14454
+ if (attempt < maxJoinRetries - 1) {
14455
+ this.clientEventReporter.startCorrelation(this.cid, 'first-attempt');
14456
+ }
14457
+ }
14458
+ }
14459
+ if (attempt === maxJoinRetries - 1) {
14460
+ throw err;
14461
+ }
14434
14462
  }
14463
+ await sleep(retryInterval(attempt));
14435
14464
  }
14436
- await sleep(retryInterval(attempt));
14437
- }
14465
+ });
14438
14466
  }
14439
14467
  catch (error) {
14440
14468
  callingX?.endCall(this, 'error');
@@ -14463,7 +14491,7 @@ class Call {
14463
14491
  performingMigration ||
14464
14492
  data?.migrating_from) {
14465
14493
  try {
14466
- const joinResponse = await this.doJoinRequest(data);
14494
+ const joinResponse = await this.clientEventReporter.track(this.cid, 'CoordinatorJoin', () => this.doJoinRequest(data));
14467
14495
  this.credentials = joinResponse.credentials;
14468
14496
  statsOptions = joinResponse.stats_options;
14469
14497
  this.lastStatsOptions = statsOptions;
@@ -14521,9 +14549,11 @@ class Call {
14521
14549
  const preferredSubscribeOptions = !isReconnecting
14522
14550
  ? this.getPreferredSubscribeOptions()
14523
14551
  : [];
14552
+ const unifiedSessionId = this.unifiedSessionId;
14553
+ const capabilities = Array.from(this.clientCapabilities);
14524
14554
  try {
14525
- const { callState, fastReconnectDeadlineSeconds, publishOptions } = await sfuClient.join({
14526
- unifiedSessionId: this.unifiedSessionId,
14555
+ const { callState, fastReconnectDeadlineSeconds, publishOptions } = await this.clientEventReporter.track(this.cid, 'WSJoin', () => sfuClient.join({
14556
+ unifiedSessionId,
14527
14557
  subscriberSdp,
14528
14558
  publisherSdp,
14529
14559
  clientDetails,
@@ -14531,9 +14561,9 @@ class Call {
14531
14561
  reconnectDetails,
14532
14562
  preferredPublishOptions,
14533
14563
  preferredSubscribeOptions,
14534
- capabilities: Array.from(this.clientCapabilities),
14564
+ capabilities,
14535
14565
  source: ParticipantSource.WEBRTC_UNSPECIFIED,
14536
- });
14566
+ }));
14537
14567
  this.currentPublishOptions = publishOptions;
14538
14568
  this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
14539
14569
  if (callState) {
@@ -14745,6 +14775,16 @@ class Call {
14745
14775
  // "ICE never connected" failure budget can be cleared.
14746
14776
  this.iceFailuresWithoutConnect = 0;
14747
14777
  },
14778
+ onPeerConnectionStateChange: (event) => {
14779
+ this.clientEventReporter.onPeerConnectionStateChange(this.cid, event);
14780
+ },
14781
+ onRemoteTrackUnmute: (trackType, trackId) => {
14782
+ const reportable = trackType === TrackType.AUDIO ||
14783
+ (isReactNative() && trackType === TrackType.VIDEO);
14784
+ if (!reportable)
14785
+ return;
14786
+ this.clientEventReporter.reportFirstFrame(this.cid, trackType, trackId);
14787
+ },
14748
14788
  };
14749
14789
  this.subscriber = new Subscriber(basePeerConnectionOptions);
14750
14790
  // anonymous users can't publish anything hence, there is no need
@@ -14754,9 +14794,7 @@ class Call {
14754
14794
  if (closePreviousInstances && this.publisher) {
14755
14795
  await this.publisher.dispose();
14756
14796
  }
14757
- this.publisher = new Publisher(basePeerConnectionOptions, publishOptions, {
14758
- selfSubEnabled: this.selfSubEnabled,
14759
- });
14797
+ this.publisher = new Publisher(basePeerConnectionOptions, publishOptions);
14760
14798
  }
14761
14799
  this.statsReporter?.stop();
14762
14800
  if (this.statsReportingIntervalInMs > 0) {
@@ -15023,7 +15061,10 @@ class Call {
15023
15061
  const reconnectStartTime = Date.now();
15024
15062
  this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
15025
15063
  this.state.setCallingState(CallingState.RECONNECTING);
15026
- await this.doJoin(this.joinCallData);
15064
+ const joinReason = this.reconnectReason === ReconnectReason.NETWORK_BACK_ONLINE
15065
+ ? 'network-available'
15066
+ : 'full-rejoin';
15067
+ await this.clientEventReporter.withJoinLifecycle(this.cid, joinReason, () => this.doJoin(this.joinCallData));
15027
15068
  await this.restorePublishedTracks();
15028
15069
  this.restoreSubscribedTracks();
15029
15070
  this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
@@ -15047,11 +15088,11 @@ class Call {
15047
15088
  const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
15048
15089
  try {
15049
15090
  const currentSfu = currentSfuClient.edgeName;
15050
- await this.doJoin({
15091
+ await this.clientEventReporter.withJoinLifecycle(this.cid, 'migration', () => this.doJoin({
15051
15092
  ...this.joinCallData,
15052
15093
  migrating_from: currentSfu,
15053
15094
  migrating_from_list: [currentSfu],
15054
- });
15095
+ }));
15055
15096
  }
15056
15097
  finally {
15057
15098
  // cleanup the migration_from field after the migration is complete or failed
@@ -15087,11 +15128,22 @@ class Call {
15087
15128
  this.registerReconnectHandlers = () => {
15088
15129
  // handles the legacy "goAway" event
15089
15130
  const unregisterGoAway = this.on('goAway', () => {
15131
+ this.clientEventReporter.captureWsError(this.cid, {
15132
+ code: 'SFU_GO_AWAY',
15133
+ reason: 'SFU goAway received during WS join',
15134
+ });
15090
15135
  this.reconnect(WebsocketReconnectStrategy.MIGRATE, ReconnectReason.GO_AWAY).catch((err) => this.logger.warn('[Reconnect] Error reconnecting', err));
15091
15136
  });
15092
15137
  // handles the "error" event, through which the SFU can request a reconnect
15093
15138
  const unregisterOnError = this.on('error', (e) => {
15094
15139
  const { reconnectStrategy: strategy, error } = e;
15140
+ if (!SfuJoinError.isJoinErrorCode(e)) {
15141
+ const code = error?.code ? ErrorCode[error.code] : undefined;
15142
+ this.clientEventReporter.captureWsError(this.cid, {
15143
+ code: code ?? 'SFU_ERROR',
15144
+ reason: error?.message || 'SFU error during WS join',
15145
+ });
15146
+ }
15095
15147
  // SFU_FULL is a join error, and when emitted, although it specifies a
15096
15148
  // `migrate` strategy, we should actually perform a REJOIN to a new SFU.
15097
15149
  // This is now handled separately in the `call.join()` method.
@@ -15786,7 +15838,9 @@ class Call {
15786
15838
  this.leave({
15787
15839
  reject: true,
15788
15840
  reason: 'timeout',
15789
- message: `ringing timeout - ${this.isCreatedByMe ? 'no one accepted' : `user didn't interact with incoming call screen`}`,
15841
+ message: `ringing timeout - ${this.isCreatedByMe
15842
+ ? 'no one accepted'
15843
+ : `user didn't interact with incoming call screen`}`,
15790
15844
  }).catch((err) => {
15791
15845
  this.logger.error('Failed to drop call', err);
15792
15846
  });
@@ -15992,15 +16046,36 @@ class Call {
15992
16046
  * @param trackType the kind of video.
15993
16047
  */
15994
16048
  this.bindVideoElement = (videoElement, sessionId, trackType) => {
15995
- const unbind = this.dynascaleManager?.bindVideoElement(videoElement, sessionId, trackType);
15996
- if (!unbind)
16049
+ const unbindDynascale = this.dynascaleManager?.bindVideoElement(videoElement, sessionId, trackType);
16050
+ const stopFirstFrameDetector = this.bindFirstVideoFrameDetector(videoElement, sessionId, trackType);
16051
+ if (!unbindDynascale && !stopFirstFrameDetector)
15997
16052
  return;
16053
+ const unbind = () => {
16054
+ stopFirstFrameDetector?.();
16055
+ unbindDynascale?.();
16056
+ };
15998
16057
  this.leaveCallHooks.add(unbind);
15999
16058
  return () => {
16000
16059
  this.leaveCallHooks.delete(unbind);
16001
16060
  unbind();
16002
16061
  };
16003
16062
  };
16063
+ this.bindFirstVideoFrameDetector = (videoElement, sessionId, trackType) => {
16064
+ if (trackType !== 'videoTrack')
16065
+ return;
16066
+ return createFirstVideoFrameDetector(videoElement, () => {
16067
+ this.reportFirstRenderedVideoFrame(sessionId);
16068
+ });
16069
+ };
16070
+ this.reportFirstRenderedVideoFrame = (sessionId) => {
16071
+ const participant = this.state.findParticipantBySessionId(sessionId);
16072
+ if (participant?.isLocalParticipant)
16073
+ return;
16074
+ const trackId = participant?.videoStream?.getVideoTracks()[0]?.id;
16075
+ if (!trackId)
16076
+ return;
16077
+ this.clientEventReporter.reportFirstFrame(this.cid, TrackType.VIDEO, trackId);
16078
+ };
16004
16079
  /**
16005
16080
  * Binds a DOM <audio> element to the given session id.
16006
16081
  *
@@ -16150,6 +16225,7 @@ class Call {
16150
16225
  this.ringingSubject = new BehaviorSubject(ringing);
16151
16226
  this.watching = watching;
16152
16227
  this.streamClient = streamClient;
16228
+ this.clientEventReporter = clientEventReporter;
16153
16229
  this.clientStore = clientStore;
16154
16230
  this.streamClientBasePath = `/call/${this.type}/${this.id}`;
16155
16231
  this.logger = videoLoggerSystem.getLogger('Call');
@@ -16186,12 +16262,6 @@ class Call {
16186
16262
  get currentUserId() {
16187
16263
  return this.clientStore.connectedUser?.id;
16188
16264
  }
16189
- /**
16190
- * A flag indicating whether self-subscription is enabled for the call.
16191
- */
16192
- get isSelfSubEnabled() {
16193
- return this.selfSubEnabled;
16194
- }
16195
16265
  /**
16196
16266
  * A flag indicating whether the call was created by the current user.
16197
16267
  */
@@ -17378,10 +17448,12 @@ class StreamClient {
17378
17448
  this.logger.info('StreamClient.connect: this.wsConnection.connect()');
17379
17449
  return await this.wsConnection.connect(this.defaultWSTimeout);
17380
17450
  };
17451
+ this.getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
17452
+ "1.53.0";
17381
17453
  this.getUserAgent = () => {
17382
17454
  if (!this.cachedUserAgent) {
17383
17455
  const { clientAppIdentifier = {} } = this.options;
17384
- const { sdkName = 'js', sdkVersion = "1.52.1-beta.0", ...extras } = clientAppIdentifier;
17456
+ const { sdkName = 'js', sdkVersion = "1.53.0", ...extras } = clientAppIdentifier;
17385
17457
  this.cachedUserAgent = [
17386
17458
  `stream-video-${sdkName}-v${sdkVersion}`,
17387
17459
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -17558,6 +17630,602 @@ const createTokenOrProvider = (options) => {
17558
17630
  return token || tokenProvider;
17559
17631
  };
17560
17632
 
17633
+ const pcKey = (cid, role) => `${cid}:${role}`;
17634
+ class ClientEventReporter {
17635
+ constructor(options) {
17636
+ this.logger = videoLoggerSystem.getLogger('ClientEventReporter');
17637
+ this.callContexts = new Map();
17638
+ this.joinAttemptIds = new Map();
17639
+ this.joinReasons = new Map();
17640
+ this.coordinatorPairs = new Map();
17641
+ this.wsPairs = new Map();
17642
+ this.peerConnectionPairs = new Map();
17643
+ this.pcEverConnected = new Map();
17644
+ this.firstFrameReported = new Set();
17645
+ /**
17646
+ * Starts a new coordinator connection correlation scope.
17647
+ *
17648
+ * @param userId the id of the user being connected. Captured here because
17649
+ * the `CoordinatorWS` stage emits before the connection flow assigns
17650
+ * the user to the client, so it can't be read from the client yet.
17651
+ */
17652
+ this.startCoordinatorConnection = (userId) => {
17653
+ this.coordinatorConnectId = generateUUIDv4();
17654
+ this.coordinatorConnectUserId = userId;
17655
+ return this.coordinatorConnectId;
17656
+ };
17657
+ this.trackCoordinatorWs = async (op) => {
17658
+ this.beginCoordinatorWs();
17659
+ try {
17660
+ const result = await op();
17661
+ this.succeedCoordinatorWs();
17662
+ return result;
17663
+ }
17664
+ catch (err) {
17665
+ applyError(this.coordinatorWsPair, mapCoordinatorWsError(err));
17666
+ throw err;
17667
+ }
17668
+ };
17669
+ this.beginCoordinatorWs = () => {
17670
+ if (!this.coordinatorWsPair) {
17671
+ this.coordinatorWsPair = {
17672
+ sid: generateUUIDv4(),
17673
+ attempts: 0,
17674
+ startedAt: Date.now(),
17675
+ userIdSnapshot: this.coordinatorConnectUserId,
17676
+ };
17677
+ this.send({
17678
+ ...this.buildCoordinatorWsCommon(this.coordinatorWsPair),
17679
+ event_type: 'initiated',
17680
+ });
17681
+ }
17682
+ this.coordinatorWsPair.attempts++;
17683
+ };
17684
+ this.succeedCoordinatorWs = () => {
17685
+ const pair = this.coordinatorWsPair;
17686
+ if (!pair)
17687
+ return;
17688
+ this.send({
17689
+ ...this.buildCoordinatorWsCommon(pair),
17690
+ event_type: 'completed',
17691
+ outcome: 'success',
17692
+ retry_count_attempt: pair.attempts - 1,
17693
+ elapsed_time: Date.now() - pair.startedAt,
17694
+ });
17695
+ this.coordinatorWsPair = undefined;
17696
+ };
17697
+ this.closeCoordinatorWs = () => {
17698
+ const pair = this.coordinatorWsPair;
17699
+ if (!pair || !pair.lastError) {
17700
+ this.coordinatorWsPair = undefined;
17701
+ return;
17702
+ }
17703
+ const { reason, code } = pair.lastError;
17704
+ this.send({
17705
+ ...this.buildCoordinatorWsCommon(pair),
17706
+ event_type: 'completed',
17707
+ outcome: 'failure',
17708
+ retry_count_attempt: pair.attempts - 1,
17709
+ elapsed_time: Date.now() - pair.startedAt,
17710
+ retry_failure_reason: reason,
17711
+ retry_failure_code: code,
17712
+ });
17713
+ this.coordinatorWsPair = undefined;
17714
+ };
17715
+ this.buildCoordinatorWsCommon = (pair) => ({
17716
+ user_id: pair.userIdSnapshot ?? this.streamClient.userID,
17717
+ stage: 'CoordinatorWS',
17718
+ stage_id: pair.sid,
17719
+ ...(this.coordinatorConnectId && {
17720
+ coordinator_connect_id: this.coordinatorConnectId,
17721
+ }),
17722
+ timestamp: new Date().toISOString(),
17723
+ user_agent: this.streamClient.getUserAgent(),
17724
+ sdk_version: this.streamClient.getSdkVersion(),
17725
+ });
17726
+ this.emitMediaPermission = (cid) => {
17727
+ if (isReactNative() || !this.callContexts.has(cid))
17728
+ return;
17729
+ const pair = {
17730
+ sid: generateUUIDv4(),
17731
+ attempts: 0,
17732
+ startedAt: Date.now(),
17733
+ joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
17734
+ };
17735
+ this.send({
17736
+ ...this.buildCommon(cid, 'MediaDevicePermission', pair),
17737
+ ...this.sessionIdField(cid),
17738
+ microphone_permission_status: readPermissionStatus(getAudioBrowserPermission()),
17739
+ camera_permission_status: readPermissionStatus(getVideoBrowserPermission()),
17740
+ event_type: 'initiated',
17741
+ });
17742
+ };
17743
+ this.registerCall = (cid, ctx) => {
17744
+ this.callContexts.set(cid, ctx);
17745
+ };
17746
+ this.unregisterCall = (cid) => {
17747
+ this.callContexts.delete(cid);
17748
+ this.joinAttemptIds.delete(cid);
17749
+ this.joinReasons.delete(cid);
17750
+ this.coordinatorPairs.delete(cid);
17751
+ this.wsPairs.delete(cid);
17752
+ this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
17753
+ this.firstFrameReported.delete(`${cid}:FirstAudioFrame`);
17754
+ for (const role of ['publish', 'subscribe']) {
17755
+ const key = pcKey(cid, role);
17756
+ this.peerConnectionPairs.delete(key);
17757
+ this.pcEverConnected.delete(key);
17758
+ }
17759
+ };
17760
+ this.startCorrelation = (cid, joinReason) => {
17761
+ try {
17762
+ this.closeCallPairs(cid);
17763
+ this.joinAttemptIds.set(cid, generateUUIDv4());
17764
+ this.joinReasons.set(cid, joinReason);
17765
+ this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
17766
+ this.firstFrameReported.delete(`${cid}:FirstAudioFrame`);
17767
+ this.emitJoinInitiated(cid);
17768
+ this.emitMediaPermission(cid);
17769
+ }
17770
+ catch (err) {
17771
+ this.logger.warn('Failed to start join correlation', err);
17772
+ }
17773
+ };
17774
+ this.withJoinLifecycle = async (cid, joinReason, op) => {
17775
+ this.startCorrelation(cid, joinReason);
17776
+ try {
17777
+ return await op();
17778
+ }
17779
+ catch (err) {
17780
+ this.closeCallPairs(cid);
17781
+ throw err;
17782
+ }
17783
+ };
17784
+ this.track = async (cid, stage, op) => {
17785
+ this.beginAttempt(cid, stage);
17786
+ try {
17787
+ const result = await op();
17788
+ this.succeedAttempt(cid, stage);
17789
+ return result;
17790
+ }
17791
+ catch (err) {
17792
+ this.applyStageError(cid, stage, err);
17793
+ throw err;
17794
+ }
17795
+ };
17796
+ this.reportFirstFrame = (cid, trackType, trackId) => {
17797
+ const stage = trackType === TrackType.VIDEO
17798
+ ? 'FirstVideoFrame'
17799
+ : trackType === TrackType.AUDIO
17800
+ ? 'FirstAudioFrame'
17801
+ : undefined;
17802
+ if (!stage)
17803
+ return;
17804
+ const key = `${cid}:${stage}`;
17805
+ if (this.firstFrameReported.has(key))
17806
+ return;
17807
+ this.firstFrameReported.add(key);
17808
+ const pair = {
17809
+ sid: generateUUIDv4(),
17810
+ attempts: 0,
17811
+ startedAt: Date.now(),
17812
+ joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
17813
+ };
17814
+ const resolvedSfuId = this.getSfuId(cid);
17815
+ this.send({
17816
+ ...this.buildCommon(cid, stage, pair),
17817
+ ...this.sessionIdField(cid),
17818
+ ...(resolvedSfuId && { sfu_id: resolvedSfuId }),
17819
+ track_id: trackId,
17820
+ event_type: 'initiated',
17821
+ });
17822
+ };
17823
+ this.captureWsError = (cid, opts) => {
17824
+ const pair = this.wsPairs.get(cid);
17825
+ if (!pair)
17826
+ return;
17827
+ applyError(pair, { reason: opts.reason, code: opts.code });
17828
+ };
17829
+ this.close = (cid) => {
17830
+ this.closeCallPairs(cid);
17831
+ };
17832
+ this.abort = (cid, opts) => {
17833
+ try {
17834
+ const { code, reason } = opts;
17835
+ const stageError = { code, reason };
17836
+ applyErrorIfAbsent(this.coordinatorPairs.get(cid), stageError);
17837
+ applyErrorIfAbsent(this.wsPairs.get(cid), stageError);
17838
+ this.failCoordinator(cid);
17839
+ this.failWs(cid);
17840
+ this.emitPeerConnectionFailure(cid, 'publish', code, reason, 'NOT_CONNECTED');
17841
+ this.emitPeerConnectionFailure(cid, 'subscribe', code, reason, 'NOT_CONNECTED');
17842
+ }
17843
+ catch (err) {
17844
+ this.logger.warn('Failed to report abort', err);
17845
+ }
17846
+ };
17847
+ this.closeCallPairs = (cid) => {
17848
+ if (this.coordinatorPairs.get(cid))
17849
+ this.failCoordinator(cid);
17850
+ if (this.wsPairs.get(cid))
17851
+ this.failWs(cid);
17852
+ for (const role of ['publish', 'subscribe']) {
17853
+ this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt', 'NOT_CONNECTED');
17854
+ }
17855
+ };
17856
+ this.emitJoinInitiated = (cid) => {
17857
+ const joinAttemptId = this.joinAttemptIds.get(cid);
17858
+ if (!joinAttemptId)
17859
+ return;
17860
+ const coordinatorConnectId = this.coordinatorConnectId;
17861
+ this.send({
17862
+ user_id: this.streamClient.userID,
17863
+ stage: 'JoinInitiated',
17864
+ join_attempt_id: joinAttemptId,
17865
+ ...(coordinatorConnectId && {
17866
+ coordinator_connect_id: coordinatorConnectId,
17867
+ }),
17868
+ timestamp: new Date().toISOString(),
17869
+ user_agent: this.streamClient.getUserAgent(),
17870
+ sdk_version: this.streamClient.getSdkVersion(),
17871
+ event_type: 'initiated',
17872
+ });
17873
+ };
17874
+ this.beginAttempt = (cid, stage) => {
17875
+ if (stage === 'CoordinatorJoin')
17876
+ this.beginCoordinatorAttempt(cid);
17877
+ else
17878
+ this.beginWsAttempt(cid);
17879
+ };
17880
+ this.succeedAttempt = (cid, stage) => {
17881
+ if (stage === 'CoordinatorJoin')
17882
+ this.succeedCoordinator(cid);
17883
+ else
17884
+ this.succeedWs(cid);
17885
+ };
17886
+ this.applyStageError = (cid, stage, err) => {
17887
+ const pair = stage === 'CoordinatorJoin'
17888
+ ? this.coordinatorPairs.get(cid)
17889
+ : this.wsPairs.get(cid);
17890
+ applyErrorIfAbsent(pair, stage === 'CoordinatorJoin'
17891
+ ? mapCoordinatorHttpError(err)
17892
+ : mapWsJoinError(err));
17893
+ };
17894
+ this.beginCoordinatorAttempt = (cid) => {
17895
+ let pair = this.coordinatorPairs.get(cid);
17896
+ if (!pair) {
17897
+ pair = {
17898
+ sid: generateUUIDv4(),
17899
+ attempts: 0,
17900
+ startedAt: Date.now(),
17901
+ joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
17902
+ joinReasonSnapshot: this.joinReasons.get(cid),
17903
+ };
17904
+ this.coordinatorPairs.set(cid, pair);
17905
+ this.send({
17906
+ ...this.buildCommon(cid, 'CoordinatorJoin', pair),
17907
+ ...(pair.joinReasonSnapshot && {
17908
+ join_reason: pair.joinReasonSnapshot,
17909
+ }),
17910
+ event_type: 'initiated',
17911
+ });
17912
+ }
17913
+ pair.lastError = undefined;
17914
+ pair.attempts++;
17915
+ };
17916
+ this.succeedCoordinator = (cid) => {
17917
+ const pair = this.coordinatorPairs.get(cid);
17918
+ if (!pair)
17919
+ return;
17920
+ this.send({
17921
+ ...this.buildCommon(cid, 'CoordinatorJoin', pair),
17922
+ ...this.sessionIdField(cid),
17923
+ ...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
17924
+ event_type: 'completed',
17925
+ outcome: 'success',
17926
+ retry_count_attempt: pair.attempts - 1,
17927
+ elapsed_time: Date.now() - pair.startedAt,
17928
+ });
17929
+ this.coordinatorPairs.delete(cid);
17930
+ };
17931
+ this.failCoordinator = (cid) => {
17932
+ const pair = this.coordinatorPairs.get(cid);
17933
+ if (!pair || !pair.lastError) {
17934
+ this.coordinatorPairs.delete(cid);
17935
+ return;
17936
+ }
17937
+ const { reason, code } = pair.lastError;
17938
+ this.send({
17939
+ ...this.buildCommon(cid, 'CoordinatorJoin', pair),
17940
+ ...this.sessionIdField(cid),
17941
+ ...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
17942
+ event_type: 'completed',
17943
+ outcome: 'failure',
17944
+ retry_count_attempt: pair.attempts - 1,
17945
+ elapsed_time: Date.now() - pair.startedAt,
17946
+ retry_failure_reason: reason,
17947
+ retry_failure_code: code,
17948
+ });
17949
+ this.coordinatorPairs.delete(cid);
17950
+ };
17951
+ this.beginWsAttempt = (cid) => {
17952
+ let pair = this.wsPairs.get(cid);
17953
+ if (!pair) {
17954
+ pair = {
17955
+ sid: generateUUIDv4(),
17956
+ attempts: 0,
17957
+ startedAt: Date.now(),
17958
+ joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
17959
+ };
17960
+ this.wsPairs.set(cid, pair);
17961
+ const sfuId = this.getSfuId(cid);
17962
+ this.send({
17963
+ ...this.buildCommon(cid, 'WSJoin', pair),
17964
+ ...this.sessionIdField(cid),
17965
+ ...(sfuId && { sfu_id: sfuId }),
17966
+ event_type: 'initiated',
17967
+ });
17968
+ }
17969
+ pair.lastError = undefined;
17970
+ pair.attempts++;
17971
+ };
17972
+ this.succeedWs = (cid) => {
17973
+ const pair = this.wsPairs.get(cid);
17974
+ if (!pair)
17975
+ return;
17976
+ const sfuId = this.getSfuId(cid);
17977
+ this.send({
17978
+ ...this.buildCommon(cid, 'WSJoin', pair),
17979
+ ...this.sessionIdField(cid),
17980
+ ...(sfuId && { sfu_id: sfuId }),
17981
+ event_type: 'completed',
17982
+ outcome: 'success',
17983
+ retry_count_attempt: pair.attempts - 1,
17984
+ elapsed_time: Date.now() - pair.startedAt,
17985
+ });
17986
+ this.wsPairs.delete(cid);
17987
+ };
17988
+ this.failWs = (cid) => {
17989
+ const pair = this.wsPairs.get(cid);
17990
+ if (!pair || !pair.lastError) {
17991
+ this.wsPairs.delete(cid);
17992
+ return;
17993
+ }
17994
+ const { reason, code } = pair.lastError;
17995
+ const sfuId = this.getSfuId(cid);
17996
+ this.send({
17997
+ ...this.buildCommon(cid, 'WSJoin', pair),
17998
+ ...this.sessionIdField(cid),
17999
+ event_type: 'completed',
18000
+ outcome: 'failure',
18001
+ retry_count_attempt: pair.attempts - 1,
18002
+ elapsed_time: Date.now() - pair.startedAt,
18003
+ ...(sfuId && { sfu_id: sfuId }),
18004
+ retry_failure_reason: reason,
18005
+ retry_failure_code: code,
18006
+ });
18007
+ this.wsPairs.delete(cid);
18008
+ };
18009
+ this.onPeerConnectionStateChange = (cid, event) => {
18010
+ const role = event.peerType === PeerType.SUBSCRIBER ? 'subscribe' : 'publish';
18011
+ if (event.stateType === 'ice' && event.state === 'failed') {
18012
+ this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed', 'FAILED');
18013
+ return;
18014
+ }
18015
+ if (event.stateType === 'peerConnection' && event.state === 'failed') {
18016
+ this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed', 'CONNECTED');
18017
+ return;
18018
+ }
18019
+ if (event.stateType !== 'peerConnection')
18020
+ return;
18021
+ switch (event.state) {
18022
+ case 'connecting':
18023
+ if (this.peerConnectionPairs.has(pcKey(cid, role)))
18024
+ return;
18025
+ this.openPeerConnectionPair(cid, role);
18026
+ break;
18027
+ case 'connected':
18028
+ this.emitPeerConnectionSuccess(cid, role);
18029
+ this.pcEverConnected.set(pcKey(cid, role), true);
18030
+ break;
18031
+ }
18032
+ };
18033
+ this.openPeerConnectionPair = (cid, role) => {
18034
+ const key = pcKey(cid, role);
18035
+ const pair = {
18036
+ sid: generateUUIDv4(),
18037
+ attempts: 0,
18038
+ startedAt: Date.now(),
18039
+ joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
18040
+ sfuId: this.getSfuId(cid),
18041
+ userSessionId: this.getUserSessionId(cid),
18042
+ wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
18043
+ };
18044
+ this.peerConnectionPairs.set(key, pair);
18045
+ this.send({
18046
+ ...this.buildCommon(cid, 'PeerConnectionConnect', pair),
18047
+ ...this.sessionIdField(cid),
18048
+ peer_connection: role,
18049
+ was_previously_connected: pair.wasPreviouslyConnected,
18050
+ ...(pair.sfuId && { sfu_id: pair.sfuId }),
18051
+ ...(pair.userSessionId && {
18052
+ user_session_id: pair.userSessionId,
18053
+ }),
18054
+ event_type: 'initiated',
18055
+ });
18056
+ };
18057
+ this.emitPeerConnectionSuccess = (cid, role) => {
18058
+ const key = pcKey(cid, role);
18059
+ const pair = this.peerConnectionPairs.get(key);
18060
+ if (!pair)
18061
+ return;
18062
+ this.send({
18063
+ ...this.buildCommon(cid, 'PeerConnectionConnect', pair),
18064
+ ...this.sessionIdField(cid),
18065
+ peer_connection: role,
18066
+ was_previously_connected: pair.wasPreviouslyConnected,
18067
+ ...(pair.sfuId && { sfu_id: pair.sfuId }),
18068
+ ...(pair.userSessionId && {
18069
+ user_session_id: pair.userSessionId,
18070
+ }),
18071
+ event_type: 'completed',
18072
+ outcome: 'success',
18073
+ retry_count_attempt: 0,
18074
+ elapsed_time: Date.now() - pair.startedAt,
18075
+ });
18076
+ this.peerConnectionPairs.delete(key);
18077
+ };
18078
+ this.emitPeerConnectionFailure = (cid, role, code, reason, iceState) => {
18079
+ const key = pcKey(cid, role);
18080
+ const pair = this.peerConnectionPairs.get(key);
18081
+ if (!pair)
18082
+ return;
18083
+ this.send({
18084
+ ...this.buildCommon(cid, 'PeerConnectionConnect', pair),
18085
+ ...this.sessionIdField(cid),
18086
+ peer_connection: role,
18087
+ was_previously_connected: pair.wasPreviouslyConnected,
18088
+ ...(pair.userSessionId && {
18089
+ user_session_id: pair.userSessionId,
18090
+ }),
18091
+ ...(pair.sfuId && { sfu_id: pair.sfuId }),
18092
+ event_type: 'completed',
18093
+ outcome: 'failure',
18094
+ retry_count_attempt: 0,
18095
+ elapsed_time: Date.now() - pair.startedAt,
18096
+ ice_state: iceState,
18097
+ retry_failure_reason: reason,
18098
+ retry_failure_code: code,
18099
+ });
18100
+ this.peerConnectionPairs.delete(key);
18101
+ };
18102
+ this.getSfuId = (cid) => this.callContexts.get(cid)?.getSfuId() ?? '';
18103
+ this.getUserSessionId = (cid) => this.callContexts.get(cid)?.getUserSessionId() ?? '';
18104
+ this.sessionIdField = (cid) => {
18105
+ const callSessionId = this.callContexts.get(cid)?.getCallSessionId() ?? '';
18106
+ return callSessionId ? { call_session_id: callSessionId } : {};
18107
+ };
18108
+ this.buildCommon = (cid, stage, pair) => {
18109
+ const ctx = this.callContexts.get(cid);
18110
+ const coordinatorConnectId = this.coordinatorConnectId;
18111
+ return {
18112
+ user_id: this.streamClient.userID,
18113
+ type: ctx?.callType ?? '',
18114
+ id: ctx?.callId ?? '',
18115
+ call_cid: cid,
18116
+ stage,
18117
+ stage_id: pair.sid,
18118
+ ...(pair.joinAttemptIdSnapshot && {
18119
+ join_attempt_id: pair.joinAttemptIdSnapshot,
18120
+ }),
18121
+ ...(coordinatorConnectId && {
18122
+ coordinator_connect_id: coordinatorConnectId,
18123
+ }),
18124
+ timestamp: new Date().toISOString(),
18125
+ user_agent: this.streamClient.getUserAgent(),
18126
+ sdk_version: this.streamClient.getSdkVersion(),
18127
+ };
18128
+ };
18129
+ this.send = (body) => {
18130
+ void this.sendWithRetry(body);
18131
+ };
18132
+ this.sendWithRetry = async (body) => {
18133
+ for (let attempt = 0; attempt < 5; attempt++) {
18134
+ try {
18135
+ await this.streamClient.doAxiosRequest('post', '/call_client_event', { events: [body] }, { publicEndpoint: true });
18136
+ return true;
18137
+ }
18138
+ catch (err) {
18139
+ const status = err?.response
18140
+ ?.status;
18141
+ if (typeof status === 'number' && status >= 400 && status < 500) {
18142
+ this.logger.debug(`Client event rejected (${status}), not retrying`, body.stage, body.event_type);
18143
+ return false;
18144
+ }
18145
+ if (attempt === 4) {
18146
+ this.logger.debug('Client event delivery failed after retries', body.stage, body.event_type, err);
18147
+ return false;
18148
+ }
18149
+ await sleep(retryInterval(attempt));
18150
+ }
18151
+ }
18152
+ return false;
18153
+ };
18154
+ this.streamClient = options.streamClient;
18155
+ }
18156
+ }
18157
+ const readPermissionStatus = (permission) => {
18158
+ const state = getCurrentValue(permission.asStateObservable());
18159
+ switch (state) {
18160
+ case 'granted':
18161
+ return 'GRANTED';
18162
+ case 'denied':
18163
+ return 'FAILED';
18164
+ case 'prompting':
18165
+ return 'INITIATED';
18166
+ case 'prompt':
18167
+ default:
18168
+ return 'NOT_INITIATED';
18169
+ }
18170
+ };
18171
+ const errorMessage = (err) => err instanceof Error ? err.message : String(err);
18172
+ const applyError = (pair, next) => {
18173
+ if (!pair)
18174
+ return;
18175
+ pair.lastError = next;
18176
+ };
18177
+ const applyErrorIfAbsent = (pair, next) => {
18178
+ if (!pair || pair.lastError)
18179
+ return;
18180
+ pair.lastError = next;
18181
+ };
18182
+ const mapCoordinatorHttpError = (err) => {
18183
+ if (err instanceof ErrorFromResponse) {
18184
+ return {
18185
+ reason: err.message,
18186
+ code: err.code != null ? String(err.code) : 'SERVER_ERROR',
18187
+ };
18188
+ }
18189
+ return { reason: errorMessage(err), code: 'SERVER_ERROR' };
18190
+ };
18191
+ const mapCoordinatorWsError = (err) => {
18192
+ if (err instanceof ErrorFromResponse) {
18193
+ return {
18194
+ reason: err.message,
18195
+ code: err.code != null ? String(err.code) : 'SERVER_ERROR',
18196
+ };
18197
+ }
18198
+ if (err instanceof Error) {
18199
+ try {
18200
+ const parsed = JSON.parse(err.message);
18201
+ if (typeof parsed.isWSFailure === 'boolean') {
18202
+ return {
18203
+ reason: parsed.message || err.message,
18204
+ code: !parsed.isWSFailure && parsed.code
18205
+ ? String(parsed.code)
18206
+ : 'SERVER_ERROR',
18207
+ };
18208
+ }
18209
+ }
18210
+ catch { }
18211
+ }
18212
+ return { reason: errorMessage(err), code: 'SERVER_ERROR' };
18213
+ };
18214
+ const mapWsJoinError = (err) => {
18215
+ if (err instanceof SfuJoinError) {
18216
+ const sfuError = err.errorEvent.error;
18217
+ return {
18218
+ reason: sfuError?.message || err.message,
18219
+ code: (sfuError && ErrorCode[sfuError.code]) || 'SFU_ERROR',
18220
+ };
18221
+ }
18222
+ const reason = errorMessage(err);
18223
+ if (err instanceof SfuTimeoutError) {
18224
+ return { reason, code: 'REQUEST_TIMEOUT' };
18225
+ }
18226
+ return { reason, code: 'SFU_ERROR' };
18227
+ };
18228
+
17561
18229
  /**
17562
18230
  * A `StreamVideoClient` instance lets you communicate with our API, and authenticate users.
17563
18231
  */
@@ -17621,6 +18289,7 @@ class StreamVideoClient {
17621
18289
  }
17622
18290
  call = new Call({
17623
18291
  streamClient: this.streamClient,
18292
+ clientEventReporter: this.clientEventReporter,
17624
18293
  type: e.call.type,
17625
18294
  id: e.call.id,
17626
18295
  members: e.members,
@@ -17690,6 +18359,8 @@ class StreamVideoClient {
17690
18359
  user.id = '!anon';
17691
18360
  return this.connectAnonymousUser(user, tokenOrProvider);
17692
18361
  }
18362
+ const reporter = this.clientEventReporter;
18363
+ reporter.startCoordinatorConnection(user.id);
17693
18364
  const connectUserResponse = await withoutConcurrency(this.connectionConcurrencyTag, async () => {
17694
18365
  const client = this.streamClient;
17695
18366
  const { onConnectUserError, persistUserOnConnectionFailure } = client.options;
@@ -17699,14 +18370,15 @@ class StreamVideoClient {
17699
18370
  for (let attempt = 0; attempt < maxConnectUserRetries; attempt++) {
17700
18371
  try {
17701
18372
  this.logger.trace(`Connecting user (${attempt})`, user);
17702
- return user.type === 'guest'
17703
- ? await client.connectGuestUser(user)
17704
- : await client.connectUser(user, tokenOrProvider);
18373
+ return await reporter.trackCoordinatorWs(() => user.type === 'guest'
18374
+ ? client.connectGuestUser(user)
18375
+ : client.connectUser(user, tokenOrProvider));
17705
18376
  }
17706
18377
  catch (err) {
17707
18378
  this.logger.warn(`Failed to connect a user (${attempt})`, err);
17708
18379
  errorQueue.push(err);
17709
18380
  if (attempt === maxConnectUserRetries - 1) {
18381
+ reporter.closeCoordinatorWs();
17710
18382
  onConnectUserError?.(err, errorQueue);
17711
18383
  throw err;
17712
18384
  }
@@ -17784,6 +18456,7 @@ class StreamVideoClient {
17784
18456
  return (call ??
17785
18457
  new Call({
17786
18458
  streamClient: this.streamClient,
18459
+ clientEventReporter: this.clientEventReporter,
17787
18460
  id: id,
17788
18461
  type: type,
17789
18462
  clientStore: this.writeableStateStore,
@@ -17808,6 +18481,7 @@ class StreamVideoClient {
17808
18481
  for (const c of response.calls) {
17809
18482
  const call = new Call({
17810
18483
  streamClient: this.streamClient,
18484
+ clientEventReporter: this.clientEventReporter,
17811
18485
  id: c.call.id,
17812
18486
  type: c.call.type,
17813
18487
  members: c.members,
@@ -17915,6 +18589,7 @@ class StreamVideoClient {
17915
18589
  const [callType, callId] = call_cid.split(':');
17916
18590
  call = new Call({
17917
18591
  streamClient: this.streamClient,
18592
+ clientEventReporter: this.clientEventReporter,
17918
18593
  type: callType,
17919
18594
  id: callId,
17920
18595
  clientStore: this.writeableStateStore,
@@ -17955,6 +18630,9 @@ class StreamVideoClient {
17955
18630
  this.logger = videoLoggerSystem.getLogger('client');
17956
18631
  this.rejectCallWhenBusy = clientOptions?.rejectCallWhenBusy ?? false;
17957
18632
  this.streamClient = createCoordinatorClient(apiKey, clientOptions);
18633
+ this.clientEventReporter = new ClientEventReporter({
18634
+ streamClient: this.streamClient,
18635
+ });
17958
18636
  this.writeableStateStore = new StreamVideoWriteableStateStore();
17959
18637
  this.readOnlyStateStore = new StreamVideoReadOnlyStateStore(this.writeableStateStore);
17960
18638
  if (typeof apiKeyOrArgs !== 'string' && apiKeyOrArgs.user) {
@@ -18017,5 +18695,5 @@ const humanize = (n) => {
18017
18695
  return String(n);
18018
18696
  };
18019
18697
 
18020
- export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LayoutSettingsRequestNameEnum, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getClientDetails, getDeviceState, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, withParticipantSource };
18698
+ export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LayoutSettingsRequestNameEnum, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SfuTimeoutError, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, checkIfAudioOutputChangeSupported, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getClientDetails, getDeviceState, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, withParticipantSource };
18021
18699
  //# sourceMappingURL=index.es.js.map