@stream-io/video-client 1.59.0 → 1.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.browser.es.js +544 -111
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +544 -110
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +544 -111
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +23 -3
  9. package/dist/src/coordinator/connection/types.d.ts +16 -0
  10. package/dist/src/coordinator/connection/utils.d.ts +10 -0
  11. package/dist/src/events/call.d.ts +0 -11
  12. package/dist/src/gen/coordinator/index.d.ts +73 -0
  13. package/dist/src/reporting/ClientEventReporter.d.ts +11 -1
  14. package/dist/src/ringing/RingStatePoller.d.ts +46 -0
  15. package/dist/src/ringing/RingTimeout.d.ts +27 -0
  16. package/dist/src/ringing/index.d.ts +4 -0
  17. package/dist/src/ringing/reconcileRingState.d.ts +18 -0
  18. package/dist/src/ringing/resolveOwnRingOutcome.d.ts +32 -0
  19. package/dist/src/sorting/participants.d.ts +13 -0
  20. package/dist/src/stats/utils.d.ts +6 -0
  21. package/dist/src/store/CallState.d.ts +10 -1
  22. package/package.json +1 -1
  23. package/src/Call.ts +93 -61
  24. package/src/StreamSfuClient.ts +6 -2
  25. package/src/__tests__/Call.autodrop.test.ts +116 -64
  26. package/src/__tests__/Call.getRingState.test.ts +87 -0
  27. package/src/__tests__/Call.lifecycle.test.ts +23 -0
  28. package/src/__tests__/Call.ringSettled.test.ts +71 -0
  29. package/src/__tests__/StreamSfuClient.test.ts +48 -0
  30. package/src/__tests__/StreamVideoClient.ringing.test.ts +32 -20
  31. package/src/coordinator/connection/__tests__/eventListenerIsolation.test.ts +115 -0
  32. package/src/coordinator/connection/client.ts +3 -2
  33. package/src/coordinator/connection/types.ts +19 -0
  34. package/src/coordinator/connection/utils.ts +28 -0
  35. package/src/events/__tests__/call.test.ts +2 -217
  36. package/src/events/call.ts +1 -73
  37. package/src/events/callEventHandlers.ts +12 -23
  38. package/src/gen/coordinator/index.ts +67 -0
  39. package/src/reporting/ClientEventReporter.ts +30 -4
  40. package/src/reporting/__tests__/ClientEventReporter.test.ts +184 -7
  41. package/src/ringing/RingStatePoller.ts +177 -0
  42. package/src/ringing/RingTimeout.ts +90 -0
  43. package/src/ringing/__tests__/RingStatePoller.test.ts +462 -0
  44. package/src/ringing/__tests__/RingTimeout.test.ts +199 -0
  45. package/src/ringing/__tests__/reconcileRingState.test.ts +330 -0
  46. package/src/ringing/__tests__/resolveOwnRingOutcome.test.ts +103 -0
  47. package/src/ringing/index.ts +4 -0
  48. package/src/ringing/reconcileRingState.ts +108 -0
  49. package/src/ringing/resolveOwnRingOutcome.ts +58 -0
  50. package/src/sorting/__tests__/presets.test.ts +188 -3
  51. package/src/sorting/__tests__/sorting.test.ts +22 -1
  52. package/src/sorting/participants.ts +20 -0
  53. package/src/sorting/presets.ts +4 -0
  54. package/src/stats/__tests__/utils.test.ts +33 -0
  55. package/src/stats/utils.ts +15 -0
  56. package/src/store/CallState.ts +25 -0
  57. package/src/store/__tests__/CallState.test.ts +61 -2
@@ -4411,6 +4411,28 @@ function isErrorResponse(res) {
4411
4411
  function isCloseEvent(res) {
4412
4412
  return res.code !== undefined;
4413
4413
  }
4414
+ /**
4415
+ * Invokes a single event listener in isolation, so that a faulty listener
4416
+ * cannot abort the dispatch loop nor the bookkeeping that follows it.
4417
+ *
4418
+ * @param listener the listener to invoke.
4419
+ * @param event the event to pass to the listener.
4420
+ * @param logger the logger to report listener failures with.
4421
+ */
4422
+ const invokeEventListener = (listener, event, logger) => {
4423
+ try {
4424
+ const result = listener(event);
4425
+ if (result && typeof result.then === 'function') {
4426
+ Promise.resolve(result).catch((error) => report(logger, error, event));
4427
+ }
4428
+ }
4429
+ catch (error) {
4430
+ report(logger, error, event);
4431
+ }
4432
+ };
4433
+ const report = (logger, error, event) => {
4434
+ logger.error('Unhandled error in event listener', error, event);
4435
+ };
4414
4436
 
4415
4437
  /**
4416
4438
  * Checks whether we are using React Native
@@ -5059,7 +5081,7 @@ const toIceCandidate = (iceTrickle) => {
5059
5081
  }
5060
5082
  };
5061
5083
 
5062
- const version = "1.59.0";
5084
+ const version = "1.60.0";
5063
5085
  const [major, minor, patch] = version.split('.');
5064
5086
  let sdkInfo = {
5065
5087
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -5908,6 +5930,27 @@ const publishingAudio = (a, b) => {
5908
5930
  return 1;
5909
5931
  return 0;
5910
5932
  };
5933
+ /**
5934
+ * A comparator which prioritizes participants who are publishing any track
5935
+ * over participants who are not publishing at all.
5936
+ *
5937
+ * Unlike `publishingVideo` and `publishingAudio`, this comparator does not
5938
+ * distinguish between track types. Muting the microphone or turning off the
5939
+ * camera does not change a participant's rank as long as they still publish
5940
+ * something, which keeps the ordering stable across ordinary device toggles.
5941
+ *
5942
+ * @param a the first participant.
5943
+ * @param b the second participant.
5944
+ */
5945
+ const publishing = (a, b) => {
5946
+ const hasA = a.publishedTracks.length > 0;
5947
+ const hasB = b.publishedTracks.length > 0;
5948
+ if (hasA && !hasB)
5949
+ return -1;
5950
+ if (!hasA && hasB)
5951
+ return 1;
5952
+ return 0;
5953
+ };
5911
5954
  /**
5912
5955
  * A comparator which prioritizes participants who are pinned.
5913
5956
  *
@@ -6018,11 +6061,11 @@ const withVideoIngressSource = withParticipantSource(ParticipantSource.RTMP, Par
6018
6061
  /**
6019
6062
  * The default sorting preset.
6020
6063
  */
6021
- const defaultSortPreset = combineComparators(screenSharing, pinned, ifInvisibleBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), publishingVideo, publishingAudio)));
6064
+ const defaultSortPreset = combineComparators(screenSharing, pinned, publishing, ifInvisibleBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), publishingVideo, publishingAudio)));
6022
6065
  /**
6023
6066
  * The sorting preset for speaker layout.
6024
6067
  */
