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