@stream-io/video-client 1.52.1-beta.0 → 1.53.1

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 (69) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/index.browser.es.js +819 -123
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +819 -122
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +819 -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/devices/MicrophoneManager.d.ts +6 -0
  12. package/dist/src/errors/SfuTimeoutError.d.ts +8 -0
  13. package/dist/src/errors/index.d.ts +1 -0
  14. package/dist/src/gen/google/protobuf/struct.d.ts +1 -3
  15. package/dist/src/gen/google/protobuf/timestamp.d.ts +1 -3
  16. package/dist/src/gen/video/sfu/event/events.d.ts +1 -22
  17. package/dist/src/gen/video/sfu/models/models.d.ts +0 -4
  18. package/dist/src/gen/video/sfu/signal_rpc/signal.client.d.ts +2 -23
  19. package/dist/src/helpers/firstVideoFrame.d.ts +7 -0
  20. package/dist/src/reporting/ClientEventReporter.d.ts +84 -0
  21. package/dist/src/reporting/index.d.ts +1 -0
  22. package/dist/src/rtc/BasePeerConnection.d.ts +5 -2
  23. package/dist/src/rtc/Publisher.d.ts +1 -4
  24. package/dist/src/rtc/Subscriber.d.ts +0 -7
  25. package/dist/src/rtc/types.d.ts +24 -1
  26. package/dist/src/types.d.ts +16 -0
  27. package/package.json +1 -1
  28. package/src/Call.ts +185 -106
  29. package/src/StreamSfuClient.ts +3 -3
  30. package/src/StreamVideoClient.ts +18 -3
  31. package/src/__tests__/Call.autodrop.test.ts +4 -1
  32. package/src/__tests__/Call.lifecycle.test.ts +4 -1
  33. package/src/__tests__/Call.publishing.test.ts +4 -1
  34. package/src/__tests__/Call.test.ts +23 -0
  35. package/src/coordinator/connection/client.ts +5 -0
  36. package/src/devices/MicrophoneManager.ts +16 -0
  37. package/src/devices/__tests__/CameraManager.test.ts +10 -1
  38. package/src/devices/__tests__/DeviceManager.test.ts +10 -1
  39. package/src/devices/__tests__/DeviceManagerFilters.test.ts +4 -1
  40. package/src/devices/__tests__/MicrophoneManager.test.ts +10 -1
  41. package/src/devices/__tests__/MicrophoneManagerRN.test.ts +78 -2
  42. package/src/devices/__tests__/ScreenShareManager.test.ts +4 -1
  43. package/src/devices/__tests__/SpeakerManager.test.ts +13 -4
  44. package/src/errors/SfuTimeoutError.ts +7 -0
  45. package/src/errors/index.ts +1 -0
  46. package/src/events/__tests__/call.test.ts +2 -0
  47. package/src/events/__tests__/mutes.test.ts +4 -1
  48. package/src/events/call.ts +8 -0
  49. package/src/gen/google/protobuf/struct.ts +12 -7
  50. package/src/gen/google/protobuf/timestamp.ts +7 -6
  51. package/src/gen/video/sfu/event/events.ts +25 -23
  52. package/src/gen/video/sfu/models/models.ts +1 -11
  53. package/src/gen/video/sfu/signal_rpc/signal.client.ts +29 -25
  54. package/src/gen/video/sfu/signal_rpc/signal.ts +0 -1
  55. package/src/helpers/__tests__/AudioBindingsWatchdog.test.ts +6 -3
  56. package/src/helpers/__tests__/DynascaleManager.test.ts +6 -3
  57. package/src/helpers/__tests__/ViewportTracker.test.ts +6 -3
  58. package/src/helpers/__tests__/firstVideoFrame.test.ts +91 -0
  59. package/src/helpers/client-details.ts +1 -1
  60. package/src/helpers/firstVideoFrame.ts +38 -0
  61. package/src/reporting/ClientEventReporter.ts +864 -0
  62. package/src/reporting/__tests__/ClientEventReporter.test.ts +620 -0
  63. package/src/reporting/index.ts +1 -0
  64. package/src/rtc/BasePeerConnection.ts +30 -0
  65. package/src/rtc/Publisher.ts +0 -4
  66. package/src/rtc/Subscriber.ts +2 -28
  67. package/src/rtc/__tests__/Call.reconnect.test.ts +3 -0
  68. package/src/rtc/types.ts +34 -0
  69. package/src/types.ts +18 -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.1";
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,
@@ -13099,6 +13140,7 @@ class MicrophoneManager extends AudioDeviceManager {
13099
13140
  ]), async ([callingState, ownCapabilities, deviceId, status, permissionState,]) => {
13100
13141
  try {
13101
13142
  if (callingState === CallingState.LEFT) {
13143
+ this.setMutedRecordingPrepared(false);
13102
13144
  await this.stopSpeakingWhileMutedDetection();
13103
13145
  }
13104
13146
  if (callingState !== CallingState.JOINED)
@@ -13108,13 +13150,16 @@ class MicrophoneManager extends AudioDeviceManager {
13108
13150
  if (ownCapabilities.includes(OwnCapability.SEND_AUDIO)) {
13109
13151
  const hasPermission = await this.hasPermission(permissionState);
13110
13152
  if (hasPermission && status !== 'enabled') {
13153
+ this.setMutedRecordingPrepared(true);
13111
13154
  await this.startSpeakingWhileMutedDetection(deviceId);
13112
13155
  }
13113
13156
  else {
13157
+ this.setMutedRecordingPrepared(false);
13114
13158
  await this.stopSpeakingWhileMutedDetection();
13115
13159
  }
13116
13160
  }
13117
13161
  else {
13162
+ this.setMutedRecordingPrepared(false);
13118
13163
  await this.stopSpeakingWhileMutedDetection();
13119
13164
  }
13120
13165
  }
@@ -13432,6 +13477,16 @@ class MicrophoneManager extends AudioDeviceManager {
13432
13477
  this.logger.warn('Failed to stop speaking while muted detector', err);
13433
13478
  });
13434
13479
  }