6025
- const speakerLayoutSortPreset = combineComparators(screenSharing, pinned, dominantSpeaker, ifInvisibleBy(combineComparators(speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)));
6068
+ const speakerLayoutSortPreset = combineComparators(screenSharing, pinned, dominantSpeaker, publishing, ifInvisibleBy(combineComparators(speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)));
6026
6069
  /**
6027
6070
  * The sorting preset for layouts that don't render all participants but
6028
6071
  * instead, render them in pages.
@@ -6031,7 +6074,7 @@ const paginatedLayoutSortPreset = combineComparators(pinned, ifInvisibleOrUnknow
6031
6074
  /**
6032
6075
  * The sorting preset for livestreams and audio rooms.
6033
6076
  */
6034
- const livestreamOrAudioRoomSortPreset = combineComparators(ifInvisibleOrUnknownBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)), role('admin', 'host', 'speaker'));
6077
+ const livestreamOrAudioRoomSortPreset = combineComparators(publishing, ifInvisibleOrUnknownBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)), role('admin', 'host', 'speaker'));
6035
6078
 
6036
6079
  /**
6037
6080
  * Returns the default egress object - when no egress data is available.
@@ -7071,6 +7114,30 @@ class CallState {
7071
7114
  this.setCurrentValue(this.captioningSubject, call.captioning);
7072
7115
  this.setCurrentValue(this.thumbnailsSubject, call.thumbnails);
7073
7116
  };
7117
+ /**
7118
+ * Merges a polled ring state into the current session. Only the ring fields
7119
+ * are touched: the endpoint does not return the session roster.
7120
+ *
7121
+ * @internal
7122
+ *
7123
+ * @param ringState the ring state as returned by the coordinator.
7124
+ */
7125
+ updateFromRingState = (ringState) => {
7126
+ this.setCurrentValue(this.sessionSubject, (session) => {
7127
+ if (!session || session.id !== ringState.session_id)
7128
+ return session;
7129
+ return {
7130
+ ...session,
7131
+ accepted_by: ringState.accepted_by,
7132
+ rejected_by: ringState.rejected_by,
7133
+ missed_by: ringState.missed_by,
7134
+ ended_at: ringState.session_ended_at ?? session.ended_at,
7135
+ };
7136
+ });
7137
+ if (ringState.call_ended_at) {
7138
+ this.setEndedAt(new Date(ringState.call_ended_at));
7139
+ }
7140
+ };
7074
7141
  /**
7075
7142
  * Updates the call state with the data received from the SFU server.
7076
7143
  *
@@ -7382,6 +7449,19 @@ const getSdkName = (sdk) => {
7382
7449
  const getSdkVersion = (sdk) => {
7383
7450
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
7384
7451
  };
7452
+ /**
7453
+ * Returns the identifier the SFU receives in the `X-Stream-Client` header,
7454
+ * e.g. `stream-video-react-v1.43.1`. Matches the format the coordinator
7455
+ * already gets through the client's user agent.
7456
+ */
7457
+ const getStreamClientId = (sdk) => {
7458
+ const sdkPlatforms = {
7459
+ [SdkType.REACT]: 'react',
7460
+ [SdkType.REACT_NATIVE]: 'react-native',
7461
+ };
7462
+ const platform = (sdk && sdkPlatforms[sdk.type]) || 'js';
7463
+ return `stream-video-${platform}-v${getSdkVersion(sdk)}`;
7464
+ };
7385
7465
 
