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