13480
+ /**
13481
+ * iOS-only: keep the mic-input chain prepared while muted
13482
+ * so the `AVAudioEngine` stays full-duplex and remote audio renders on a
13483
+ * muted join.
13484
+ */
13485
+ setMutedRecordingPrepared(enabled) {
13486
+ if (!isReactNative())
13487
+ return;
13488
+ globalThis.streamRNVideoSDK?.callManager.setMutedRecordingPrepared?.(enabled);
13489
+ }
13435
13490
  async hasPermission(permissionState) {
13436
13491
  if (!isReactNative())
13437
13492
  return permissionState === 'granted';
@@ -13824,7 +13879,7 @@ class Call {
13824
13879
  * Use the [`StreamVideoClient.call`](./StreamVideoClient.md/#call)
13825
13880
  * method to construct a `Call` instance.
13826
13881
  */
13827
- constructor({ type, id, streamClient, members, ownCapabilities, sortParticipantsBy, clientStore, ringing = false, watching = false, }) {
13882
+ constructor({ type, id, streamClient, clientEventReporter, members, ownCapabilities, sortParticipantsBy, clientStore, ringing = false, watching = false, }) {
13828
13883
  /**
13829
13884
  * The state of this call.
13830
13885
  */
@@ -13861,7 +13916,6 @@ class Call {
13861
13916
  // maintain the order of publishing tracks to restore them after a reconnection
13862
13917
  // it shouldn't contain duplicates
13863
13918
  this.trackPublishOrder = [];
13864
- this.selfSubEnabled = false;
13865
13919
  this.hasJoinedOnce = false;
13866
13920
  this.deviceSettingsAppliedOnce = false;
13867
13921
  this.initialized = false;
@@ -14152,9 +14206,14 @@ class Call {
14152
14206
  this.sfuStatsReporter = undefined;
14153
14207
  this.lastStatsOptions = undefined;
14154
14208
  await this.subscriber?.dispose();
14209
+ this.clientEventReporter.abort(this.cid, {
14210
+ code: 'CLIENT_ABORTED',
14211
+ reason: leaveReason,
14212
+ });
14155
14213
  this.subscriber = undefined;
14156
14214
  await this.publisher?.dispose();
14157
14215
  this.publisher = undefined;
14216
+ this.clientEventReporter.unregisterCall(this.cid);
14158
14217
  await this.sfuClient?.leaveAndClose(leaveReason);
14159
14218
  this.sfuClient = undefined;
14160
14219
  this.trackSubscriptionManager.setSfuClient(undefined);
@@ -14206,30 +14265,6 @@ class Call {
14206
14265
  await Promise.all(stopOnLeavePromises);
14207
14266
  });
14208
14267
  };
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
14268
  /**
14234
14269
  * Update from the call response from the "call.ring" event
14235
14270
  * @internal
@@ -14376,7 +14411,7 @@ class Call {
14376
14411
  *
14377
14412
  * @returns a promise which resolves once the call join-flow has finished.
14378
14413
  */
14379
- this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, selfSubEnabled = false, ...data } = {}) => {
14414
+ this.join = async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, ...data } = {}) => {
14380
14415
  const callingState = this.state.callingState;
14381
14416
  if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
14382
14417
  throw new Error(`Illegal State: call.join() shall be called only once`);
@@ -14384,15 +14419,19 @@ class Call {
14384
14419
  if (data?.ring) {
14385
14420
  this.ringingSubject.next(true);
14386
14421
  }
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
14422
  const callingX = globalThis.streamRNVideoSDK?.callingX;
14391
14423
  if (callingX) {
14392
14424
  // for Android/iOS, we need to start the call in the callingx library as soon as possible
14393
14425
  await callingX.joinCall(this, this.clientStore.calls);
14394
14426
  }
14395
14427
  await this.setup();
14428
+ this.clientEventReporter.registerCall(this.cid, {
14429
+ callType: this.type,
14430
+ callId: this.id,
14431
+ getCallSessionId: () => this.state.session?.id ?? '',
14432
+ getSfuId: () => this.credentials?.server.edge_name ?? '',
14433
+ getUserSessionId: () => this.sfuClient?.sessionId ?? '',
14434
+ });
14396
14435
  this.joinResponseTimeout = joinResponseTimeout;
14397
14436
  this.rpcRequestTimeout = rpcRequestTimeout;
14398
14437
  // we will count the number of join failures per SFU.
@@ -14402,39 +14441,42 @@ class Call {
14402
14441
  const joinData = data;
14403
14442
  maxJoinRetries = Math.max(maxJoinRetries, 1);
14404
14443
  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());
14444
+ await this.clientEventReporter.withJoinLifecycle(this.cid, 'first-attempt', async () => {
14445
+ for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
14446
+ try {
14447
+ this.logger.trace(`Joining call (${attempt})`, this.cid);
14448
+ await this.doJoin(data);
14449
+ delete joinData.migrating_from;
14450
+ delete joinData.migrating_from_list;
14451
+ return;
14431
14452
  }
14432
- if (attempt === maxJoinRetries - 1) {
14433
- throw err;
14453
+ catch (err) {
14454
+ this.logger.warn(`Failed to join call (${attempt})`, this.cid);
14455
+ if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
14456
+ (err instanceof SfuJoinError && err.unrecoverable)) {
14457
+ throw err;
14458
+ }
14459
+ const switchSfu = err instanceof SfuJoinError &&
14460
+ SfuJoinError.isJoinErrorCode(err.errorEvent);
14461
+ const sfuId = this.credentials?.server.edge_name;
14462
+ if (sfuId) {
14463
+ const failures = (sfuJoinFailures.get(sfuId) || 0) + 1;
14464
+ sfuJoinFailures.set(sfuId, failures);
14465
+ if (switchSfu || failures >= 2) {
14466
+ joinData.migrating_from = sfuId;
14467
+ joinData.migrating_from_list = Array.from(sfuJoinFailures.keys());
14468
+ if (attempt < maxJoinRetries - 1) {
14469
+ this.clientEventReporter.startCorrelation(this.cid, 'first-attempt');
14470
+ }
14471
+ }
14472
+ }
14473
+ if (attempt === maxJoinRetries - 1) {
14474
+ throw err;
14475
+ }
14434
14476
  }
14477
+ await sleep(retryInterval(attempt));
14435
14478
  }
14436
- await sleep(retryInterval(attempt));
14437
- }
14479
+ });
14438
14480
  }
14439
14481
  catch (error) {
14440
14482
  callingX?.endCall(this, 'error');
@@ -14463,7 +14505,7 @@ class Call {
14463
14505
  performingMigration ||
14464
14506
  data?.migrating_from) {
14465
14507
  try {
14466
- const joinResponse = await this.doJoinRequest(data);
14508
+ const joinResponse = await this.clientEventReporter.track(this.cid, 'CoordinatorJoin', () => this.doJoinRequest(data));
14467
14509
  this.credentials = joinResponse.credentials;
14468
14510
  statsOptions = joinResponse.stats_options;
14469
14511
  this.lastStatsOptions = statsOptions;
@@ -14521,9 +14563,11 @@ class Call {
14521
14563
  const preferredSubscribeOptions = !isReconnecting
14522
14564
  ? this.getPreferredSubscribeOptions()
14523
14565
  : [];
14566
+ const unifiedSessionId = this.unifiedSessionId;
14567
+ const capabilities = Array.from(this.clientCapabilities);
14524
14568
  try {
14525
- const { callState, fastReconnectDeadlineSeconds, publishOptions } = await sfuClient.join({
14526
- unifiedSessionId: this.unifiedSessionId,
14569
+ const { callState, fastReconnectDeadlineSeconds, publishOptions } = await this.clientEventReporter.track(this.cid, 'WSJoin', () => sfuClient.join({
14570
+ unifiedSessionId,
14527
14571
  subscriberSdp,
14528
14572
  publisherSdp,
14529
14573
  clientDetails,
@@ -14531,9 +14575,9 @@ class Call {
14531
14575
  reconnectDetails,
14532
14576
  preferredPublishOptions,
14533
14577
  preferredSubscribeOptions,
14534
- capabilities: Array.from(this.clientCapabilities),
14578
+ capabilities,
14535
14579
  source: ParticipantSource.WEBRTC_UNSPECIFIED,
14536
- });
14580
+ }));
14537
14581
  this.currentPublishOptions = publishOptions;
14538
14582
  this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
14539
14583
  if (callState) {
@@ -14745,6 +14789,16 @@ class Call {
14745
14789
  // "ICE never connected" failure budget can be cleared.
14746
14790
  this.iceFailuresWithoutConnect = 0;
14747
14791
  },
14792
+ onPeerConnectionStateChange: (event) => {
14793
+ this.clientEventReporter.onPeerConnectionStateChange(this.cid, event);
14794
+ },
14795
+ onRemoteTrackUnmute: (trackType, trackId) => {
14796
+ const reportable = trackType === TrackType.AUDIO ||
14797
+ (isReactNative() && trackType === TrackType.VIDEO);
14798
+ if (!reportable)
14799
+ return;
14800
+ this.clientEventReporter.reportFirstFrame(this.cid, trackType, trackId);
14801
+ },
14748
14802
  };
14749
14803
  this.subscriber = new Subscriber(basePeerConnectionOptions);
14750
14804
  // anonymous users can't publish anything hence, there is no need
@@ -14754,9 +14808,7 @@ class Call {
14754
14808
  if (closePreviousInstances && this.publisher) {
14755
14809
  await this.publisher.dispose();
14756
14810
  }
14757
- this.publisher = new Publisher(basePeerConnectionOptions, publishOptions, {
14758
- selfSubEnabled: this.selfSubEnabled,
14759
- });
14811
+ this.publisher = new Publisher(basePeerConnectionOptions, publishOptions);
14760
14812
  }
14761
14813
  this.statsReporter?.stop();
14762
14814
  if (this.statsReportingIntervalInMs > 0) {
@@ -15023,7 +15075,10 @@ class Call {
15023
15075
  const reconnectStartTime = Date.now();
15024
15076
  this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
15025
15077
  this.state.setCallingState(CallingState.RECONNECTING);
15026
- await this.doJoin(this.joinCallData);
15078
+ const joinReason = this.reconnectReason === ReconnectReason.NETWORK_BACK_ONLINE
15079
+ ? 'network-available'
15080
+ : 'full-rejoin';
15081
+ await this.clientEventReporter.withJoinLifecycle(this.cid, joinReason, () => this.doJoin(this.joinCallData));
15027
15082
  await this.restorePublishedTracks();
15028
15083
  this.restoreSubscribedTracks();
15029
15084
  this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
@@ -15047,11 +15102,11 @@ class Call {
15047
15102
  const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
15048
15103
  try {
15049
15104
  const currentSfu = currentSfuClient.edgeName;
15050
- await this.doJoin({
15105
+ await this.clientEventReporter.withJoinLifecycle(this.cid, 'migration', () => this.doJoin({
15051
15106
  ...this.joinCallData,
15052
15107
  migrating_from: currentSfu,
15053
15108
  migrating_from_list: [currentSfu],
15054
- });
15109
+ }));
15055
15110
  }
15056
15111
  finally {
15057
15112
  // cleanup the migration_from field after the migration is complete or failed
@@ -15087,11 +15142,22 @@ class Call {
15087
15142
  this.registerReconnectHandlers = () => {
15088
15143
  // handles the legacy "goAway" event
15089
15144
  const unregisterGoAway = this.on('goAway', () => {
15145
+ this.clientEventReporter.captureWsError(this.cid, {
15146
+ code: 'SFU_GO_AWAY',
15147
+ reason: 'SFU goAway received during WS join',
15148
+ });
15090
15149
  this.reconnect(WebsocketReconnectStrategy.MIGRATE, ReconnectReason.GO_AWAY).catch((err) => this.logger.warn('[Reconnect] Error reconnecting', err));
15091
15150
  });
15092
15151
  // handles the "error" event, through which the SFU can request a reconnect
15093
15152
  const unregisterOnError = this.on('error', (e) => {
15094
15153
  const { reconnectStrategy: strategy, error } = e;
15154
+ if (!SfuJoinError.isJoinErrorCode(e)) {
15155
+ const code = error?.code ? ErrorCode[error.code] : undefined;
15156
+ this.clientEventReporter.captureWsError(this.cid, {
15157
+ code: code ?? 'SFU_ERROR',
15158
+ reason: error?.message || 'SFU error during WS join',
15159
+ });
15160
+ }
15095
15161
  // SFU_FULL is a join error, and when emitted, although it specifies a
15096
15162
  // `migrate` strategy, we should actually perform a REJOIN to a new SFU.
15097
15163
  // This is now handled separately in the `call.join()` method.
@@ -15786,7 +15852,9 @@ class Call {
15786
15852
  this.leave({
15787
15853
  reject: true,
15788
15854
  reason: 'timeout',
15789
- message: `ringing timeout - ${this.isCreatedByMe ? 'no one accepted' : `user didn't interact with incoming call screen`}`,
15855
+ message: `ringing timeout - ${this.isCreatedByMe
15856
+ ? 'no one accepted'
15857
+ : `user didn't interact with incoming call screen`}`,
15790
15858
  }).catch((err) => {
15791
15859
  this.logger.error('Failed to drop call', err);
15792
15860
  });
@@ -15992,15 +16060,36 @@ class Call {
15992
16060
  * @param trackType the kind of video.
15993
16061
  */
15994
16062
  this.bindVideoElement = (videoElement, sessionId, trackType) => {
15995
- const unbind = this.dynascaleManager?.bindVideoElement(videoElement, sessionId, trackType);
15996
- if (!unbind)
16063
+ const unbindDynascale = this.dynascaleManager?.bindVideoElement(videoElement, sessionId, trackType);
16064
+ const stopFirstFrameDetector = this.bindFirstVideoFrameDetector(videoElement, sessionId, trackType);
16065
+ if (!unbindDynascale && !stopFirstFrameDetector)
15997
16066
  return;
16067
+ const unbind = () => {
16068
+ stopFirstFrameDetector?.();
16069
+ unbindDynascale?.();
16070
+ };
15998
16071
  this.leaveCallHooks.add(unbind);
15999
16072
  return () => {
16000
16073
  this.leaveCallHooks.delete(unbind);
16001
16074
  unbind();
16002
16075
  };
16003
16076
  };
16077
+ this.bindFirstVideoFrameDetector = (videoElement, sessionId, trackType) => {
16078
+ if (trackType !== 'videoTrack')
16079
+ return;
16080
+ return createFirstVideoFrameDetector(videoElement, () => {
16081
+ this.reportFirstRenderedVideoFrame(sessionId);
16082
+ });
16083
+ };
16084
+ this.reportFirstRenderedVideoFrame = (sessionId) => {
16085
+ const participant = this.state.findParticipantBySessionId(sessionId);
16086
+ if (participant?.isLocalParticipant)
16087
+ return;
16088
+ const trackId = participant?.videoStream?.getVideoTracks()[0]?.id;
16089
+ if (!trackId)
16090
+ return;
16091
+ this.clientEventReporter.reportFirstFrame(this.cid, TrackType.VIDEO, trackId);
16092
+ };
16004
16093
  /**
16005
16094
  * Binds a DOM <audio> element to the given session id.
16006
16095
  *
@@ -16150,6 +16239,7 @@ class Call {
16150
16239
  this.ringingSubject = new BehaviorSubject(ringing);
16151
16240
  this.watching = watching;
16152
16241
  this.streamClient = streamClient;
16242
+ this.clientEventReporter = clientEventReporter;
16153
16243
  this.clientStore = clientStore;
16154
16244
  this.streamClientBasePath = `/call/${this.type}/${this.id}`;
16155
16245
  this.logger = videoLoggerSystem.getLogger('Call');
@@ -16186,12 +16276,6 @@ class Call {
16186
16276
  get currentUserId() {
16187
16277
  return this.clientStore.connectedUser?.id;
16188
16278
  }
16189
- /**
16190
- * A flag indicating whether self-subscription is enabled for the call.
16191
- */
16192
- get isSelfSubEnabled() {
16193
- return this.selfSubEnabled;
16194
- }
16195
16279
  /**
16196
16280
  * A flag indicating whether the call was created by the current user.
16197
16281
  */
@@ -17378,10 +17462,12 @@ class StreamClient {
17378
17462
  this.logger.info('StreamClient.connect: this.wsConnection.connect()');
17379
17463
  return await this.wsConnection.connect(this.defaultWSTimeout);
17380
17464
  };
17465
+ this.getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
17466
+ "1.53.1";
17381
17467
  this.getUserAgent = () => {
17382
17468
  if (!this.cachedUserAgent) {
17383
17469
  const { clientAppIdentifier = {} } = this.options;
17384
- const { sdkName = 'js', sdkVersion = "1.52.1-beta.0", ...extras } = clientAppIdentifier;
17470
+ const { sdkName = 'js', sdkVersion = "1.53.1", ...extras } = clientAppIdentifier;
17385
17471
  this.cachedUserAgent = [
17386
17472
  `stream-video-${sdkName}-v${sdkVersion}`,
17387
17473
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -17558,6 +17644,606 @@ const createTokenOrProvider = (options) => {
17558
17644
  return token || tokenProvider;
17559
17645
  };
17560
17646
 
17647
+ const pcKey = (cid, role) => `${cid}:${role}`;
17648
+ class ClientEventReporter {
17649
+ constructor(options) {
17650
+ this.logger = videoLoggerSystem.getLogger('ClientEventReporter');
17651
+ this.callContexts = new Map();
17652
+ this.joinAttemptIds = new Map();
17653
+ this.joinReasons = new Map();
17654
+ this.coordinatorPairs = new Map();
17655
+ this.wsPairs = new Map();
17656
+ this.peerConnectionPairs = new Map();
17657
+ this.pcEverConnected = new Map();
17658
+ this.firstFrameReported = new Set();
17659
+ /**
17660
+ * Starts a new coordinator connection correlation scope.
17661
+ *
17662
+ * @param userId the id of the user being connected. Captured here because
17663
+ * the `CoordinatorWS` stage emits before the connection flow assigns
17664
+ * the user to the client, so it can't be read from the client yet.
17665
+ */
17666
+ this.startCoordinatorConnection = (userId) => {
17667
+ this.coordinatorConnectId = generateUUIDv4();
17668
+ this.coordinatorConnectUserId = userId;
17669
+ return this.coordinatorConnectId;
17670
+ };
17671
+ this.trackCoordinatorWs = async (op) => {
17672
+ this.beginCoordinatorWs();
17673
+ try {
17674
+ const result = await op();
17675
+ this.succeedCoordinatorWs();
17676
+ return result;
17677
+ }
17678
+ catch (err) {
17679
+ applyError(this.coordinatorWsPair, mapCoordinatorWsError(err));
17680
+ throw err;
17681
+ }
17682
+ };
17683
+ this.beginCoordinatorWs = () => {
17684
+ if (!this.coordinatorWsPair) {
17685
+ this.coordinatorWsPair = {
17686
+ sid: generateUUIDv4(),
17687
+ attempts: 0,
17688
+ startedAt: Date.now(),
17689
+ userIdSnapshot: this.coordinatorConnectUserId,
17690
+ };
17691
+ this.send({
17692
+ ...this.buildCoordinatorWsCommon(this.coordinatorWsPair),
17693
+ event_type: 'initiated',
17694
+ });
17695
+ }
17696
+ this.coordinatorWsPair.attempts++;
17697
+ };
17698
+ this.succeedCoordinatorWs = () => {
17699
+ const pair = this.coordinatorWsPair;
17700
+ if (!pair)
17701
+ return;
17702
+ this.send({
17703
+ ...this.buildCoordinatorWsCommon(pair),
17704
+ event_type: 'completed',
17705
+ outcome: 'success',
17706
+ retry_count_attempt: pair.attempts - 1,
17707
+ elapsed_time: Date.now() - pair.startedAt,
17708
+ });
17709
+ this.coordinatorWsPair = undefined;
17710
+ };
17711
+ this.closeCoordinatorWs = () => {
17712
+ const pair = this.coordinatorWsPair;
17713
+ if (!pair || !pair.lastError) {
17714
+ this.coordinatorWsPair = undefined;
17715
+ return;
17716
+ }
17717
+ const { reason, code } = pair.lastError;
17718
+ this.send({
17719
+ ...this.buildCoordinatorWsCommon(pair),
17720
+ event_type: 'completed',
17721
+ outcome: 'failure',
17722
+ retry_count_attempt: pair.attempts - 1,
17723
+ elapsed_time: Date.now() - pair.startedAt,
17724
+ retry_failure_reason: reason,
17725
+ retry_failure_code: code,
17726
+ });
17727
+ this.coordinatorWsPair = undefined;
17728
+ };
17729
+ this.buildCoordinatorWsCommon = (pair) => ({
17730
+ user_id: pair.userIdSnapshot ?? this.streamClient.userID,
17731
+ stage: 'CoordinatorWS',
17732
+ stage_id: pair.sid,
17733
+ ...(this.coordinatorConnectId && {
17734
+ coordinator_connect_id: this.coordinatorConnectId,
17735
+ }),
17736
+ timestamp: new Date().toISOString(),
17737
+ user_agent: this.streamClient.getUserAgent(),
17738
+ sdk_version: this.streamClient.getSdkVersion(),
17739
+ });
17740
+ this.emitMediaPermission = (cid) => {
17741
+ if (isReactNative() || !this.callContexts.has(cid))
17742
+ return;
17743
+ const pair = {
17744
+ sid: generateUUIDv4(),
17745
+ attempts: 0,
17746
+ startedAt: Date.now(),
17747
+ joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
17748
+ };
17749
+ this.send({
17750
+ ...this.buildCommon(cid, 'MediaDevicePermission', pair),
17751
+ ...this.sessionIdField(cid),
17752
+ microphone_permission_status: readPermissionStatus(getAudioBrowserPermission()),
17753
+ camera_permission_status: readPermissionStatus(getVideoBrowserPermission()),
17754
+ event_type: 'initiated',
17755
+ });
17756
+ };
17757
+ this.registerCall = (cid, ctx) => {
17758
+ this.callContexts.set(cid, ctx);
17759
+ };
17760
+ this.unregisterCall = (cid) => {
17761
+ this.callContexts.delete(cid);
17762
+ this.joinAttemptIds.delete(cid);
17763
+ this.joinReasons.delete(cid);
17764
+ this.coordinatorPairs.delete(cid);
17765
+ this.wsPairs.delete(cid);
17766
+ this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
17767
+ this.firstFrameReported.delete(`${cid}:FirstAudioFrame`);
17768
+ for (const role of ['publish', 'subscribe']) {
17769
+ const key = pcKey(cid, role);
17770
+ this.peerConnectionPairs.delete(key);
17771
+ this.pcEverConnected.delete(key);
17772
+ }
17773
+ };
17774
+ this.startCorrelation = (cid, joinReason) => {
17775
+ try {
17776
+ this.closeCallPairs(cid);
17777
+ this.joinAttemptIds.set(cid, generateUUIDv4());
17778
+ this.joinReasons.set(cid, joinReason);
17779
+ this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
17780
+ this.firstFrameReported.delete(`${cid}:FirstAudioFrame`);
17781
+ this.emitJoinInitiated(cid);
17782
+ this.emitMediaPermission(cid);
17783
+ }
17784
+ catch (err) {
17785
+ this.logger.warn('Failed to start join correlation', err);
17786
+ }
17787
+ };
17788
+ this.withJoinLifecycle = async (cid, joinReason, op) => {
17789
+ this.startCorrelation(cid, joinReason);
17790
+ try {
17791
+ return await op();
17792
+ }
17793
+ catch (err) {
17794
+ this.closeCallPairs(cid);
17795
+ throw err;
17796
+ }
17797
+ };
17798
+ this.track = async (cid, stage, op) => {
17799
+ this.beginAttempt(cid, stage);
17800
+ try {
17801
+ const result = await op();
17802
+ this.succeedAttempt(cid, stage);
17803
+ return result;
17804
+ }
17805
+ catch (err) {
17806
+ this.applyStageError(cid, stage, err);
17807
+ throw err;
17808
+ }
17809
+ };
17810
+ this.reportFirstFrame = (cid, trackType, trackId) => {
17811
+ const stage = trackType === TrackType.VIDEO
17812
+ ? 'FirstVideoFrame'
17813
+ : trackType === TrackType.AUDIO
17814
+ ? 'FirstAudioFrame'
17815
+ : undefined;
17816
+ if (!stage)
17817
+ return;
17818
+ const key = `${cid}:${stage}`;
17819
+ if (this.firstFrameReported.has(key))
17820
+ return;
17821
+ this.firstFrameReported.add(key);
17822
+ const pair = {
17823
+ sid: generateUUIDv4(),
17824
+ attempts: 0,
17825
+ startedAt: Date.now(),
17826
+ joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
17827
+ };
17828
+ const resolvedSfuId = this.getSfuId(cid);
17829
+ this.send({
17830
+ ...this.buildCommon(cid, stage, pair),
17831
+ ...this.sessionIdField(cid),
17832
+ ...(resolvedSfuId && { sfu_id: resolvedSfuId }),
17833
+ track_id: trackId,
17834
+ event_type: 'initiated',
17835
+ });
17836
+ };
17837
+ this.captureWsError = (cid, opts) => {
17838
+ const pair = this.wsPairs.get(cid);
17839
+ if (!pair)
17840
+ return;
17841
+ applyError(pair, { reason: opts.reason, code: opts.code });
17842
+ };
17843
+ this.close = (cid) => {
17844
+ this.closeCallPairs(cid);
17845
+ };
17846
+ this.abort = (cid, opts) => {
17847
+ try {
17848
+ const { code, reason } = opts;
17849
+ const stageError = { code, reason };
17850
+ applyErrorIfAbsent(this.coordinatorPairs.get(cid), stageError);
17851
+ applyErrorIfAbsent(this.wsPairs.get(cid), stageError);
17852
+ this.failCoordinator(cid);
17853
+ this.failWs(cid);
17854
+ this.emitPeerConnectionFailure(cid, 'publish', code, reason, 'NOT_CONNECTED');
17855
+ this.emitPeerConnectionFailure(cid, 'subscribe', code, reason, 'NOT_CONNECTED');
17856
+ }
17857
+ catch (err) {
17858
+ this.logger.warn('Failed to report abort', err);
17859
+ }
17860
+ };
17861
+ this.closeCallPairs = (cid) => {
17862
+ if (this.coordinatorPairs.get(cid))
17863
+ this.failCoordinator(cid);
17864
+ if (this.wsPairs.get(cid))
17865
+ this.failWs(cid);
17866
+ for (const role of ['publish', 'subscribe']) {
17867
+ this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt', 'NOT_CONNECTED');
17868
+ }
17869
+ };
17870
+ this.emitJoinInitiated = (cid) => {
17871
+ const joinAttemptId = this.joinAttemptIds.get(cid);
17872
+ if (!joinAttemptId)
17873
+ return;
17874
+ const coordinatorConnectId = this.coordinatorConnectId;
17875
+ const ctx = this.callContexts.get(cid);
17876
+ this.send({
17877
+ user_id: this.streamClient.userID,
17878
+ type: ctx?.callType,
17879
+ id: ctx?.callId,
17880
+ call_cid: cid,
17881
+ stage: 'JoinInitiated',
17882
+ join_attempt_id: joinAttemptId,
17883
+ ...(coordinatorConnectId && {
17884
+ coordinator_connect_id: coordinatorConnectId,
17885
+ }),
17886
+ timestamp: new Date().toISOString(),
17887
+ user_agent: this.streamClient.getUserAgent(),
17888
+ sdk_version: this.streamClient.getSdkVersion(),
17889
+ event_type: 'initiated',
17890
+ });
17891
+ };
17892
+ this.beginAttempt = (cid, stage) => {
17893
+ if (stage === 'CoordinatorJoin')
17894
+ this.beginCoordinatorAttempt(cid);
17895
+ else
17896
+ this.beginWsAttempt(cid);
17897
+ };
17898
+ this.succeedAttempt = (cid, stage) => {
17899
+ if (stage === 'CoordinatorJoin')
17900
+ this.succeedCoordinator(cid);
17901
+ else
17902
+ this.succeedWs(cid);
17903
+ };
17904
+ this.applyStageError = (cid, stage, err) => {
17905
+ const pair = stage === 'CoordinatorJoin'
17906
+ ? this.coordinatorPairs.get(cid)
17907
+ : this.wsPairs.get(cid);
17908
+ applyErrorIfAbsent(pair, stage === 'CoordinatorJoin'
17909
+ ? mapCoordinatorHttpError(err)
17910
+ : mapWsJoinError(err));
17911
+ };
17912
+ this.beginCoordinatorAttempt = (cid) => {
17913
+ let pair = this.coordinatorPairs.get(cid);
17914
+ if (!pair) {
17915
+ pair = {
17916
+ sid: generateUUIDv4(),
17917
+ attempts: 0,
17918
+ startedAt: Date.now(),
17919
+ joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
17920
+ joinReasonSnapshot: this.joinReasons.get(cid),
17921
+ };
17922
+ this.coordinatorPairs.set(cid, pair);
17923
+ this.send({
17924
+ ...this.buildCommon(cid, 'CoordinatorJoin', pair),
17925
+ ...(pair.joinReasonSnapshot && {
17926
+ join_reason: pair.joinReasonSnapshot,
17927
+ }),
17928
+ event_type: 'initiated',
17929
+ });
17930
+ }
17931
+ pair.lastError = undefined;
17932
+ pair.attempts++;
17933
+ };
17934
+ this.succeedCoordinator = (cid) => {
17935
+ const pair = this.coordinatorPairs.get(cid);
17936
+ if (!pair)
17937
+ return;
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: 'success',
17944
+ retry_count_attempt: pair.attempts - 1,
17945
+ elapsed_time: Date.now() - pair.startedAt,
17946
+ });
17947
+ this.coordinatorPairs.delete(cid);
17948
+ };
17949
+ this.failCoordinator = (cid) => {
17950
+ const pair = this.coordinatorPairs.get(cid);
17951
+ if (!pair || !pair.lastError) {
17952
+ this.coordinatorPairs.delete(cid);
17953
+ return;
17954
+ }
17955
+ const { reason, code } = pair.lastError;
17956
+ this.send({
17957
+ ...this.buildCommon(cid, 'CoordinatorJoin', pair),
17958
+ ...this.sessionIdField(cid),
17959
+ ...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
17960
+ event_type: 'completed',
17961
+ outcome: 'failure',
17962
+ retry_count_attempt: pair.attempts - 1,
17963
+ elapsed_time: Date.now() - pair.startedAt,
17964
+ retry_failure_reason: reason,
17965
+ retry_failure_code: code,
17966
+ });
17967
+ this.coordinatorPairs.delete(cid);
17968
+ };
17969
+ this.beginWsAttempt = (cid) => {
17970
+ let pair = this.wsPairs.get(cid);
17971
+ if (!pair) {
17972
+ pair = {
17973
+ sid: generateUUIDv4(),
17974
+ attempts: 0,
17975
+ startedAt: Date.now(),
17976
+ joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
17977
+ };
17978
+ this.wsPairs.set(cid, pair);
17979
+ const sfuId = this.getSfuId(cid);
17980
+ this.send({
17981
+ ...this.buildCommon(cid, 'WSJoin', pair),
17982
+ ...this.sessionIdField(cid),
17983
+ ...(sfuId && { sfu_id: sfuId }),
17984
+ event_type: 'initiated',
17985
+ });
17986
+ }
17987
+ pair.lastError = undefined;
17988
+ pair.attempts++;
17989
+ };
17990
+ this.succeedWs = (cid) => {
17991
+ const pair = this.wsPairs.get(cid);
17992
+ if (!pair)
17993
+ return;
17994
+ const sfuId = this.getSfuId(cid);
17995
+ this.send({
17996
+ ...this.buildCommon(cid, 'WSJoin', pair),
17997
+ ...this.sessionIdField(cid),
17998
+ ...(sfuId && { sfu_id: sfuId }),
17999
+ event_type: 'completed',
18000
+ outcome: 'success',
18001
+ retry_count_attempt: pair.attempts - 1,
18002
+ elapsed_time: Date.now() - pair.startedAt,
18003
+ });
18004
+ this.wsPairs.delete(cid);
18005
+ };
18006
+ this.failWs = (cid) => {
18007
+ const pair = this.wsPairs.get(cid);
18008
+ if (!pair || !pair.lastError) {
18009
+ this.wsPairs.delete(cid);
18010
+ return;
18011
+ }
18012
+ const { reason, code } = pair.lastError;
18013
+ const sfuId = this.getSfuId(cid);
18014
+ this.send({
18015
+ ...this.buildCommon(cid, 'WSJoin', pair),
18016
+ ...this.sessionIdField(cid),
18017
+ event_type: 'completed',
18018
+ outcome: 'failure',
18019
+ retry_count_attempt: pair.attempts - 1,
18020
+ elapsed_time: Date.now() - pair.startedAt,
18021
+ ...(sfuId && { sfu_id: sfuId }),
18022
+ retry_failure_reason: reason,
18023
+ retry_failure_code: code,
18024
+ });
18025
+ this.wsPairs.delete(cid);
18026
+ };
18027
+ this.onPeerConnectionStateChange = (cid, event) => {
18028
+ const role = event.peerType === PeerType.SUBSCRIBER ? 'subscribe' : 'publish';
18029
+ if (event.stateType === 'ice' && event.state === 'failed') {
18030
+ this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed', 'FAILED');
18031
+ return;
18032
+ }
18033
+ if (event.stateType === 'peerConnection' && event.state === 'failed') {
18034
+ this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed', 'CONNECTED');
18035
+ return;
18036
+ }
18037
+ if (event.stateType !== 'peerConnection')
18038
+ return;
18039
+ switch (event.state) {
18040
+ case 'connecting':
18041
+ if (this.peerConnectionPairs.has(pcKey(cid, role)))
18042
+ return;
18043
+ this.openPeerConnectionPair(cid, role);
18044
+ break;
18045
+ case 'connected':
18046
+ this.emitPeerConnectionSuccess(cid, role);
18047
+ this.pcEverConnected.set(pcKey(cid, role), true);
18048
+ break;
18049
+ }
18050
+ };
18051
+ this.openPeerConnectionPair = (cid, role) => {
18052
+ const key = pcKey(cid, role);
18053
+ const pair = {
18054
+ sid: generateUUIDv4(),
18055
+ attempts: 0,
18056
+ startedAt: Date.now(),
18057
+ joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
18058
+ sfuId: this.getSfuId(cid),
18059
+ userSessionId: this.getUserSessionId(cid),
18060
+ wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
18061
+ };
18062
+ this.peerConnectionPairs.set(key, pair);
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: 'initiated',
18073
+ });
18074
+ };
18075
+ this.emitPeerConnectionSuccess = (cid, role) => {
18076
+ const key = pcKey(cid, role);
18077
+ const pair = this.peerConnectionPairs.get(key);
18078
+ if (!pair)
18079
+ return;
18080
+ this.send({
18081
+ ...this.buildCommon(cid, 'PeerConnectionConnect', pair),
18082
+ ...this.sessionIdField(cid),
18083
+ peer_connection: role,
18084
+ was_previously_connected: pair.wasPreviouslyConnected,
18085
+ ...(pair.sfuId && { sfu_id: pair.sfuId }),
18086
+ ...(pair.userSessionId && {
18087
+ user_session_id: pair.userSessionId,
18088
+ }),
18089
+ event_type: 'completed',
18090
+ outcome: 'success',
18091
+ retry_count_attempt: 0,
18092
+ elapsed_time: Date.now() - pair.startedAt,
18093
+ });
18094
+ this.peerConnectionPairs.delete(key);
18095
+ };
18096
+ this.emitPeerConnectionFailure = (cid, role, code, reason, iceState) => {
18097
+ const key = pcKey(cid, role);
18098
+ const pair = this.peerConnectionPairs.get(key);
18099
+ if (!pair)
18100
+ return;
18101
+ this.send({
18102
+ ...this.buildCommon(cid, 'PeerConnectionConnect', pair),
18103
+ ...this.sessionIdField(cid),
18104
+ peer_connection: role,
18105
+ was_previously_connected: pair.wasPreviouslyConnected,
18106
+ ...(pair.userSessionId && {
18107
+ user_session_id: pair.userSessionId,
18108
+ }),
18109
+ ...(pair.sfuId && { sfu_id: pair.sfuId }),
18110
+ event_type: 'completed',
18111
+ outcome: 'failure',
18112
+ retry_count_attempt: 0,
18113
+ elapsed_time: Date.now() - pair.startedAt,
18114
+ ice_state: iceState,
18115
+ retry_failure_reason: reason,
18116
+ retry_failure_code: code,
18117
+ });
18118
+ this.peerConnectionPairs.delete(key);
18119
+ };
18120
+ this.getSfuId = (cid) => this.callContexts.get(cid)?.getSfuId() ?? '';
18121
+ this.getUserSessionId = (cid) => this.callContexts.get(cid)?.getUserSessionId() ?? '';
18122
+ this.sessionIdField = (cid) => {
18123
+ const callSessionId = this.callContexts.get(cid)?.getCallSessionId() ?? '';
18124
+ return callSessionId ? { call_session_id: callSessionId } : {};
18125
+ };
18126
+ this.buildCommon = (cid, stage, pair) => {
18127
+ const ctx = this.callContexts.get(cid);
18128
+ const coordinatorConnectId = this.coordinatorConnectId;
18129
+ return {
18130
+ user_id: this.streamClient.userID,
18131
+ type: ctx?.callType ?? '',
18132
+ id: ctx?.callId ?? '',
18133
+ call_cid: cid,
18134
+ stage,
18135
+ stage_id: pair.sid,
18136
+ ...(pair.joinAttemptIdSnapshot && {
18137
+ join_attempt_id: pair.joinAttemptIdSnapshot,
18138
+ }),
18139
+ ...(coordinatorConnectId && {
18140
+ coordinator_connect_id: coordinatorConnectId,
18141
+ }),
18142
+ timestamp: new Date().toISOString(),
18143
+ user_agent: this.streamClient.getUserAgent(),
18144
+ sdk_version: this.streamClient.getSdkVersion(),
18145
+ };
18146
+ };
18147
+ this.send = (body) => {
18148
+ void this.sendWithRetry(body);
18149
+ };
18150
+ this.sendWithRetry = async (body) => {
18151
+ for (let attempt = 0; attempt < 5; attempt++) {
18152
+ try {
18153
+ await this.streamClient.doAxiosRequest('post', '/call_client_event', { events: [body] }, { publicEndpoint: true });
18154
+ return true;
18155
+ }
18156
+ catch (err) {
18157
+ const status = err?.response
18158
+ ?.status;
18159
+ if (typeof status === 'number' && status >= 400 && status < 500) {
18160
+ this.logger.debug(`Client event rejected (${status}), not retrying`, body.stage, body.event_type);
18161
+ return false;
18162
+ }
18163
+ if (attempt === 4) {
18164
+ this.logger.debug('Client event delivery failed after retries', body.stage, body.event_type, err);
18165
+ return false;
18166
+ }
18167
+ await sleep(retryInterval(attempt));
18168
+ }
18169
+ }
18170
+ return false;
18171
+ };
18172
+ this.streamClient = options.streamClient;
18173
+ }
18174
+ }
18175
+ const readPermissionStatus = (permission) => {
18176
+ const state = getCurrentValue(permission.asStateObservable());
18177
+ switch (state) {
18178
+ case 'granted':
18179
+ return 'GRANTED';
18180
+ case 'denied':
18181
+ return 'FAILED';
18182
+ case 'prompting':
18183
+ return 'INITIATED';
18184
+ case 'prompt':
18185
+ default:
18186
+ return 'NOT_INITIATED';
18187
+ }
18188
+ };
18189
+ const errorMessage = (err) => err instanceof Error ? err.message : String(err);
18190
+ const applyError = (pair, next) => {
18191
+ if (!pair)
18192
+ return;
18193
+ pair.lastError = next;
18194
+ };
18195
+ const applyErrorIfAbsent = (pair, next) => {
18196
+ if (!pair || pair.lastError)
18197
+ return;
18198
+ pair.lastError = next;
18199
+ };
18200
+ const mapCoordinatorHttpError = (err) => {
18201
+ if (err instanceof ErrorFromResponse) {
18202
+ return {
18203
+ reason: err.message,
18204
+ code: err.code != null ? String(err.code) : 'SERVER_ERROR',
18205
+ };
18206
+ }
18207
+ return { reason: errorMessage(err), code: 'SERVER_ERROR' };
18208
+ };
18209
+ const mapCoordinatorWsError = (err) => {
18210
+ if (err instanceof ErrorFromResponse) {
18211
+ return {
18212
+ reason: err.message,
18213
+ code: err.code != null ? String(err.code) : 'SERVER_ERROR',
18214
+ };
18215
+ }
18216
+ if (err instanceof Error) {
18217
+ try {
18218
+ const parsed = JSON.parse(err.message);
18219
+ if (typeof parsed.isWSFailure === 'boolean') {
18220
+ return {
18221
+ reason: parsed.message || err.message,
18222
+ code: !parsed.isWSFailure && parsed.code
18223
+ ? String(parsed.code)
18224
+ : 'SERVER_ERROR',
18225
+ };
18226
+ }
18227
+ }
18228
+ catch { }
18229
+ }
18230
+ return { reason: errorMessage(err), code: 'SERVER_ERROR' };
18231
+ };
18232
+ const mapWsJoinError = (err) => {
18233
+ if (err instanceof SfuJoinError) {
18234
+ const sfuError = err.errorEvent.error;
18235
+ return {
18236
+ reason: sfuError?.message || err.message,
18237
+ code: (sfuError && ErrorCode[sfuError.code]) || 'SFU_ERROR',
18238
+ };
18239
+ }
18240
+ const reason = errorMessage(err);
18241
+ if (err instanceof SfuTimeoutError) {
18242
+ return { reason, code: 'REQUEST_TIMEOUT' };
18243
+ }
18244
+ return { reason, code: 'SFU_ERROR' };
18245
+ };
18246
+
17561
18247
  /**
17562
18248
  * A `StreamVideoClient` instance lets you communicate with our API, and authenticate users.
17563
18249
  */
@@ -17621,6 +18307,7 @@ class StreamVideoClient {
17621
18307
  }
17622
18308
  call = new Call({
17623
18309
  streamClient: this.streamClient,
18310
+ clientEventReporter: this.clientEventReporter,
17624
18311
  type: e.call.type,
17625
18312
  id: e.call.id,
17626
18313
  members: e.members,
@@ -17690,6 +18377,8 @@ class StreamVideoClient {
17690
18377
  user.id = '!anon';
17691
18378
  return this.connectAnonymousUser(user, tokenOrProvider);
17692
18379
  }
18380
+ const reporter = this.clientEventReporter;
18381
+ reporter.startCoordinatorConnection(user.id);
17693
18382
  const connectUserResponse = await withoutConcurrency(this.connectionConcurrencyTag, async () => {
17694
18383
  const client = this.streamClient;
17695
18384
  const { onConnectUserError, persistUserOnConnectionFailure } = client.options;
@@ -17699,14 +18388,15 @@ class StreamVideoClient {
17699
18388
  for (let attempt = 0; attempt < maxConnectUserRetries; attempt++) {
17700
18389
  try {
17701
18390
  this.logger.trace(`Connecting user (${attempt})`, user);
17702
- return user.type === 'guest'
17703
- ? await client.connectGuestUser(user)
17704
- : await client.connectUser(user, tokenOrProvider);
18391
+ return await reporter.trackCoordinatorWs(() => user.type === 'guest'
18392
+ ? client.connectGuestUser(user)
18393
+ : client.connectUser(user, tokenOrProvider));
17705
18394
  }
17706
18395
  catch (err) {
17707
18396
  this.logger.warn(`Failed to connect a user (${attempt})`, err);
17708
18397
  errorQueue.push(err);
17709
18398
  if (attempt === maxConnectUserRetries - 1) {
18399
+ reporter.closeCoordinatorWs();
17710
18400
  onConnectUserError?.(err, errorQueue);
17711
18401
  throw err;
17712
18402
  }
@@ -17784,6 +18474,7 @@ class StreamVideoClient {
17784
18474
  return (call ??
17785
18475
  new Call({
17786
18476
  streamClient: this.streamClient,
18477
+ clientEventReporter: this.clientEventReporter,
17787
18478
  id: id,
17788
18479
  type: type,
17789
18480
  clientStore: this.writeableStateStore,
@@ -17808,6 +18499,7 @@ class StreamVideoClient {
17808
18499
  for (const c of response.calls) {
17809
18500
  const call = new Call({
17810
18501
  streamClient: this.streamClient,
18502
+ clientEventReporter: this.clientEventReporter,
17811
18503
  id: c.call.id,
17812
18504
  type: c.call.type,
17813
18505
  members: c.members,
@@ -17915,6 +18607,7 @@ class StreamVideoClient {
17915
18607
  const [callType, callId] = call_cid.split(':');
17916
18608
  call = new Call({
17917
18609
  streamClient: this.streamClient,
18610
+ clientEventReporter: this.clientEventReporter,
17918
18611
  type: callType,
17919
18612
  id: callId,
17920
18613
  clientStore: this.writeableStateStore,
@@ -17955,6 +18648,9 @@ class StreamVideoClient {
17955
18648
  this.logger = videoLoggerSystem.getLogger('client');
17956
18649
  this.rejectCallWhenBusy = clientOptions?.rejectCallWhenBusy ?? false;
17957
18650
  this.streamClient = createCoordinatorClient(apiKey, clientOptions);
18651
+ this.clientEventReporter = new ClientEventReporter({
18652
+ streamClient: this.streamClient,
18653
+ });
17958
18654
  this.writeableStateStore = new StreamVideoWriteableStateStore();
17959
18655
  this.readOnlyStateStore = new StreamVideoReadOnlyStateStore(this.writeableStateStore);
17960
18656
  if (typeof apiKeyOrArgs !== 'string' && apiKeyOrArgs.user) {
@@ -18017,5 +18713,5 @@ const humanize = (n) => {
18017
18713
  return String(n);
18018
18714
  };
18019
18715
 
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 };
18716
+ 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
18717
  //# sourceMappingURL=index.es.js.map