7386
7466
  /**
7387
7467
  * Creates a new StatsReporter instance that collects metrics about the ongoing call and reports them to the state store
@@ -10263,7 +10343,10 @@ class StreamSfuClient {
10263
10343
  this.rpc = createSignalClient({
10264
10344
  baseUrl: server.url,
10265
10345
  interceptors: [
10266
- withHeaders({ Authorization: `Bearer ${token}` }),
10346
+ withHeaders({
10347
+ Authorization: `Bearer ${token}`,
10348
+ 'X-Stream-Client': getStreamClientId(getSdkInfo()),
10349
+ }),
10267
10350
  this.tracer && withRequestTracer(this.tracer.trace),
10268
10351
  this.logger.getLogLevel() === 'trace' && withRequestLogger(this.logger),
10269
10352
  withTimeout(rpcRequestTimeout, this.tracer?.trace),
@@ -10600,64 +10683,370 @@ class StreamSfuClient {
10600
10683
  }
10601
10684
 
10602
10685
  /**
10603
- * Event handler that watched the delivery of `call.accepted`.
10604
- * Once the event is received, the call is joined.
10605
- */
10606
- const watchCallAccepted = (call) => {
10607
- return async function onCallAccepted(event) {
10608
- // We want to discard the event if it's from the current user
10609
- if (event.user.id === call.currentUserId)
10610
- return;
10611
- const { state } = call;
10612
- if (event.call.created_by.id === call.currentUserId &&
10613
- state.callingState === CallingState.RINGING) {
10614
- await call.join();
10686
+ * Decides what a ringing call should do next, based on the current call state.
10687
+ *
10688
+ * The `call.accepted`, `call.rejected` and `call.missed` handlers and the ring
10689
+ * state poller both run this. They differ only in how the state got there: the
10690
+ * handlers rely on `CallState.updateFromEvent`, which runs before them, and the
10691
+ * poller applies the polled ring state itself.
10692
+ *
10693
+ * @param call the call to reconcile.
10694
+ * @param joinSource which of the two triggered this run, reported on the
10695
+ * caller's join: `ring-ws` for the event handlers, `ring-poll-api` for the
10696
+ * poller.
10697
+ * @returns whether the ring reached a terminal state. A failed join is not
10698
+ * terminal: the caller should keep trying while the ring is open.
10699
+ */
10700
+ const reconcileRingState = async (call, joinSource) => {
10701
+ if (call.state.callingState !== CallingState.RINGING)
10702
+ return true;
10703
+ return call.isCreatedByMe
10704
+ ? reconcileAsCaller(call, joinSource)
10705
+ : reconcileAsCallee(call);
10706
+ };
10707
+ const reconcileAsCaller = async (call, joinSource) => {
10708
+ const { session, members, endedAt } = call.state;
10709
+ const currentUserId = call.currentUserId;
10710
+ // checked before `accepted_by`: an ended session cannot be joined
10711
+ if (endedAt || session?.ended_at) {
10712
+ call.logger.info('ring: the call has ended, leaving');
10713
+ // `leave` reports the remote end to callingx off `reason: 'ended'`
10714
+ return leave(call, {
10715
+ reject: false,
10716
+ reason: 'ended',
10717
+ message: 'ring: call ended',
10718
+ });
10719
+ }
10720
+ const acceptedBy = session?.accepted_by ?? {};
10721
+ if (Object.keys(acceptedBy).some((userId) => userId !== currentUserId)) {
10722
+ call.logger.info('ring: the call was accepted, joining');
10723
+ try {
10724
+ await call.join({ joinSource });
10615
10725
  }
10616
- };
10726
+ catch (err) {
10727
+ // `doJoin` restores the ringing state when a join fails, so the ring is
10728
+ // still open. Report it unsettled and let the next poll retry.
10729
+ call.logger.error('Failed to join an accepted call', err);
10730
+ return false;
10731
+ }
10732
+ return true;
10733
+ }
10734
+ const otherMembers = members
10735
+ .filter((member) => member.user_id !== currentUserId)
10736
+ .map((member) => member.user_id);
10737
+ if (otherMembers.length === 0)
10738
+ return false;
10739
+ const rejectedBy = session?.rejected_by ?? {};
10740
+ if (otherMembers.every((userId) => rejectedBy[userId])) {
10741
+ call.logger.info('ring: everyone rejected, leaving');
10742
+ return leave(call, {
10743
+ reject: true,
10744
+ reason: 'cancel',
10745
+ message: 'ring: everyone rejected',
10746
+ });
10747
+ }
10748
+ return false;
10749
+ };
10750
+ // the current user's own accept or reject, on this or another device, is
10751
+ // handled by `resolveOwnRingOutcome`, and `call.ended` by `watchCallEnded`.
10752
+ const reconcileAsCallee = async (call) => {
10753
+ const createdById = call.state.createdBy?.id;
10754
+ const rejectedBy = call.state.session?.rejected_by ?? {};
10755
+ if (createdById && rejectedBy[createdById]) {
10756
+ call.logger.info('ring: the caller cancelled, leaving');
10757
+ return leave(call, {
10758
+ reason: 'ended',
10759
+ message: 'ring: creator rejected',
10760
+ });
10761
+ }
10762
+ return false;
10763
+ };
10764
+ // `false` when the call could not be left, so the ring stays open for a retry.
10765
+ const leave = async (call, options) => {
10766
+ try {
10767
+ await call.leave(options);
10768
+ return true;
10769
+ }
10770
+ catch (err) {
10771
+ call.logger.error('Failed to leave a ringing call', err);
10772
+ return false;
10773
+ }
10617
10774
  };
10775
+
10776
+ // `doJoin` restores the previous calling state when a join fails, so JOINING can
10777
+ // go back to RINGING. Only a state the ring cannot return from ends the poller.
10778
+ const ringIsOver = (callingState) => callingState !== CallingState.RINGING &&
10779
+ callingState !== CallingState.JOINING;
10618
10780
  /**
10619
- * Event handler that watches delivery of `call.rejected` Websocket event.
10620
- * Once the event is received, the call is left.
10781
+ * Polls the coordinator for the outcome of a ring the current user started.
10782
+ *
10783
+ * `call.accepted`, `call.rejected` and `call.missed` are delivered best-effort,
10784
+ * with no store-and-forward, so a caller that drops one is left on a ringing
10785
+ * screen while the callee is already in the call. After a quiet period this
10786
+ * reads the ring state until a terminal outcome or the end of the ring window.
10621
10787
  */
10622
- const watchCallRejected = (call) => {
10623
- return async function onCallRejected(event) {
10624
- // We want to discard the event if it's from the current user
10625
- if (event.user.id === call.currentUserId)
10788
+ class RingStatePoller {
10789
+ logger = videoLoggerSystem.getLogger('RingStatePoller');
10790
+ call;
10791
+ startAfterMs;
10792
+ intervalMs;
10793
+ sessionId;
10794
+ deadlineAt = 0;
10795
+ idleTimeoutId;
10796
+ intervalId;
10797
+ stopped = false;
10798
+ wasPolling = false;
10799
+ inFlight = false;
10800
+ unsubscribe = [];
10801
+ constructor(call, options = {}) {
10802
+ this.call = call;
10803
+ this.startAfterMs = options.startAfterMs ?? 15_000;
10804
+ this.intervalMs = options.intervalMs ?? 5_000;
10805
+ }
10806
+ /**
10807
+ * Starts polling. Does nothing when the call has no session yet.
10808
+ */
10809
+ start = () => {
10810
+ if (this.stopped || this.sessionId)
10811
+ return;
10812
+ if (this.call.state.callingState !== CallingState.RINGING)
10626
10813
  return;
10627
- const { call: eventCall } = event;
10628
- const { session: callSession } = eventCall;
10629
- if (!callSession) {
10630
- call.logger.warn('No call session provided. Ignoring call.rejected event.', event);
10814
+ // captured once: `call.ended` clears the call's current session
10815
+ const sessionId = this.call.state.session?.id;
10816
+ if (!sessionId) {
10817
+ this.logger.warn('the call has no session');
10631
10818
  return;
10632
10819
  }
10633
- const rejectedBy = callSession.rejected_by;
10634
- const { members, callingState } = call.state;
10635
- if (callingState !== CallingState.RINGING) {
10636
- call.logger.info('Call is not in ringing mode (it is either accepted or rejected already). Ignoring call.rejected event.', event);
10820
+ this.sessionId = sessionId;
10821
+ const ring = this.call.state.settings?.ring;
10822
+ const maxDurationMs = ring?.auto_cancel_timeout_ms || ring?.missed_call_timeout_ms || 30_000;
10823
+ this.deadlineAt = Date.now() + maxDurationMs;
10824
+ // an incoming event proves the socket is alive, so the quiet period starts
10825
+ // over: in a group ring a single rejection does not settle the ring.
10826
+ this.unsubscribe.push(this.call.on('call.accepted', () => this.armIdleWindow()), this.call.on('call.rejected', () => this.armIdleWindow()), this.call.on('call.missed', () => this.armIdleWindow()),
10827
+ // the ring is over, whichever way it went. Joining an accepted call
10828
+ // never goes through `leave`, so this is the only signal for it.
10829
+ createSubscription(this.call.state.callingState$, (callingState) => {
10830
+ if (ringIsOver(callingState))
10831
+ this.stop();
10832
+ }));
10833
+ this.armIdleWindow();
10834
+ };
10835
+ /**
10836
+ * Pauses polling, keeping the captured session, the deadline and the event
10837
+ * subscriptions. Resuming does not extend the ring window.
10838
+ */
10839
+ pause = () => {
10840
+ if (this.stopped)
10841
+ return;
10842
+ this.wasPolling = this.intervalId !== undefined;
10843
+ const timers = getTimers();
10844
+ timers.clearTimeout(this.idleTimeoutId);
10845
+ timers.clearInterval(this.intervalId);
10846
+ this.idleTimeoutId = undefined;
10847
+ this.intervalId = undefined;
10848
+ };
10849
+ /**
10850
+ * Resumes a paused poller. Goes straight back to polling if it was already
10851
+ * past the quiet period, so a pause does not buy the ring another one.
10852
+ */
10853
+ resume = () => {
10854
+ if (this.stopped || !this.sessionId)
10855
+ return;
10856
+ if (this.idleTimeoutId !== undefined || this.intervalId !== undefined) {
10637
10857
  return;
10638
10858
  }
10639
- if (call.isCreatedByMe) {
10640
- const everyoneElseRejected = members
10641
- .filter((m) => m.user_id !== call.currentUserId)
10642
- .every((m) => rejectedBy[m.user_id]);
10643
- if (everyoneElseRejected) {
10644
- call.logger.info('everyone rejected, leaving the call');
10645
- await call.leave({
10646
- reject: true,
10647
- reason: 'cancel',
10648
- message: 'ring: everyone rejected',
10649
- });
10650
- }
10859
+ if (!this.wasPolling) {
10860
+ this.armIdleWindow();
10861
+ return;
10651
10862
  }
10652
- else {
10653
- if (rejectedBy[eventCall.created_by.id]) {
10654
- call.logger.info('call creator rejected, leaving call');
10655
- globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
10656
- await call.leave({ message: 'ring: creator rejected' });
10863
+ this.intervalId = getTimers().setInterval(this.runTick, this.intervalMs);
10864
+ this.runTick();
10865
+ };
10866
+ /**
10867
+ * Stops polling. The poller cannot be restarted.
10868
+ */
10869
+ stop = () => {
10870
+ if (this.stopped)
10871
+ return;
10872
+ this.stopped = true;
10873
+ const timers = getTimers();
10874
+ timers.clearTimeout(this.idleTimeoutId);
10875
+ timers.clearInterval(this.intervalId);
10876
+ this.idleTimeoutId = undefined;
10877
+ this.intervalId = undefined;
10878
+ this.unsubscribe.forEach((off) => off());
10879
+ this.unsubscribe = [];
10880
+ };
10881
+ armIdleWindow = () => {
10882
+ if (this.stopped)
10883
+ return;
10884
+ const timers = getTimers();
10885
+ timers.clearTimeout(this.idleTimeoutId);
10886
+ timers.clearInterval(this.intervalId);
10887
+ this.intervalId = undefined;
10888
+ this.idleTimeoutId = timers.setTimeout(() => {
10889
+ this.idleTimeoutId = undefined;
10890
+ if (this.stopped)
10891
+ return;
10892
+ this.intervalId = timers.setInterval(this.runTick, this.intervalMs);
10893
+ this.runTick();
10894
+ }, this.startAfterMs);
10895
+ };
10896
+ runTick = () => {
10897
+ this.tick().catch((err) => {
10898
+ this.logger.warn('Failed to poll the ring state', err);
10899
+ });
10900
+ };
10901
+ tick = async () => {
10902
+ if (this.stopped || this.inFlight || !this.sessionId)
10903
+ return;
10904
+ const { callingState } = this.call.state;
10905
+ if (ringIsOver(callingState) || Date.now() >= this.deadlineAt) {
10906
+ this.stop();
10907
+ return;
10908
+ }
10909
+ // mid-join: nothing to reconcile until we know whether it succeeded
10910
+ if (callingState !== CallingState.RINGING)
10911
+ return;
10912
+ this.inFlight = true;
10913
+ try {
10914
+ const ringState = await this.call.getRingState(this.sessionId);
10915
+ if (this.stopped)
10916
+ return;
10917
+ this.call.state.updateFromRingState(ringState);
10918
+ if (await reconcileRingState(this.call, 'ring-poll-api'))
10919
+ this.stop();
10920
+ }
10921
+ catch (err) {
10922
+ // a missing session, or one of another call, will never resolve
10923
+ const status = err instanceof ErrorFromResponse ? err.status : undefined;
10924
+ if (status === 400 || status === 404) {
10925
+ this.logger.warn('Stopped polling the ring state', err);
10926
+ this.stop();
10657
10927
  }
10928
+ else {
10929
+ this.logger.debug('Failed to poll the ring state', err);
10930
+ }
10931
+ }
10932
+ finally {
10933
+ this.inFlight = false;
10658
10934
  }
10659
10935
  };
10936
+ }
10937
+
10938
+ /**
10939
+ * Drops a call that has been ringing for too long: the caller cancels after
10940
+ * `auto_cancel_timeout_ms`, a callee after `incoming_call_timeout_ms`.
10941
+ *
10942
+ * Either timeout being `0` means the call rings until something else settles it.
10943
+ */
10944
+ class RingTimeout {
10945
+ call;
10946
+ timeoutId;
10947
+ deadlineAt;
10948
+ stopped = false;
10949
+ constructor(call) {
10950
+ this.call = call;
10951
+ }
10952
+ /**
10953
+ * Schedules an auto-drop timeout based on the call settings.
10954
+ * Applicable only for ringing calls.
10955
+ */
10956
+ start = () => {
10957
+ if (this.stopped || this.timeoutId !== undefined)
10958
+ return;
10959
+ // ignore if the call is not ringing
10960
+ if (this.call.state.callingState !== CallingState.RINGING)
10961
+ return;
10962
+ const ring = this.call.state.settings?.ring;
10963
+ if (!ring)
10964
+ return;
10965
+ const isCaller = this.call.isCreatedByMe;
10966
+ const timeoutMs = isCaller
10967
+ ? ring.auto_cancel_timeout_ms
10968
+ : ring.incoming_call_timeout_ms;
10969
+ // 0 means no auto-drop
10970
+ if (timeoutMs <= 0)
10971
+ return;
10972
+ const now = Date.now();
10973
+ this.deadlineAt ??= now + timeoutMs;
10974
+ const delayMs = Math.max(0, this.deadlineAt - now);
10975
+ const timers = getTimers();
10976
+ this.timeoutId = timers.setTimeout(() => {
10977
+ this.timeoutId = undefined;
10978
+ // A failed timeout-triggered leave can arm a fresh retry window.
10979
+ this.deadlineAt = undefined;
10980
+ if (this.stopped)
10981
+ return;
10982
+ // the call might have stopped ringing by this point, e.g. it was already
10983
+ // accepted and joined
10984
+ if (this.call.state.callingState !== CallingState.RINGING)
10985
+ return;
10986
+ this.call
10987
+ .leave({
10988
+ reject: true,
10989
+ reason: 'timeout',
10990
+ message: `ringing timeout - ${isCaller
10991
+ ? 'no one accepted'
10992
+ : `user didn't interact with incoming call screen`}`,
10993
+ })
10994
+ .catch((err) => {
10995
+ videoLoggerSystem
10996
+ .getLogger('RingTimeout')
10997
+ .error('Failed to drop the call', err);
10998
+ });
10999
+ }, delayMs);
11000
+ };
11001
+ /**
11002
+ * Pauses the timeout while preserving its original deadline.
11003
+ */
11004
+ pause = () => {
11005
+ if (this.stopped || this.timeoutId === undefined)
11006
+ return;
11007
+ getTimers().clearTimeout(this.timeoutId);
11008
+ this.timeoutId = undefined;
11009
+ };
11010
+ /**
11011
+ * Cancels a scheduled auto-drop timeout. It cannot be armed again.
11012
+ */
11013
+ stop = () => {
11014
+ if (this.stopped)
11015
+ return;
11016
+ this.stopped = true;
11017
+ getTimers().clearTimeout(this.timeoutId);
11018
+ this.timeoutId = undefined;
11019
+ this.deadlineAt = undefined;
11020
+ };
11021
+ }
11022
+
11023
+ /**
11024
+ * Decides what a ringing call should do about the current user's own accept or
11025
+ * reject, which may have happened on another device.
11026
+ *
11027
+ * Accepting on *this* device also lands in `accepted_by`, so an acceptance only
11028
+ * means another device took the call when this one is still ringing.
11029
+ *
11030
+ * Only meaningful for a ringing call; the caller checks that.
11031
+ */
11032
+ const resolveOwnRingOutcome = ({ session, currentUserId, callingState, }) => {
11033
+ if (!currentUserId)
11034
+ return { settledByMe: false };
11035
+ const acceptedByMe = Boolean(session?.accepted_by[currentUserId]);
11036
+ const rejectedByMe = Boolean(session?.rejected_by[currentUserId]);
11037
+ if (!acceptedByMe && !rejectedByMe)
11038
+ return { settledByMe: false };
11039
+ const answeredElsewhere = acceptedByMe && callingState === CallingState.RINGING;
11040
+ return {
11041
+ settledByMe: true,
11042
+ leaveReason: answeredElsewhere
11043
+ ? 'answeredElsewhere'
11044
+ : rejectedByMe
11045
+ ? 'rejected'
11046
+ : undefined,
11047
+ };
10660
11048
  };
11049
+
10661
11050
  /**
10662
11051
  * Event handler that watches the delivery of `call.ended` Websocket event.
10663
11052
  */
@@ -11058,14 +11447,18 @@ const registerEventHandlers = (call, dispatcher) => {
11058
11447
  * @param call the call to register event handlers for.
11059
11448
  */
11060
11449
  const registerRingingCallEventHandlers = (call) => {
11061
- const coordinatorRingEvents = {
11062
- 'call.accepted': watchCallAccepted(call),
11063
- 'call.rejected': watchCallRejected(call),
11450
+ const reconcile = () => {
11451
+ reconcileRingState(call, 'ring-ws').catch((err) => {
11452
+ call.logger.error('Failed to reconcile the ring state', err);
11453
+ });
11064
11454
  };
11065
- const eventHandlers = Object.keys(coordinatorRingEvents).map((event) => {
11066
- const eventName = event;
11067
- return call.on(eventName, coordinatorRingEvents[eventName]);
11068
- });
11455
+ // each event needs its own closure. `call.missed` is deliberately absent:
11456
+ // nothing in the reconciler acts on `missed_by`, so the auto-drop owns the
11457
+ // "nobody answered" case until the server-owned ring timeout lands.
11458
+ const eventHandlers = [
11459
+ call.on('call.accepted', () => reconcile()),
11460
+ call.on('call.rejected', () => reconcile()),
11461
+ ];
11069
11462
  return () => {
11070
11463
  eventHandlers.forEach((unsubscribe) => unsubscribe());
11071
11464
  };
@@ -15113,7 +15506,8 @@ class Call {
15113
15506
  statsReporter;
15114
15507
  sfuStatsReporter;
15115
15508
  lastStatsOptions;
15116
- dropTimeout;
15509
+ ringTimeout;
15510
+ ringStatePoller;
15117
15511
  clientStore;
15118
15512
  streamClient;
15119
15513
  clientEventReporter;
@@ -15291,22 +15685,19 @@ class Call {
15291
15685
  createSubscription(this.state.session$, (session) => {
15292
15686
  if (!this.ringing)
15293
15687
  return;
15294
- const receiverId = this.clientStore.connectedUser?.id;
15295
- if (!receiverId)
15296
- return;
15297
- const isAcceptedByMe = Boolean(session?.accepted_by[receiverId]);
15298
- const isRejectedByMe = Boolean(session?.rejected_by[receiverId]);
15299
- if (isAcceptedByMe || isRejectedByMe) {
15688
+ const { settledByMe, leaveReason } = resolveOwnRingOutcome({
15689
+ session,
15690
+ currentUserId: this.currentUserId,
15691
+ callingState: this.state.callingState,
15692
+ });
15693
+ if (settledByMe)
15300
15694
  this.cancelAutoDrop();
15301
- }
15302
- const isAcceptedElsewhere = isAcceptedByMe && this.state.callingState === CallingState.RINGING;
15303
- if ((isAcceptedElsewhere || isRejectedByMe) &&
15304
- !hasPending(this.joinLeaveConcurrencyTag)) {
15305
- globalThis.streamRNVideoSDK?.callingX?.endCall(this, isAcceptedElsewhere ? 'answeredElsewhere' : 'rejected');
15306
- this.leave().catch(() => {
15307
- this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
15308
- });
15309
- }
15695
+ if (!leaveReason || hasPending(this.joinLeaveConcurrencyTag))
15696
+ return;
15697
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, leaveReason);
15698
+ this.leave().catch(() => {
15699
+ this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
15700
+ });
15310
15701
  }));
15311
15702
  };
15312
15703
  handleRingingCall = () => {
@@ -15352,6 +15743,7 @@ class Call {
15352
15743
  this.state.setCallingState(CallingState.RINGING);
15353
15744
  }
15354
15745
  this.scheduleAutoDrop();
15746
+ this.scheduleRingStatePolling();
15355
15747
  this.leaveCallHooks.add(registerRingingCallEventHandlers(this));
15356
15748
  }
15357
15749
  };
@@ -15438,6 +15830,12 @@ class Call {
15438
15830
  if (this.state.callingState === CallingState.LEFT) {
15439
15831
  throw new Error('Cannot leave call that has already been left.');
15440
15832
  }
15833
+ // before the first await: the calling state stays RINGING well into the
15834
+ // teardown, so pause both watchdogs before they can race this leave. They
15835
+ // keep their deadlines, so a failed leave resumes them without handing the
15836
+ // ring another window.
15837
+ this.ringTimeout?.pause();
15838
+ this.ringStatePoller?.pause();
15441
15839
  await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
15442
15840
  const callingState = this.state.callingState;
15443
15841
  if (callingState === CallingState.LEFT) {
@@ -15527,6 +15925,7 @@ class Call {
15527
15925
  this.unifiedSessionId = undefined;
15528
15926
  this.ringingSubject.next(false);
15529
15927
  this.cancelAutoDrop();
15928
+ this.cancelRingStatePolling();
15530
15929
  this.clientStore.unregisterCall(this);
15531
15930
  globalThis.streamRNVideoSDK?.callManager.stop({
15532
15931
  isRingingTypeCall: this.ringing,
@@ -15565,6 +15964,15 @@ class Call {
15565
15964
  this.logger.warn('Failed to dispose media engine', err);
15566
15965
  });
15567
15966
  }
15967
+ }).catch((err) => {
15968
+ if (!hasPending(this.joinLeaveConcurrencyTag) &&
15969
+ this.state.callingState === CallingState.RINGING) {
15970
+ // resume, never re-arm: a fresh watchdog would restart the ring
15971
+ // window this leave was already most of the way through
15972
+ this.ringTimeout?.start();
15973
+ this.ringStatePoller?.resume();
15974
+ }
15975
+ throw err;
15568
15976
  });
15569
15977
  };
15570
15978
  /**
@@ -15715,6 +16123,21 @@ class Call {
15715
16123
  ring = async (data = {}) => {
15716
16124
  return this.streamClient.post(`${this.streamClientBasePath}/ring`, data);
15717
16125
  };
16126
+ /**
16127
+ * Returns who accepted, rejected or missed the ring for a call session.
16128
+ * Safe to poll: it performs no writes and emits no events.
16129
+ *
16130
+ * @param callSessionId the call session to read. Defaults to the current one.
16131
+ * Pass it explicitly to read a session that has already ended, as ending a
16132
+ * call clears its current session.
16133
+ */
16134
+ getRingState = async (callSessionId) => {
16135
+ const sessionId = callSessionId ?? this.state.session?.id;
16136
+ if (!sessionId) {
16137
+ throw new Error('Cannot read the ring state: the call has no session');
16138
+ }
16139
+ return this.streamClient.get(`${this.streamClientBasePath}/ring_state`, { call_session_id: sessionId });
16140
+ };
15718
16141
  /**
15719
16142
  * A shortcut for {@link Call.get} with `notify` parameter set to `true`.
15720
16143
  * Will send a `call.notification` event to the call members.
@@ -15755,7 +16178,7 @@ class Call {
15755
16178
  *
15756
16179
  * @returns a promise which resolves once the call join-flow has finished.
15757
16180
  */
15758
- join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
16181
+ join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, joinSource, ...data } = {}) => {
15759
16182
  const callingState = this.state.callingState;
15760
16183
  if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
15761
16184
  throw new Error(`Illegal State: call.join() shall be called only once`);
@@ -15787,7 +16210,7 @@ class Call {
15787
16210
  const joinData = data;
15788
16211
  maxJoinRetries = Math.max(maxJoinRetries, 1);
15789
16212
  try {
15790
- await this.clientEventReporter.withJoinLifecycle(this.cid, 'first-attempt', async () => {
16213
+ await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason: 'first-attempt', joinSource }, async () => {
15791
16214
  for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
15792
16215
  try {
15793
16216
  this.logger.trace(`Joining call (${attempt})`, this.cid);
@@ -16513,7 +16936,7 @@ class Call {
16513
16936
  const joinReason = this.reconnectReason === ReconnectReason.NETWORK_BACK_ONLINE
16514
16937
  ? 'network-available'
16515
16938
  : 'full-rejoin';
16516
- await this.clientEventReporter.withJoinLifecycle(this.cid, joinReason, () => this.doJoin(this.joinCallData));
16939
+ await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason }, () => this.doJoin(this.joinCallData));
16517
16940
  await this.restorePublishedTracks();
16518
16941
  this.restoreSubscribedTracks();
16519
16942
  this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
@@ -16537,7 +16960,7 @@ class Call {
16537
16960
  const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
16538
16961
  try {
16539
16962
  const currentSfu = currentSfuClient.edgeName;
16540
- await this.clientEventReporter.withJoinLifecycle(this.cid, 'migration', () => this.doJoin({
16963
+ await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason: 'migration' }, () => this.doJoin({
16541
16964
  ...this.joinCallData,
16542
16965
  migrating_from: currentSfu,
16543
16966
  migrating_from_list: [currentSfu],
@@ -17310,40 +17733,36 @@ class Call {
17310
17733
  */
17311
17734
  scheduleAutoDrop = () => {
17312
17735
  this.cancelAutoDrop();
17313
- const settings = this.state.settings;
17314
- if (!settings)
17315
- return;
17316
- // ignore if the call is not ringing
17317
- if (this.state.callingState !== CallingState.RINGING)
17318
- return;
17319
- const timeoutInMs = this.isCreatedByMe
17320
- ? settings.ring.auto_cancel_timeout_ms
17321
- : settings.ring.incoming_call_timeout_ms;
17322
- // 0 means no auto-drop
17323
- if (timeoutInMs <= 0)
17324
- return;
17325
- this.dropTimeout = setTimeout(() => {
17326
- // the call might have stopped ringing by this point,
17327
- // e.g. it was already accepted and joined
17328
- if (this.state.callingState !== CallingState.RINGING)
17329
- return;
17330
- this.leave({
17331
- reject: true,
17332
- reason: 'timeout',
17333
- message: `ringing timeout - ${this.isCreatedByMe
17334
- ? 'no one accepted'
17335
- : `user didn't interact with incoming call screen`}`,
17336
- }).catch((err) => {
17337
- this.logger.error('Failed to drop call', err);
17338
- });
17339
- }, timeoutInMs);
17736
+ this.ringTimeout = new RingTimeout(this);
17737
+ this.ringTimeout.start();
17340
17738
  };
17341
17739
  /**
17342
17740
  * Cancels a scheduled auto-drop timeout.
17343
17741
  */
17344
17742
  cancelAutoDrop = () => {
17345
- clearTimeout(this.dropTimeout);
17346
- this.dropTimeout = undefined;
17743
+ this.ringTimeout?.stop();
17744
+ this.ringTimeout = undefined;
17745
+ };
17746
+ /**
17747
+ * Starts polling for the ring outcome. Applicable only to ringing calls the
17748
+ * current user created.
17749
+ */
17750
+ scheduleRingStatePolling = () => {
17751
+ this.cancelRingStatePolling();
17752
+ if (!this.isCreatedByMe)
17753
+ return;
17754
+ const options = this.streamClient.options.ringStatePolling;
17755
+ if (options === false)
17756
+ return;
17757
+ this.ringStatePoller = new RingStatePoller(this, options);
17758
+ this.ringStatePoller.start();
17759
+ };
17760
+ /**
17761
+ * Cancels the ring state polling.
17762
+ */
17763
+ cancelRingStatePolling = () => {
17764
+ this.ringStatePoller?.stop();
17765
+ this.ringStatePoller = undefined;
17347
17766
  };
17348
17767
  /**
17349
17768
  * Retrieves the list of recordings for the current call or call session.
@@ -18992,11 +19411,11 @@ class StreamClient {
18992
19411
  return;
18993
19412
  // call generic listeners
18994
19413
  for (const listener of this.listeners.all || []) {
18995
- listener(event);
19414
+ invokeEventListener(listener, event, this.logger);
18996
19415
  }
18997
19416
  // call type specific listeners
18998
19417
  for (const listener of this.listeners[event.type] || []) {
18999
- listener(event);
19418
+ invokeEventListener(listener, event, this.logger);
19000
19419
  }
19001
19420
  };
19002
19421
  /**
@@ -19016,11 +19435,11 @@ class StreamClient {
19016
19435
  return await this.wsConnection.connect(this.defaultWSTimeout);
19017
19436
  };
19018
19437
  getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
19019
- "1.59.0";
19438
+ "1.60.0";
19020
19439
  getUserAgent = () => {
19021
19440
  if (!this.cachedUserAgent) {
19022
19441
  const { clientAppIdentifier = {} } = this.options;
19023
- const { sdkName = 'js', sdkVersion = "1.59.0", ...extras } = clientAppIdentifier;
19442
+ const { sdkName = 'js', sdkVersion = "1.60.0", ...extras } = clientAppIdentifier;
19024
19443
  this.cachedUserAgent = [
19025
19444
  `stream-video-${sdkName}-v${sdkVersion}`,
19026
19445
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -19153,6 +19572,7 @@ class ClientEventReporter {
19153
19572
  callContexts = new Map();
19154
19573
  joinAttemptIds = new Map();
19155
19574
  joinReasons = new Map();
19575
+ joinSources = new Map();
19156
19576
  coordinatorPairs = new Map();
19157
19577
  wsPairs = new Map();
19158
19578
  peerConnectionPairs = new Map();
@@ -19267,6 +19687,7 @@ class ClientEventReporter {
19267
19687
  this.callContexts.delete(cid);
19268
19688
  this.joinAttemptIds.delete(cid);
19269
19689
  this.joinReasons.delete(cid);
19690
+ this.joinSources.delete(cid);
19270
19691
  this.coordinatorPairs.delete(cid);
19271
19692
  this.wsPairs.delete(cid);
19272
19693
  this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
@@ -19291,7 +19712,12 @@ class ClientEventReporter {
19291
19712
  this.logger.warn('Failed to start join correlation', err);
19292
19713
  }
19293
19714
  };
19294
- withJoinLifecycle = async (cid, joinReason, op) => {
19715
+ withJoinLifecycle = async (cid, options, op) => {
19716
+ const { joinReason, joinSource } = options;
19717
+ if (joinSource)
19718
+ this.joinSources.set(cid, joinSource);
19719
+ else
19720
+ this.joinSources.delete(cid);
19295
19721
  this.startCorrelation(cid, joinReason);
19296
19722
  try {
19297
19723
  return await op();
@@ -19300,6 +19726,9 @@ class ClientEventReporter {
19300
19726
  this.closeCallPairs(cid);
19301
19727
  throw err;
19302
19728
  }
19729
+ finally {
19730
+ this.joinSources.delete(cid);
19731
+ }
19303
19732
  };
19304
19733
  track = async (cid, stage, op) => {
19305
19734
  this.beginAttempt(cid, stage);
@@ -19423,6 +19852,7 @@ class ClientEventReporter {
19423
19852
  startedAt: Date.now(),
19424
19853
  joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
19425
19854
  joinReasonSnapshot: this.joinReasons.get(cid),
19855
+ joinSourceSnapshot: this.joinSources.get(cid),
19426
19856
  };
19427
19857
  this.coordinatorPairs.set(cid, pair);
19428
19858
  this.sendForCall(cid, {
@@ -19430,6 +19860,7 @@ class ClientEventReporter {
19430
19860
  ...(pair.joinReasonSnapshot && {
19431
19861
  join_reason: pair.joinReasonSnapshot,
19432
19862
  }),
19863
+ ...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
19433
19864
  event_type: 'initiated',
19434
19865
  });
19435
19866
  }
@@ -19444,6 +19875,7 @@ class ClientEventReporter {
19444
19875
  ...this.buildCommon(cid, 'CoordinatorJoin', pair),
19445
19876
  ...this.sessionIdField(cid),
19446
19877
  ...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
19878
+ ...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
19447
19879
  event_type: 'completed',
19448
19880
  outcome: 'success',
19449
19881
  retry_count_attempt: pair.attempts - 1,
@@ -19462,6 +19894,7 @@ class ClientEventReporter {
19462
19894
  ...this.buildCommon(cid, 'CoordinatorJoin', pair),
19463
19895
  ...this.sessionIdField(cid),
19464
19896
  ...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
19897
+ ...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
19465
19898
  event_type: 'completed',
19466
19899
  outcome: 'failure',
19467
19900
  retry_count_attempt: pair.attempts - 1,
@@ -20773,5 +21206,5 @@ class EncryptionManager extends TypedEventEmitter {
20773
21206
  };
20774
21207
  }
20775
21208
 
20776
- export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DEFAULT_LOOPBACK_RECORDING_DURATION_MS, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, EncryptionManager, EncryptionSettingsRequestModeEnum, EncryptionSettingsResponseModeEnum, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, HLSSettingsRequestQualityTracksEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsRequestOutputTypesEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LOOPBACK_STREAMS_WAIT_TIMEOUT_MS, LayoutSettingsRequestNameEnum, LoopbackStreamsTimeoutError, MAX_LOOPBACK_RECORDING_DURATION_MS, MIN_LOOPBACK_RECORDING_DURATION_MS, MediaStreamRecorder, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, RequestPermissionRequestPermissionsEnum, 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, TranslationSettingsLanguagesEnum, UpdateUserPermissionsRequestGrantPermissionsEnum, UpdateUserPermissionsRequestRevokePermissionsEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, WSAuthMessageProductsEnum, checkIfAudioOutputChangeSupported, clampLoopbackRecordingDuration, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, enableLoopbackAudio, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getCallMediaEngineProvider, getClientDetails, getDeviceState, getLoopbackStreams, getLoopbackTracks, 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, setCallMediaEngineProvider, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, waitForLoopbackStreams, withLoopbackAudioEnabled, withParticipantSource };
21209
+ export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DEFAULT_LOOPBACK_RECORDING_DURATION_MS, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, EncryptionManager, EncryptionSettingsRequestModeEnum, EncryptionSettingsResponseModeEnum, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, HLSSettingsRequestQualityTracksEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsRequestOutputTypesEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LOOPBACK_STREAMS_WAIT_TIMEOUT_MS, LayoutSettingsRequestNameEnum, LoopbackStreamsTimeoutError, MAX_LOOPBACK_RECORDING_DURATION_MS, MIN_LOOPBACK_RECORDING_DURATION_MS, MediaStreamRecorder, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, RequestPermissionRequestPermissionsEnum, 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, TranslationSettingsLanguagesEnum, UpdateUserPermissionsRequestGrantPermissionsEnum, UpdateUserPermissionsRequestRevokePermissionsEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, WSAuthMessageProductsEnum, checkIfAudioOutputChangeSupported, clampLoopbackRecordingDuration, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, enableLoopbackAudio, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getCallMediaEngineProvider, getClientDetails, getDeviceState, getLoopbackStreams, getLoopbackTracks, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishing, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setCallMediaEngineProvider, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, waitForLoopbackStreams, withLoopbackAudioEnabled, withParticipantSource };
20777
21210
  //# sourceMappingURL=index.browser.es.js.map