@stream-io/video-client 2.0.0-beta.0 → 2.0.0-beta.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 (90) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/index.browser.es.js +1126 -791
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +1126 -791
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.es.js +1126 -791
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/src/Call.d.ts +37 -11
  10. package/dist/src/StreamVideoClient.d.ts +15 -21
  11. package/dist/src/coordinator/connection/types.d.ts +16 -0
  12. package/dist/src/events/call.d.ts +0 -11
  13. package/dist/src/gen/coordinator/index.d.ts +73 -0
  14. package/dist/src/reporting/ClientEventReporter.d.ts +11 -1
  15. package/dist/src/ringing/RingStatePoller.d.ts +46 -0
  16. package/dist/src/ringing/RingTimeout.d.ts +27 -0
  17. package/dist/src/ringing/index.d.ts +4 -0
  18. package/dist/src/ringing/reconcileRingState.d.ts +18 -0
  19. package/dist/src/ringing/resolveOwnRingOutcome.d.ts +32 -0
  20. package/dist/src/rtc/Publisher.d.ts +18 -0
  21. package/dist/src/rtc/TransceiverCache.d.ts +10 -0
  22. package/dist/src/rtc/e2ee/E2EEManager.d.ts +10 -1
  23. package/dist/src/stats/utils.d.ts +6 -0
  24. package/dist/src/store/CallState.d.ts +118 -267
  25. package/dist/src/store/ClientState.d.ts +62 -0
  26. package/dist/src/store/index.d.ts +1 -1
  27. package/dist/src/store/subjects.d.ts +15 -0
  28. package/dist/src/store/types.d.ts +22 -0
  29. package/dist/src/types.d.ts +33 -4
  30. package/index.ts +1 -0
  31. package/package.json +2 -2
  32. package/src/Call.ts +285 -125
  33. package/src/StreamSfuClient.ts +6 -2
  34. package/src/StreamVideoClient.ts +40 -47
  35. package/src/__tests__/Call.autodrop.test.ts +118 -66
  36. package/src/__tests__/Call.getRingState.test.ts +87 -0
  37. package/src/__tests__/Call.lifecycle.test.ts +499 -6
  38. package/src/__tests__/Call.publishing.test.ts +2 -2
  39. package/src/__tests__/Call.ringSettled.test.ts +71 -0
  40. package/src/__tests__/Call.ringingLifecycle.test.ts +242 -0
  41. package/src/__tests__/StreamSfuClient.test.ts +48 -0
  42. package/src/__tests__/StreamVideoClient.disconnect.test.ts +62 -0
  43. package/src/coordinator/connection/types.ts +19 -0
  44. package/src/devices/__tests__/CameraManager.test.ts +2 -2
  45. package/src/devices/__tests__/DeviceManager.test.ts +2 -2
  46. package/src/devices/__tests__/DeviceManagerFilters.test.ts +2 -2
  47. package/src/devices/__tests__/MicrophoneManager.test.ts +2 -2
  48. package/src/devices/__tests__/MicrophoneManagerRN.test.ts +2 -2
  49. package/src/devices/__tests__/ScreenShareManager.test.ts +3 -3
  50. package/src/devices/__tests__/SpeakerManager.test.ts +4 -4
  51. package/src/events/__tests__/call.test.ts +5 -220
  52. package/src/events/call.ts +1 -73
  53. package/src/events/callEventHandlers.ts +12 -23
  54. package/src/gen/coordinator/index.ts +67 -0
  55. package/src/helpers/__tests__/AudioBindingsWatchdog.test.ts +2 -2
  56. package/src/helpers/__tests__/DynascaleManager.test.ts +2 -2
  57. package/src/helpers/__tests__/ViewportTracker.test.ts +2 -2
  58. package/src/reporting/ClientEventReporter.ts +30 -4
  59. package/src/reporting/__tests__/ClientEventReporter.test.ts +184 -7
  60. package/src/ringing/RingStatePoller.ts +177 -0
  61. package/src/ringing/RingTimeout.ts +90 -0
  62. package/src/ringing/__tests__/RingStatePoller.test.ts +462 -0
  63. package/src/ringing/__tests__/RingTimeout.test.ts +199 -0
  64. package/src/ringing/__tests__/reconcileRingState.test.ts +330 -0
  65. package/src/ringing/__tests__/resolveOwnRingOutcome.test.ts +103 -0
  66. package/src/ringing/index.ts +4 -0
  67. package/src/ringing/reconcileRingState.ts +108 -0
  68. package/src/ringing/resolveOwnRingOutcome.ts +58 -0
  69. package/src/rtc/Publisher.ts +66 -12
  70. package/src/rtc/TransceiverCache.ts +17 -0
  71. package/src/rtc/__tests__/Call.reconnect.test.ts +5 -5
  72. package/src/rtc/__tests__/Publisher.test.ts +209 -0
  73. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +1 -0
  74. package/src/rtc/e2ee/E2EEManager.ts +10 -1
  75. package/src/rtc/e2ee/EncryptionManager.ts +1 -1
  76. package/src/rtc/e2ee/SPEC.md +1 -1
  77. package/src/stats/__tests__/utils.test.ts +33 -0
  78. package/src/stats/utils.ts +15 -0
  79. package/src/store/CallState.ts +305 -550
  80. package/src/store/ClientState.ts +101 -0
  81. package/src/store/__tests__/CallState.test.ts +60 -1
  82. package/src/store/__tests__/ClientState.test.ts +35 -0
  83. package/src/store/__tests__/rxUtils.test.ts +67 -1
  84. package/src/store/index.ts +1 -1
  85. package/src/store/rxUtils.ts +8 -4
  86. package/src/store/subjects.ts +30 -0
  87. package/src/store/types.ts +45 -0
  88. package/src/types.ts +40 -4
  89. package/dist/src/store/stateStore.d.ts +0 -84
  90. package/src/store/stateStore.ts +0 -156
package/dist/index.cjs.js CHANGED
@@ -5101,7 +5101,7 @@ const toIceCandidate = (iceTrickle) => {
5101
5101
  }
5102
5102
  };
5103
5103
 
5104
- const version = "2.0.0-beta.0";
5104
+ const version = "2.0.0-beta.1";
5105
5105
  const [major, minor, patch] = version.split('.');
5106
5106
  let sdkInfo = {
5107
5107
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -5481,11 +5481,14 @@ const isFunctionPatch = (update) => typeof update === 'function';
5481
5481
  * @param observable$ the observable to get the value from.
5482
5482
  */
5483
5483
  const getCurrentValue = (observable$) => {
5484
+ if (observable$ instanceof rxjs.BehaviorSubject) {
5485
+ return observable$.getValue();
5486
+ }
5484
5487
  let value;
5485
5488
  let err = undefined;
5486
- rxjs.combineLatest([observable$])
5489
+ observable$
5487
5490
  .subscribe({
5488
- next: ([v]) => {
5491
+ next: (v) => {
5489
5492
  value = v;
5490
5493
  },
5491
5494
  error: (e) => {
@@ -5634,38 +5637,39 @@ exports.CallingState = void 0;
5634
5637
  CallingState["OFFLINE"] = "offline";
5635
5638
  })(exports.CallingState || (exports.CallingState = {}));
5636
5639
 
5637
- class StreamVideoWriteableStateStore {
5638
- /**
5639
- * A store keeping data of a successfully connected user over WS to the coordinator server.
5640
- */
5641
- connectedUserSubject = new rxjs.BehaviorSubject(undefined);
5642
- /**
5643
- * A list of {@link Call} objects created/tracked by this client.
5644
- */
5645
- callsSubject = new rxjs.BehaviorSubject([]);
5646
- constructor() {
5647
- this.connectedUserSubject.subscribe(async (user) => {
5648
- // leave all calls when the user disconnects.
5649
- if (!user) {
5650
- const logger = videoLoggerSystem.getLogger('client-state');
5651
- for (const call of this.calls) {
5652
- if (call.state.callingState === exports.CallingState.LEFT)
5653
- continue;
5654
- logger.info(`User disconnected, leaving call: ${call.cid}`);
5655
- await call
5656
- .leave({ message: 'client.disconnectUser() called' })
5657
- .catch((err) => {
5658
- logger.error(`Error leaving call: ${call.cid}`, err);
5659
- });
5660
- }
5661
- }
5662
- });
5663
- }
5664
- /**
5665
- * The currently connected user.
5666
- */
5640
+ function subject(initialValue) {
5641
+ return new rxjs.BehaviorSubject(initialValue);
5642
+ }
5643
+ /**
5644
+ * Creates an Observable from the given subject by piping it to the
5645
+ * `distinctUntilChanged()` operator.
5646
+ */
5647
+ const duc = (source, comparator) => source.pipe(rxjs.distinctUntilChanged(comparator));
5648
+ /**
5649
+ * Multicasts the given Observable, replaying the latest computed value to
5650
+ * every new subscriber instead of re-running the pipeline for each of them.
5651
+ */
5652
+ const shared = (source) => source.pipe(rxjs.shareReplay({ bufferSize: 1, refCount: true }));
5653
+
5654
+ /**
5655
+ * Holds the state of the client, and the calls it created or tracks.
5656
+ * You can subscribe to the exposed observables to get notified about changes.
5657
+ */
5658
+ class ClientState {
5659
+ logger = videoLoggerSystem.getLogger('ClientState');
5660
+ connectedUserSubject = subject();
5661
+ callsSubject = subject([]);
5662
+ /** Data describing a user successfully connected over WS to the coordinator. */
5663
+ connectedUser$ = this.connectedUserSubject.asObservable();
5664
+ /** A list of {@link Call} objects created/tracked by this client. */
5665
+ calls$ = this.callsSubject.asObservable();
5666
+ /** The currently connected user. */
5667
5667
  get connectedUser() {
5668
- return this.connectedUserSubject.getValue();
5668
+ return getCurrentValue(this.connectedUserSubject);
5669
+ }
5670
+ /** A list of {@link Call} objects created/tracked by this client. */
5671
+ get calls() {
5672
+ return getCurrentValue(this.callsSubject);
5669
5673
  }
5670
5674
  /**
5671
5675
  * Sets the currently connected user.
@@ -5676,22 +5680,19 @@ class StreamVideoWriteableStateStore {
5676
5680
  setConnectedUser = (user) => {
5677
5681
  return setCurrentValue(this.connectedUserSubject, user);
5678
5682
  };
5679
- /**
5680
- * A list of {@link Call} objects created/tracked by this client.
5681
- */
5682
- get calls() {
5683
- return getCurrentValue(this.callsSubject);
5684
- }
5685
5683
  /**
5686
5684
  * Sets the list of {@link Call} objects created/tracked by this client.
5687
- * @param calls
5685
+ *
5686
+ * @internal
5687
+ * @param calls the calls to set.
5688
5688
  */
5689
5689
  setCalls = (calls) => {
5690
5690
  return setCurrentValue(this.callsSubject, calls);
5691
5691
  };
5692
5692
  /**
5693
- * Adds a {@link Call} object to the list of {@link Call} objects created/tracked by this client.
5693
+ * Adds a {@link Call} object to the list of calls tracked by this client.
5694
5694
  *
5695
+ * @internal
5695
5696
  * @param call the call to add.
5696
5697
  */
5697
5698
  registerCall = (call) => {
@@ -5702,6 +5703,7 @@ class StreamVideoWriteableStateStore {
5702
5703
  /**
5703
5704
  * Registers a {@link Call} object if it doesn't exist, otherwise updates it.
5704
5705
  *
5706
+ * @internal
5705
5707
  * @param call the call to register or update.
5706
5708
  */
5707
5709
  registerOrUpdateCall = (call) => {
@@ -5713,17 +5715,17 @@ class StreamVideoWriteableStateStore {
5713
5715
  }
5714
5716
  };
5715
5717
  /**
5716
- * Removes a {@link Call} object from the list of {@link Call} objects created/tracked by this client.
5718
+ * Removes a {@link Call} object from the list of calls tracked by this client.
5717
5719
  *
5718
- * @param call the call to remove
5720
+ * @internal
5721
+ * @param call the call to remove.
5719
5722
  */
5720
5723
  unregisterCall = (call) => {
5721
- const logger = videoLoggerSystem.getLogger('client-state');
5722
- logger.trace(`Unregistering call: ${call.cid}`);
5724
+ this.logger.trace(`Unregistering call: ${call.cid}`);
5723
5725
  return this.setCalls((calls) => calls.filter((c) => c !== call));
5724
5726
  };
5725
5727
  /**
5726
- * Finds a {@link Call} object in the list of {@link Call} objects created/tracked by this client.
5728
+ * Finds a {@link Call} object in the list of calls tracked by this client.
5727
5729
  *
5728
5730
  * @param type the type of call to find.
5729
5731
  * @param id the id of the call to find.
@@ -5732,38 +5734,6 @@ class StreamVideoWriteableStateStore {
5732
5734
  return this.calls.find((c) => c.type === type && c.id === id);
5733
5735
  };
5734
5736
  }
5735
- /**
5736
- * A reactive store that exposes state variables in a reactive manner.
5737
- * You can subscribe to changes of the different state variables.
5738
- * This central store contains all the state variables related to [`StreamVideoClient`](./StreamVideClient.md) and [`Call`](./Call.md).
5739
- */
5740
- class StreamVideoReadOnlyStateStore {
5741
- /**
5742
- * Data describing a user successfully connected over WS to coordinator server.
5743
- */
5744
- connectedUser$;
5745
- /**
5746
- * A list of {@link Call} objects created/tracked by this client.
5747
- */
5748
- calls$;
5749
- constructor(store) {
5750
- // convert and expose subjects as observables
5751
- this.connectedUser$ = store.connectedUserSubject.asObservable();
5752
- this.calls$ = store.callsSubject.asObservable();
5753
- }
5754
- /**
5755
- * The current user connected over WS to the backend.
5756
- */
5757
- get connectedUser() {
5758
- return getCurrentValue(this.connectedUser$);
5759
- }
5760
- /**
5761
- * A list of {@link Call} objects created/tracked by this client.
5762
- */
5763
- get calls() {
5764
- return getCurrentValue(this.calls$);
5765
- }
5766
- }
5767
5737
 
5768
5738
  /**
5769
5739
  * Creates a new combined {@link Comparator<T>} which sorts items by the given comparators.
@@ -6096,9 +6066,7 @@ const paginatedLayoutSortPreset = combineComparators(pinned, ifInvisibleOrUnknow
6096
6066
  */
6097
6067
  const livestreamOrAudioRoomSortPreset = combineComparators(publishing, ifInvisibleOrUnknownBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)), role('admin', 'host', 'speaker'));
6098
6068
 
6099
- /**
6100
- * Returns the default egress object - when no egress data is available.
6101
- */
6069
+ /** Returns the default egress object - when no egress data is available. */
6102
6070
  const defaultEgress = {
6103
6071
  broadcasting: false,
6104
6072
  hls: { playlist_url: '', status: '' },
@@ -6109,93 +6077,120 @@ const defaultEgress = {
6109
6077
  * @react You don't have to use this class directly, as we are exposing the state through Hooks.
6110
6078
  */
6111
6079
  class CallState {
6112
- backstageSubject = new rxjs.BehaviorSubject(true);
6113
- blockedUserIdsSubject = new rxjs.BehaviorSubject([]);
6114
- createdAtSubject = new rxjs.BehaviorSubject(new Date());
6115
- endedAtSubject = new rxjs.BehaviorSubject(undefined);
6116
- startsAtSubject = new rxjs.BehaviorSubject(undefined);
6117
- updatedAtSubject = new rxjs.BehaviorSubject(new Date());
6118
- createdBySubject = new rxjs.BehaviorSubject(undefined);
6119
- customSubject = new rxjs.BehaviorSubject({});
6120
- egressSubject = new rxjs.BehaviorSubject(undefined);
6121
- ingressSubject = new rxjs.BehaviorSubject(undefined);
6122
- recordingSubject = new rxjs.BehaviorSubject(false);
6123
- individualRecordingSubject = new rxjs.BehaviorSubject(false);
6124
- rawRecordingSubject = new rxjs.BehaviorSubject(false);
6125
- sessionSubject = new rxjs.BehaviorSubject(undefined);
6126
- settingsSubject = new rxjs.BehaviorSubject(undefined);
6127
- transcribingSubject = new rxjs.BehaviorSubject(false);
6128
- captioningSubject = new rxjs.BehaviorSubject(false);
6129
- e2eeEnabledSubject = new rxjs.BehaviorSubject(false);
6130
- endedBySubject = new rxjs.BehaviorSubject(undefined);
6131
- thumbnailsSubject = new rxjs.BehaviorSubject(undefined);
6132
- membersSubject = new rxjs.BehaviorSubject([]);
6133
- ownCapabilitiesSubject = new rxjs.BehaviorSubject([]);
6134
- callingStateSubject = new rxjs.BehaviorSubject(exports.CallingState.UNKNOWN);
6135
- startedAtSubject = new rxjs.BehaviorSubject(undefined);
6136
- participantCountSubject = new rxjs.BehaviorSubject(0);
6137
- anonymousParticipantCountSubject = new rxjs.BehaviorSubject(0);
6138
- participantsSubject = new rxjs.BehaviorSubject([]);
6139
- callStatsReportSubject = new rxjs.BehaviorSubject(undefined);
6140
- closedCaptionsSubject = new rxjs.BehaviorSubject([]);
6141
- // These are tracks that were delivered to the Subscriber's onTrack event
6142
- // that we couldn't associate with a participant yet.
6143
- // This happens when the participantJoined event hasn't been received yet.
6144
- // We keep these tracks around until we can associate them with a participant.
6145
- orphanedTracks = [];
6080
+ logger = videoLoggerSystem.getLogger('CallState');
6081
+ /** A list of comparators that are used to sort the participants. */
6082
+ sortParticipantsBy = defaultSortPreset;
6083
+ backstageSubject = subject(true);
6084
+ blockedUserIdsSubject = subject([]);
6085
+ createdAtSubject = subject(new Date());
6086
+ endedAtSubject = subject();
6087
+ startsAtSubject = subject();
6088
+ updatedAtSubject = subject(new Date());
6089
+ createdBySubject = subject();
6090
+ customSubject = subject({});
6091
+ egressSubject = subject();
6092
+ ingressSubject = subject();
6093
+ recordingSubject = subject(false);
6094
+ individualRecordingSubject = subject(false);
6095
+ rawRecordingSubject = subject(false);
6096
+ sessionSubject = subject();
6097
+ settingsSubject = subject();
6098
+ transcribingSubject = subject(false);
6099
+ captioningSubject = subject(false);
6100
+ e2eeEnabledSubject = subject(false);
6101
+ endedBySubject = subject();
6102
+ thumbnailsSubject = subject();
6103
+ membersSubject = subject([]);
6104
+ ownCapabilitiesSubject = subject([]);
6105
+ callingStateSubject = subject(exports.CallingState.UNKNOWN);
6106
+ startedAtSubject = subject();
6107
+ participantCountSubject = subject(0);
6108
+ anonymousParticipantCountSubject = subject(0);
6109
+ participantsSubject = subject([]);
6110
+ callStatsReportSubject = subject();
6111
+ closedCaptionsSubject = subject([]);
6146
6112
  callGrantsSubject = new rxjs.ReplaySubject(1);
6147
- // Derived state
6113
+ /** The backstage state. */
6114
+ backstage$ = duc(this.backstageSubject);
6115
+ /** Will provide the list of blocked user IDs. */
6116
+ blockedUserIds$ = duc(this.blockedUserIdsSubject, isShallowArrayEqual);
6117
+ /** Will provide the time when this call has been created. */
6118
+ createdAt$ = this.createdAtSubject.asObservable();
6119
+ /** Will provide the time when this call has been ended. */
6120
+ endedAt$ = this.endedAtSubject.asObservable();
6121
+ /** Will provide the time when this call has been scheduled to start. */
6122
+ startsAt$ = this.startsAtSubject.asObservable();
6123
+ /** Will provide the time when this call has been updated. */
6124
+ updatedAt$ = this.updatedAtSubject.asObservable();
6125
+ /** Will provide the user who created this call. */
6126
+ createdBy$ = this.createdBySubject.asObservable();
6127
+ /** Will provide the custom data of this call. */
6128
+ custom$ = this.customSubject.asObservable();
6129
+ /** Will provide the egress data of this call. */
6130
+ egress$ = this.egressSubject.asObservable();
6131
+ /** Will provide the ingress data of this call. */
6132
+ ingress$ = this.ingressSubject.asObservable();
6133
+ /** Will provide the recording state of this call. */
6134
+ recording$ = duc(this.recordingSubject);
6135
+ /** Will provide the recording state of this call. */
6136
+ individualRecording$ = duc(this.individualRecordingSubject);
6137
+ /** Will provide the recording state of this call. */
6138
+ rawRecording$ = duc(this.rawRecordingSubject);
6139
+ /** Will provide the session data of this call. */
6140
+ session$ = this.sessionSubject.asObservable();
6141
+ /** Will provide the settings of this call. */
6142
+ settings$ = this.settingsSubject.asObservable();
6143
+ /** Will provide the transcribing state of this call. */
6144
+ transcribing$ = duc(this.transcribingSubject);
6145
+ /** Will provide the closed captioning state of this call. */
6146
+ captioning$ = duc(this.captioningSubject);
6147
+ /**
6148
+ * Whether end-to-end encryption is active for this call, as reported by the
6149
+ * SFU in the join response.
6150
+ */
6151
+ e2eeEnabled$ = duc(this.e2eeEnabledSubject);
6152
+ /** Will provide the user who ended this call. */
6153
+ endedBy$ = this.endedBySubject.asObservable();
6154
+ /** Will provide the thumbnails of this call. */
6155
+ thumbnails$ = this.thumbnailsSubject.asObservable();
6156
+ /** The list of members in the current call. */
6157
+ members$ = this.membersSubject.asObservable();
6158
+ /** The calling state. */
6159
+ callingState$ = duc(this.callingStateSubject);
6148
6160
  /**
6149
6161
  * The time the call session actually started.
6150
6162
  * Useful for displaying the call duration.
6151
6163
  */
6152
- startedAt$;
6164
+ startedAt$ = this.startedAtSubject.asObservable();
6153
6165
  /**
6154
6166
  * The server-side counted number of participants connected to the current call.
6155
6167
  * This number includes the anonymous participants as well.
6156
6168
  */
6157
- participantCount$;
6169
+ participantCount$ = duc(this.participantCountSubject);
6158
6170
  /**
6159
6171
  * The server-side counted number of anonymous participants connected to the current call.
6160
6172
  * This number excludes the regular participants.
6161
6173
  */
6162
- anonymousParticipantCount$;
6174
+ anonymousParticipantCount$ = duc(this.anonymousParticipantCountSubject);
6163
6175
  /**
6164
6176
  * All participants of the current call (this includes the current user and other participants as well),
6165
6177
  * unsorted. This observable only updates when participants join or leave the call.
6166
6178
  */
6167
- rawParticipants$;
6168
- /**
6169
- * All participants of the current call (this includes the current user and other participants as well),
6170
- * sorted according to the current `sortByParticipantsBy` setting
6171
- */
6172
- participants$;
6173
- /**
6174
- * Remote participants of the current call (this includes every participant except the logged-in user).
6175
- */
6176
- remoteParticipants$;
6177
- /**
6178
- * The local participant of the current call (the logged-in user).
6179
- */
6180
- localParticipant$;
6181
- /**
6182
- * Pinned participants of the current call.
6183
- */
6184
- pinnedParticipants$;
6185
- /**
6186
- * The currently elected dominant speaker in the current call.
6187
- */
6188
- dominantSpeaker$;
6189
- /**
6190
- * Emits true whenever there is an active screen sharing session within
6191
- * the current call. Useful for displaying a "screen sharing" indicator and
6192
- * switching the layout to a screen sharing layout.
6193
- *
6194
- * The actual screen sharing track isn't exposed here, but can be retrieved
6195
- * from the list of call participants. We also don't want to be limiting
6196
- * to the number of share screen tracks are displayed in a call.
6197
- */
6198
- hasOngoingScreenShare$;
6179
+ rawParticipants$ = shared(this.participantsSubject.asObservable());
6180
+ /** All participants of the current call sorted according to the current `sortByParticipantsBy` setting. */
6181
+ participants$ = shared(this.participantsSubject.asObservable().pipe(
6182
+ // maintain stable-sort by mutating the participants stored in the original subject
6183
+ rxjs.map((ps) => ps.sort(this.sortParticipantsBy))));
6184
+ /** The local participant of the current call. */
6185
+ localParticipant$ = shared(this.participants$.pipe(rxjs.map((ps) => ps.find((p) => p.isLocalParticipant))));
6186
+ /** Remote participants of the current call (all except the local participant). */
6187
+ remoteParticipants$ = shared(this.participants$.pipe(rxjs.map((ps) => ps.filter((p) => !p.isLocalParticipant))));
6188
+ /** Pinned participants of the current call. */
6189
+ pinnedParticipants$ = shared(this.participants$.pipe(rxjs.map((ps) => ps.filter((p) => !!p.pin))));
6190
+ /** The currently elected dominant speaker in the current call. */
6191
+ dominantSpeaker$ = shared(this.participants$.pipe(rxjs.map((ps) => ps.find((p) => p.isDominantSpeaker))));
6192
+ /** Emits true whenever there is an active screen sharing session within the current call. */
6193
+ hasOngoingScreenShare$ = shared(this.participants$.pipe(rxjs.map((ps) => ps.some((p) => hasScreenShare(p))), rxjs.distinctUntilChanged()));
6199
6194
  /**
6200
6195
  * The latest stats report of the current call.
6201
6196
  * When stats gathering is enabled, this observable will emit a new value
@@ -6204,232 +6199,66 @@ class CallState {
6204
6199
  * Consumers of this observable can implement their own batching logic
6205
6200
  * in case they want to show historical stats data.
6206
6201
  */
6207
- callStatsReport$;
6208
- /**
6209
- * The list of members in the current call.
6210
- */
6211
- members$;
6212
- /**
6213
- * The list of capabilities of the current user.
6214
- */
6215
- ownCapabilities$;
6216
- /**
6217
- * The calling state.
6218
- */
6219
- callingState$;
6220
- /**
6221
- * The backstage state.
6222
- */
6223
- backstage$;
6224
- /**
6225
- * Will provide the list of blocked user IDs.
6226
- */
6227
- blockedUserIds$;
6228
- /**
6229
- * Will provide the time when this call has been created.
6230
- */
6231
- createdAt$;
6232
- /**
6233
- * Will provide the time when this call has been ended.
6234
- */
6235
- endedAt$;
6236
- /**
6237
- * Will provide the time when this call has been scheduled to start.
6238
- */
6239
- startsAt$;
6240
- /**
6241
- * Will provide the time when this call has been updated.
6242
- */
6243
- updatedAt$;
6244
- /**
6245
- * Will provide the user who created this call.
6246
- */
6247
- createdBy$;
6248
- /**
6249
- * Will provide the custom data of this call.
6250
- */
6251
- custom$;
6252
- /**
6253
- * Will provide the egress data of this call.
6254
- */
6255
- egress$;
6256
- /**
6257
- * Will provide the ingress data of this call.
6258
- */
6259
- ingress$;
6260
- /**
6261
- * Will provide the recording state of this call.
6262
- */
6263
- recording$;
6264
- /**
6265
- * Will provide the recording state of this call.
6266
- */
6267
- individualRecording$;
6268
- /**
6269
- * Will provide the recording state of this call.
6270
- */
6271
- rawRecording$;
6272
- /**
6273
- * Will provide the session data of this call.
6274
- */
6275
- session$;
6276
- /**
6277
- * Will provide the settings of this call.
6278
- */
6279
- settings$;
6280
- /**
6281
- * Will provide the transcribing state of this call.
6282
- */
6283
- transcribing$;
6284
- /**
6285
- * Will provide the closed captioning state of this call.
6286
- */
6287
- captioning$;
6288
- /**
6289
- * Whether end-to-end encryption is active for this call, as reported by the
6290
- * SFU in the join response.
6291
- */
6292
- e2eeEnabled$;
6293
- /**
6294
- * Will provide the user who ended this call.
6295
- */
6296
- endedBy$;
6297
- /**
6298
- * Will provide the thumbnails of this call.
6299
- */
6300
- thumbnails$;
6301
- /**
6302
- * The queue of closed captions.
6303
- */
6304
- closedCaptions$;
6305
- logger = videoLoggerSystem.getLogger('CallState');
6306
- /**
6307
- * A list of comparators that are used to sort the participants.
6308
- */
6309
- sortParticipantsBy = defaultSortPreset;
6310
- /**
6311
- * The closed captions configuration.
6312
- */
6202
+ callStatsReport$ = this.callStatsReportSubject.asObservable();
6203
+ /** The queue of closed captions. */
6204
+ closedCaptions$ = this.closedCaptionsSubject.asObservable();
6205
+ /** The list of capabilities of the current user. */
6206
+ ownCapabilities$ = shared(rxjs.combineLatest([
6207
+ this.ownCapabilitiesSubject,
6208
+ this.callGrantsSubject.pipe(rxjs.startWith(undefined)),
6209
+ ]).pipe(rxjs.map(([capabilities, grants]) => {
6210
+ if (!grants)
6211
+ return capabilities;
6212
+ const { canPublishAudio, canPublishVideo, canScreenshare } = grants;
6213
+ const update = {
6214
+ [OwnCapability.SEND_AUDIO]: canPublishAudio,
6215
+ [OwnCapability.SEND_VIDEO]: canPublishVideo,
6216
+ [OwnCapability.SCREENSHARE]: canScreenshare,
6217
+ };
6218
+ const nextCapabilities = [...capabilities];
6219
+ for (const _capability in update) {
6220
+ const capability = _capability;
6221
+ const allowed = update[capability];
6222
+ // grants take precedence over capabilities, reconstruct the capabilities
6223
+ if (allowed && !nextCapabilities.includes(capability)) {
6224
+ nextCapabilities.push(capability);
6225
+ }
6226
+ else if (!allowed && nextCapabilities.includes(capability)) {
6227
+ const index = nextCapabilities.indexOf(capability);
6228
+ nextCapabilities.splice(index, 1);
6229
+ }
6230
+ }
6231
+ return nextCapabilities;
6232
+ }), rxjs.distinctUntilChanged(isShallowArrayEqual)));
6233
+ // These are tracks that were delivered to the Subscriber's onTrack event
6234
+ // that we couldn't associate with a participant yet.
6235
+ // This happens when the participantJoined event hasn't been received yet.
6236
+ // We keep these tracks around until we can associate them with a participant.
6237
+ orphanedTracks = [];
6238
+ /** The closed captions configuration. */
6313
6239
  closedCaptionsSettings;
6314
6240
  closedCaptionsTasks = new Map();
6315
6241
  eventHandlers;
6316
- /**
6317
- * Creates a new instance of the CallState class.
6318
- *
6319
- */
6242
+ /** Creates a new instance of the CallState class. */
6320
6243
  constructor() {
6321
- this.rawParticipants$ = this.participantsSubject
6322
- .asObservable()
6323
- .pipe(rxjs.shareReplay({ bufferSize: 1, refCount: true }));
6324
- this.participants$ = this.participantsSubject.asObservable().pipe(
6325
- // maintain stable-sort by mutating the participants stored
6326
- // in the original subject
6327
- rxjs.map((ps) => ps.sort(this.sortParticipantsBy)), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
6328
- this.localParticipant$ = this.participants$.pipe(rxjs.map((participants) => participants.find((p) => p.isLocalParticipant)), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
6329
- this.remoteParticipants$ = this.participants$.pipe(rxjs.map((participants) => participants.filter((p) => !p.isLocalParticipant)), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
6330
- this.pinnedParticipants$ = this.participants$.pipe(rxjs.map((participants) => participants.filter((p) => !!p.pin)), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
6331
- this.dominantSpeaker$ = this.participants$.pipe(rxjs.map((participants) => participants.find((p) => p.isDominantSpeaker)), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
6332
- this.hasOngoingScreenShare$ = this.participants$.pipe(rxjs.map((participants) => participants.some((p) => hasScreenShare(p))), rxjs.distinctUntilChanged(), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
6333
- // dates
6334
- this.createdAt$ = this.createdAtSubject.asObservable();
6335
- this.endedAt$ = this.endedAtSubject.asObservable();
6336
- this.startsAt$ = this.startsAtSubject.asObservable();
6337
- this.startedAt$ = this.startedAtSubject.asObservable();
6338
- this.updatedAt$ = this.updatedAtSubject.asObservable();
6339
- this.callStatsReport$ = this.callStatsReportSubject.asObservable();
6340
- this.members$ = this.membersSubject.asObservable();
6341
- // complex objects should work as streams of data
6342
- this.createdBy$ = this.createdBySubject.asObservable();
6343
- this.custom$ = this.customSubject.asObservable();
6344
- this.egress$ = this.egressSubject.asObservable();
6345
- this.ingress$ = this.ingressSubject.asObservable();
6346
- this.session$ = this.sessionSubject.asObservable();
6347
- this.settings$ = this.settingsSubject.asObservable();
6348
- this.endedBy$ = this.endedBySubject.asObservable();
6349
- this.thumbnails$ = this.thumbnailsSubject.asObservable();
6350
- this.closedCaptions$ = this.closedCaptionsSubject.asObservable();
6351
- /**
6352
- * Creates an Observable from the given subject by piping to the
6353
- * `distinctUntilChanged()` operator.
6354
- */
6355
- const duc = (subject, comparator) => subject.pipe(rxjs.distinctUntilChanged(comparator));
6356
- // primitive values should only emit once the value they hold changes
6357
- this.anonymousParticipantCount$ = duc(this.anonymousParticipantCountSubject);
6358
- this.blockedUserIds$ = duc(this.blockedUserIdsSubject, isShallowArrayEqual);
6359
- this.backstage$ = duc(this.backstageSubject);
6360
- this.callingState$ = duc(this.callingStateSubject);
6361
- this.ownCapabilities$ = rxjs.combineLatest([
6362
- this.ownCapabilitiesSubject,
6363
- this.callGrantsSubject.pipe(rxjs.startWith(undefined)),
6364
- ]).pipe(rxjs.map(([capabilities, grants]) => {
6365
- if (!grants)
6366
- return capabilities;
6367
- const { canPublishAudio, canPublishVideo, canScreenshare } = grants;
6368
- const update = {
6369
- [OwnCapability.SEND_AUDIO]: canPublishAudio,
6370
- [OwnCapability.SEND_VIDEO]: canPublishVideo,
6371
- [OwnCapability.SCREENSHARE]: canScreenshare,
6372
- };
6373
- const nextCapabilities = [...capabilities];
6374
- for (const _capability in update) {
6375
- const capability = _capability;
6376
- const allowed = update[capability];
6377
- // grants take precedence over capabilities, reconstruct the capabilities
6378
- if (allowed && !nextCapabilities.includes(capability)) {
6379
- nextCapabilities.push(capability);
6380
- }
6381
- else if (!allowed && nextCapabilities.includes(capability)) {
6382
- const index = nextCapabilities.indexOf(capability);
6383
- nextCapabilities.splice(index, 1);
6384
- }
6385
- }
6386
- return nextCapabilities;
6387
- }), rxjs.distinctUntilChanged(isShallowArrayEqual), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
6388
- this.participantCount$ = duc(this.participantCountSubject);
6389
- this.recording$ = duc(this.recordingSubject);
6390
- this.individualRecording$ = duc(this.individualRecordingSubject);
6391
- this.rawRecording$ = duc(this.rawRecordingSubject);
6392
- this.transcribing$ = duc(this.transcribingSubject);
6393
- this.captioning$ = duc(this.captioningSubject);
6394
- this.e2eeEnabled$ = duc(this.e2eeEnabledSubject);
6395
6244
  this.eventHandlers = {
6396
- // these events are not updating the call state:
6397
- 'call.frame_recording_ready': undefined,
6398
- 'call.kicked_user': undefined,
6399
- 'call.moderation_blur': undefined,
6400
- 'call.moderation_warning': undefined,
6401
- 'call.permission_request': undefined,
6402
- 'call.recording_ready': undefined,
6403
- 'call.rtmp_broadcast_failed': undefined,
6404
- 'call.rtmp_broadcast_started': undefined,
6405
- 'call.rtmp_broadcast_stopped': undefined,
6406
- 'call.stats_report_ready': undefined,
6407
- 'call.transcription_ready': undefined,
6408
- 'call.user_feedback_submitted': undefined,
6409
- 'call.user_muted': undefined,
6410
- 'connection.error': undefined,
6411
- 'connection.ok': undefined,
6412
- 'health.check': undefined,
6413
- 'user.updated': undefined,
6414
- custom: undefined,
6415
- // events that update call state:
6416
6245
  'call.accepted': (e) => this.updateFromCallResponse(e.call),
6417
6246
  'call.blocked_user': this.blockUser,
6418
6247
  'call.closed_caption': this.updateFromClosedCaptions,
6419
6248
  'call.closed_captions_failed': () => {
6420
- this.setCurrentValue(this.captioningSubject, false);
6249
+ setCurrentValue(this.captioningSubject, false);
6421
6250
  },
6422
6251
  'call.closed_captions_started': () => {
6423
- this.setCurrentValue(this.captioningSubject, true);
6252
+ setCurrentValue(this.captioningSubject, true);
6424
6253
  },
6425
6254
  'call.closed_captions_stopped': () => {
6426
- this.setCurrentValue(this.captioningSubject, false);
6255
+ setCurrentValue(this.captioningSubject, false);
6427
6256
  },
6428
6257
  'call.created': (e) => this.updateFromCallResponse(e.call),
6429
6258
  'call.deleted': (e) => this.updateFromCallResponse(e.call),
6430
6259
  'call.ended': (e) => {
6431
6260
  this.updateFromCallResponse(e.call);
6432
- this.setCurrentValue(this.endedBySubject, e.user);
6261
+ setCurrentValue(this.endedBySubject, e.user);
6433
6262
  },
6434
6263
  'call.frame_recording_failed': (e) => {
6435
6264
  this.updateFromCallResponse(e.call);
@@ -6474,21 +6303,19 @@ class CallState {
6474
6303
  'call.session_participant_left': this.updateFromSessionParticipantLeft,
6475
6304
  'call.session_started': (e) => this.updateFromCallResponse(e.call),
6476
6305
  'call.transcription_started': () => {
6477
- this.setCurrentValue(this.transcribingSubject, true);
6306
+ setCurrentValue(this.transcribingSubject, true);
6478
6307
  },
6479
6308
  'call.transcription_stopped': () => {
6480
- this.setCurrentValue(this.transcribingSubject, false);
6309
+ setCurrentValue(this.transcribingSubject, false);
6481
6310
  },
6482
6311
  'call.transcription_failed': () => {
6483
- this.setCurrentValue(this.transcribingSubject, false);
6312
+ setCurrentValue(this.transcribingSubject, false);
6484
6313
  },
6485
6314
  'call.unblocked_user': this.unblockUser,
6486
6315
  'call.updated': (e) => this.updateFromCallResponse(e.call),
6487
6316
  };
6488
6317
  }
6489
- /**
6490
- * Runs the cleanup tasks.
6491
- */
6318
+ /** Runs the cleanup tasks. */
6492
6319
  dispose = () => {
6493
6320
  for (const [ccKey, taskId] of this.closedCaptionsTasks.entries()) {
6494
6321
  clearTimeout(taskId);
@@ -6505,39 +6332,18 @@ class CallState {
6505
6332
  setSortParticipantsBy = (comparator) => {
6506
6333
  this.sortParticipantsBy = comparator;
6507
6334
  // trigger re-sorting of participants
6508
- this.setCurrentValue(this.participantsSubject, (ps) => ps);
6335
+ setCurrentValue(this.participantsSubject, (ps) => ps);
6509
6336
  };
6510
- /**
6511
- * Returns the comparator currently used to sort the participants.
6512
- */
6337
+ /** Returns the comparator currently used to sort the participants. */
6513
6338
  getSortParticipantsBy = () => {
6514
6339
  return this.sortParticipantsBy;
6515
6340
  };
6516
- /**
6517
- * Gets the current value of an observable, or undefined if the observable has
6518
- * not emitted a value yet.
6519
- *
6520
- * @param observable$ the observable to get the value from.
6521
- */
6522
- getCurrentValue = getCurrentValue;
6523
- /**
6524
- * Updates the value of the provided Subject.
6525
- * An `update` can either be a new value or a function which takes
6526
- * the current value and returns a new value.
6527
- *
6528
- * @internal
6529
- *
6530
- * @param subject the subject to update.
6531
- * @param update the update to apply to the subject.
6532
- * @return the updated value.
6533
- */
6534
- setCurrentValue = setCurrentValue;
6535
6341
  /**
6536
6342
  * The server-side counted number of participants connected to the current call.
6537
6343
  * This number includes the anonymous participants as well.
6538
6344
  */
6539
6345
  get participantCount() {
6540
- return this.getCurrentValue(this.participantCount$);
6346
+ return getCurrentValue(this.participantCountSubject);
6541
6347
  }
6542
6348
  /**
6543
6349
  * Sets the number of participants in the current call.
@@ -6546,14 +6352,14 @@ class CallState {
6546
6352
  * @param count the number of participants.
6547
6353
  */
6548
6354
  setParticipantCount = (count) => {
6549
- return this.setCurrentValue(this.participantCountSubject, count);
6355
+ return setCurrentValue(this.participantCountSubject, count);
6550
6356
  };
6551
6357
  /**
6552
6358
  * The time the call session actually started.
6553
6359
  * Useful for displaying the call duration.
6554
6360
  */
6555
6361
  get startedAt() {
6556
- return this.getCurrentValue(this.startedAt$);
6362
+ return getCurrentValue(this.startedAtSubject);
6557
6363
  }
6558
6364
  /**
6559
6365
  * Sets the time the call session actually started.
@@ -6562,13 +6368,11 @@ class CallState {
6562
6368
  * @param startedAt the time the call session actually started.
6563
6369
  */
6564
6370
  setStartedAt = (startedAt) => {
6565
- return this.setCurrentValue(this.startedAtSubject, startedAt);
6371
+ return setCurrentValue(this.startedAtSubject, startedAt);
6566
6372
  };
6567
- /**
6568
- * Returns whether closed captions are enabled in the current call.
6569
- */
6373
+ /** Returns whether closed captions are enabled in the current call. */
6570
6374
  get captioning() {
6571
- return this.getCurrentValue(this.captioning$);
6375
+ return getCurrentValue(this.captioningSubject);
6572
6376
  }
6573
6377
  /**
6574
6378
  * Sets the closed captioning state of the current call.
@@ -6584,7 +6388,7 @@ class CallState {
6584
6388
  * This number includes the anonymous participants as well.
6585
6389
  */
6586
6390
  get anonymousParticipantCount() {
6587
- return this.getCurrentValue(this.anonymousParticipantCount$);
6391
+ return getCurrentValue(this.anonymousParticipantCountSubject);
6588
6392
  }
6589
6393
  /**
6590
6394
  * Sets the number of anonymous participants in the current call.
@@ -6593,19 +6397,15 @@ class CallState {
6593
6397
  * @param count the number of anonymous participants.
6594
6398
  */
6595
6399
  setAnonymousParticipantCount = (count) => {
6596
- return this.setCurrentValue(this.anonymousParticipantCountSubject, count);
6400
+ return setCurrentValue(this.anonymousParticipantCountSubject, count);
6597
6401
  };
6598
- /**
6599
- * The list of participants in the current call.
6600
- */
6402
+ /** The list of participants in the current call. */
6601
6403
  get participants() {
6602
- return this.getCurrentValue(this.participants$);
6404
+ return getCurrentValue(this.participants$);
6603
6405
  }
6604
- /**
6605
- * The stable list of participants in the current call, unsorted.
6606
- */
6406
+ /** The stable list of participants in the current call, unsorted. */
6607
6407
  get rawParticipants() {
6608
- return this.getCurrentValue(this.rawParticipants$);
6408
+ return getCurrentValue(this.rawParticipants$);
6609
6409
  }
6610
6410
  /**
6611
6411
  * Returns the current participants array directly from the BehaviorSubject.
@@ -6626,43 +6426,31 @@ class CallState {
6626
6426
  * @param participants the list of participants.
6627
6427
  */
6628
6428
  setParticipants = (participants) => {
6629
- return this.setCurrentValue(this.participantsSubject, participants);
6429
+ return setCurrentValue(this.participantsSubject, participants);
6630
6430
  };
6631
- /**
6632
- * The local participant in the current call.
6633
- */
6431
+ /** The local participant in the current call. */
6634
6432
  get localParticipant() {
6635
- return this.getCurrentValue(this.localParticipant$);
6433
+ return getCurrentValue(this.localParticipant$);
6636
6434
  }
6637
- /**
6638
- * The list of remote participants in the current call.
6639
- */
6435
+ /** The list of remote participants in the current call. */
6640
6436
  get remoteParticipants() {
6641
- return this.getCurrentValue(this.remoteParticipants$);
6437
+ return getCurrentValue(this.remoteParticipants$);
6642
6438
  }
6643
- /**
6644
- * The dominant speaker in the current call.
6645
- */
6439
+ /** The dominant speaker in the current call. */
6646
6440
  get dominantSpeaker() {
6647
- return this.getCurrentValue(this.dominantSpeaker$);
6441
+ return getCurrentValue(this.dominantSpeaker$);
6648
6442
  }
6649
- /**
6650
- * The list of pinned participants in the current call.
6651
- */
6443
+ /** The list of pinned participants in the current call. */
6652
6444
  get pinnedParticipants() {
6653
- return this.getCurrentValue(this.pinnedParticipants$);
6445
+ return getCurrentValue(this.pinnedParticipants$);
6654
6446
  }
6655
- /**
6656
- * Tell if there is an ongoing screen share in this call.
6657
- */
6447
+ /** Tell if there is an ongoing screen share in this call. */
6658
6448
  get hasOngoingScreenShare() {
6659
- return this.getCurrentValue(this.hasOngoingScreenShare$);
6449
+ return getCurrentValue(this.hasOngoingScreenShare$);
6660
6450
  }
6661
- /**
6662
- * The calling state.
6663
- */
6451
+ /** The calling state. */
6664
6452
  get callingState() {
6665
- return this.getCurrentValue(this.callingState$);
6453
+ return getCurrentValue(this.callingStateSubject);
6666
6454
  }
6667
6455
  /**
6668
6456
  * Sets the calling state.
@@ -6671,13 +6459,11 @@ class CallState {
6671
6459
  * @param state the new calling state.
6672
6460
  */
6673
6461
  setCallingState = (state) => {
6674
- return this.setCurrentValue(this.callingStateSubject, state);
6462
+ return setCurrentValue(this.callingStateSubject, state);
6675
6463
  };
6676
- /**
6677
- * The call stats report.
6678
- */
6464
+ /** The call stats report. */
6679
6465
  get callStatsReport() {
6680
- return this.getCurrentValue(this.callStatsReport$);
6466
+ return getCurrentValue(this.callStatsReportSubject);
6681
6467
  }
6682
6468
  /**
6683
6469
  * Returns whether the call stats report is being observed or not.
@@ -6693,13 +6479,11 @@ class CallState {
6693
6479
  * @param report the report to set.
6694
6480
  */
6695
6481
  setCallStatsReport = (report) => {
6696
- return this.setCurrentValue(this.callStatsReportSubject, report);
6482
+ return setCurrentValue(this.callStatsReportSubject, report);
6697
6483
  };
6698
- /**
6699
- * The members of the current call.
6700
- */
6484
+ /** The members of the current call. */
6701
6485
  get members() {
6702
- return this.getCurrentValue(this.members$);
6486
+ return getCurrentValue(this.membersSubject);
6703
6487
  }
6704
6488
  /**
6705
6489
  * Sets the members of the current call.
@@ -6708,13 +6492,11 @@ class CallState {
6708
6492
  * @param members the members to set.
6709
6493
  */
6710
6494
  setMembers = (members) => {
6711
- this.setCurrentValue(this.membersSubject, members);
6495
+ setCurrentValue(this.membersSubject, members);
6712
6496
  };
6713
- /**
6714
- * The capabilities of the current user for the current call.
6715
- */
6497
+ /** The capabilities of the current user for the current call. */
6716
6498
  get ownCapabilities() {
6717
- return this.getCurrentValue(this.ownCapabilities$);
6499
+ return getCurrentValue(this.ownCapabilities$);
6718
6500
  }
6719
6501
  /**
6720
6502
  * Sets the own capabilities.
@@ -6723,7 +6505,7 @@ class CallState {
6723
6505
  * @param capabilities the capabilities to set.
6724
6506
  */
6725
6507
  setOwnCapabilities = (capabilities) => {
6726
- return this.setCurrentValue(this.ownCapabilitiesSubject, capabilities);
6508
+ return setCurrentValue(this.ownCapabilitiesSubject, capabilities);
6727
6509
  };
6728
6510
  /**
6729
6511
  * Sets the call grants (used for own capabilities).
@@ -6732,141 +6514,105 @@ class CallState {
6732
6514
  * @param grants the grants to set.
6733
6515
  */
6734
6516
  setCallGrants = (grants) => {
6735
- return this.setCurrentValue(this.callGrantsSubject, grants);
6517
+ return setCurrentValue(this.callGrantsSubject, grants);
6736
6518
  };
6737
- /**
6738
- * The backstage state.
6739
- */
6519
+ /** The backstage state. */
6740
6520
  get backstage() {
6741
- return this.getCurrentValue(this.backstage$);
6521
+ return getCurrentValue(this.backstageSubject);
6742
6522
  }
6743
6523
  /**
6744
6524
  * Sets the backstage state.
6745
6525
  * @param backstage the backstage state.
6746
6526
  */
6747
6527
  setBackstage = (backstage) => {
6748
- return this.setCurrentValue(this.backstageSubject, backstage);
6528
+ return setCurrentValue(this.backstageSubject, backstage);
6749
6529
  };
6750
- /**
6751
- * Will provide the list of blocked user IDs.
6752
- */
6530
+ /** Will provide the list of blocked user IDs. */
6753
6531
  get blockedUserIds() {
6754
- return this.getCurrentValue(this.blockedUserIds$);
6532
+ return getCurrentValue(this.blockedUserIdsSubject);
6755
6533
  }
6756
- /**
6757
- * Will provide the time when this call has been created.
6758
- */
6534
+ /** Will provide the time when this call has been created. */
6759
6535
  get createdAt() {
6760
- return this.getCurrentValue(this.createdAt$);
6536
+ return getCurrentValue(this.createdAtSubject);
6761
6537
  }
6762
- /**
6763
- * Will provide the time when this call has been ended.
6764
- */
6538
+ /** Will provide the time when this call has been ended. */
6765
6539
  get endedAt() {
6766
- return this.getCurrentValue(this.endedAt$);
6540
+ return getCurrentValue(this.endedAtSubject);
6767
6541
  }
6768
6542
  /**
6769
6543
  * Sets the time when this call has been ended.
6770
6544
  * @param endedAt the time when this call has been ended.
6771
6545
  */
6772
6546
  setEndedAt = (endedAt) => {
6773
- return this.setCurrentValue(this.endedAtSubject, endedAt);
6547
+ return setCurrentValue(this.endedAtSubject, endedAt);
6774
6548
  };
6775
- /**
6776
- * Will provide the time when this call has been scheduled to start.
6777
- */
6549
+ /** Will provide the time when this call has been scheduled to start. */
6778
6550
  get startsAt() {
6779
- return this.getCurrentValue(this.startsAt$);
6551
+ return getCurrentValue(this.startsAtSubject);
6780
6552
  }
6781
- /**
6782
- * Will provide the time when this call has been updated.
6783
- */
6553
+ /** Will provide the time when this call has been updated. */
6784
6554
  get updatedAt() {
6785
- return this.getCurrentValue(this.updatedAt$);
6555
+ return getCurrentValue(this.updatedAtSubject);
6786
6556
  }
6787
- /**
6788
- * Will provide the user who created this call.
6789
- */
6557
+ /** Will provide the user who created this call. */
6790
6558
  get createdBy() {
6791
- return this.getCurrentValue(this.createdBy$);
6559
+ return getCurrentValue(this.createdBySubject);
6792
6560
  }
6793
- /**
6794
- * Will provide the custom data of this call.
6795
- */
6561
+ /** Will provide the custom data of this call. */
6796
6562
  get custom() {
6797
- return this.getCurrentValue(this.custom$);
6563
+ return getCurrentValue(this.customSubject);
6798
6564
  }
6799
6565
  /**
6800
6566
  * Will provide the egress data of this call.
6801
6567
  */
6802
6568
  get egress() {
6803
- return this.getCurrentValue(this.egress$);
6569
+ return getCurrentValue(this.egressSubject);
6804
6570
  }
6805
- /**
6806
- * Will provide the ingress data of this call.
6807
- */
6571
+ /** Will provide the ingress data of this call. */
6808
6572
  get ingress() {
6809
- return this.getCurrentValue(this.ingress$);
6573
+ return getCurrentValue(this.ingressSubject);
6810
6574
  }
6811
- /**
6812
- * Will provide the composite recording state of this call.
6813
- */
6575
+ /** Will provide the composite recording state of this call. */
6814
6576
  get recording() {
6815
- return this.getCurrentValue(this.recording$);
6577
+ return getCurrentValue(this.recordingSubject);
6816
6578
  }
6817
- /**
6818
- * Will provide the individual recording state of this call.
6819
- */
6579
+ /** Will provide the individual recording state of this call. */
6820
6580
  get individualRecording() {
6821
- return this.getCurrentValue(this.individualRecording$);
6581
+ return getCurrentValue(this.individualRecordingSubject);
6822
6582
  }
6823
- /**
6824
- * Will provide the raw recording state of this call.
6825
- */
6583
+ /** Will provide the raw recording state of this call. */
6826
6584
  get rawRecording() {
6827
- return this.getCurrentValue(this.rawRecording$);
6585
+ return getCurrentValue(this.rawRecordingSubject);
6828
6586
  }
6829
- /**
6830
- * Will provide the session data of this call.
6831
- */
6587
+ /** Will provide the session data of this call. */
6832
6588
  get session() {
6833
- return this.getCurrentValue(this.session$);
6589
+ return getCurrentValue(this.sessionSubject);
6834
6590
  }
6835
- /**
6836
- * Will provide the settings of this call.
6837
- */
6591
+ /** Will provide the settings of this call. */
6838
6592
  get settings() {
6839
- return this.getCurrentValue(this.settings$);
6593
+ return getCurrentValue(this.settingsSubject);
6840
6594
  }
6841
- /**
6842
- * Will provide the transcribing state of this call.
6843
- */
6595
+ /** Will provide the transcribing state of this call. */
6844
6596
  get transcribing() {
6845
- return this.getCurrentValue(this.transcribing$);
6597
+ return getCurrentValue(this.transcribingSubject);
6846
6598
  }
6847
- /**
6848
- * Whether end-to-end encryption is active for this call.
6849
- */
6599
+ /** Whether end-to-end encryption is active for this call. */
6850
6600
  get e2eeEnabled() {
6851
- return this.getCurrentValue(this.e2eeEnabled$);
6601
+ return getCurrentValue(this.e2eeEnabledSubject);
6852
6602
  }
6853
- /**
6854
- * Will provide the user who ended this call.
6855
- */
6603
+ /** Will provide the user who ended this call. */
6856
6604
  get endedBy() {
6857
- return this.getCurrentValue(this.endedBy$);
6605
+ return getCurrentValue(this.endedBySubject);
6858
6606
  }
6859
6607
  /**
6860
6608
  * Will provide the thumbnails of this call, if enabled in the call settings.
6861
6609
  */
6862
6610
  get thumbnails() {
6863
- return this.getCurrentValue(this.thumbnails$);
6611
+ return getCurrentValue(this.thumbnailsSubject);
6864
6612
  }
6865
- /**
6866
- * Returns the current queue of closed captions.
6867
- */
6613
+ /** Returns the current queue of closed captions. */
6868
6614
  get closedCaptions() {
6869
- return this.getCurrentValue(this.closedCaptions$);
6615
+ return getCurrentValue(this.closedCaptionsSubject);
6870
6616
  }
6871
6617
  /**
6872
6618
  * Will try to find the participant with the given sessionId in the current call.
@@ -6877,9 +6623,7 @@ class CallState {
6877
6623
  findParticipantBySessionId = (sessionId) => {
6878
6624
  return this.participants.find((p) => p.sessionId === sessionId);
6879
6625
  };
6880
- /**
6881
- * Returns a new lookup table of participants indexed by their session ID.
6882
- */
6626
+ /** Returns a new lookup table of participants indexed by their session ID. */
6883
6627
  getParticipantLookupBySessionId = () => {
6884
6628
  return this.participants.reduce((lookupTable, participant) => {
6885
6629
  lookupTable[participant.sessionId] = participant;
@@ -7114,25 +6858,49 @@ class CallState {
7114
6858
  */
7115
6859
  updateFromCallResponse = (call) => {
7116
6860
  this.setBackstage(call.backstage);
7117
- this.setCurrentValue(this.blockedUserIdsSubject, call.blocked_user_ids);
7118
- this.setCurrentValue(this.createdAtSubject, new Date(call.created_at));
7119
- this.setCurrentValue(this.updatedAtSubject, new Date(call.updated_at));
7120
- this.setCurrentValue(this.startsAtSubject, call.starts_at ? new Date(call.starts_at) : undefined);
6861
+ setCurrentValue(this.blockedUserIdsSubject, call.blocked_user_ids);
6862
+ setCurrentValue(this.createdAtSubject, new Date(call.created_at));
6863
+ setCurrentValue(this.updatedAtSubject, new Date(call.updated_at));
6864
+ setCurrentValue(this.startsAtSubject, call.starts_at ? new Date(call.starts_at) : undefined);
7121
6865
  this.setEndedAt(call.ended_at ? new Date(call.ended_at) : undefined);
7122
- this.setCurrentValue(this.createdBySubject, call.created_by);
7123
- this.setCurrentValue(this.customSubject, call.custom);
7124
- this.setCurrentValue(this.egressSubject, call.egress);
7125
- this.setCurrentValue(this.ingressSubject, call.ingress);
6866
+ setCurrentValue(this.createdBySubject, call.created_by);
6867
+ setCurrentValue(this.customSubject, call.custom);
6868
+ setCurrentValue(this.egressSubject, call.egress);
6869
+ setCurrentValue(this.ingressSubject, call.ingress);
7126
6870
  const { individual_recording, composite_recording, raw_recording } = call.egress;
7127
- this.setCurrentValue(this.recordingSubject, call.recording || composite_recording?.status === 'running');
7128
- this.setCurrentValue(this.individualRecordingSubject, individual_recording?.status === 'running');
7129
- this.setCurrentValue(this.rawRecordingSubject, raw_recording?.status === 'running');
7130
- const s = this.setCurrentValue(this.sessionSubject, call.session);
6871
+ setCurrentValue(this.recordingSubject, call.recording || composite_recording?.status === 'running');
6872
+ setCurrentValue(this.individualRecordingSubject, individual_recording?.status === 'running');
6873
+ setCurrentValue(this.rawRecordingSubject, raw_recording?.status === 'running');
6874
+ const s = setCurrentValue(this.sessionSubject, call.session);
7131
6875
  this.updateParticipantCountFromSession(s);
7132
- this.setCurrentValue(this.settingsSubject, call.settings);
7133
- this.setCurrentValue(this.transcribingSubject, call.transcribing);
7134
- this.setCurrentValue(this.captioningSubject, call.captioning);
7135
- this.setCurrentValue(this.thumbnailsSubject, call.thumbnails);
6876
+ setCurrentValue(this.settingsSubject, call.settings);
6877
+ setCurrentValue(this.transcribingSubject, call.transcribing);
6878
+ setCurrentValue(this.captioningSubject, call.captioning);
6879
+ setCurrentValue(this.thumbnailsSubject, call.thumbnails);
6880
+ };
6881
+ /**
6882
+ * Merges a polled ring state into the current session. Only the ring fields
6883
+ * are touched: the endpoint does not return the session roster.
6884
+ *
6885
+ * @internal
6886
+ *
6887
+ * @param ringState the ring state as returned by the coordinator.
6888
+ */
6889
+ updateFromRingState = (ringState) => {
6890
+ setCurrentValue(this.sessionSubject, (session) => {
6891
+ if (!session || session.id !== ringState.session_id)
6892
+ return session;
6893
+ return {
6894
+ ...session,
6895
+ accepted_by: ringState.accepted_by,
6896
+ rejected_by: ringState.rejected_by,
6897
+ missed_by: ringState.missed_by,
6898
+ ended_at: ringState.session_ended_at ?? session.ended_at,
6899
+ };
6900
+ });
6901
+ if (ringState.call_ended_at) {
6902
+ this.setEndedAt(new Date(ringState.call_ended_at));
6903
+ }
7136
6904
  };
7137
6905
  /**
7138
6906
  * Updates the call state with the data received from the SFU server.
@@ -7170,21 +6938,21 @@ class CallState {
7170
6938
  this.setAnonymousParticipantCount(participantCount?.anonymous || 0);
7171
6939
  this.setStartedAt(startedAt ? Timestamp.toDate(startedAt) : new Date());
7172
6940
  this.setServerSidePins(pins);
7173
- this.setCurrentValue(this.e2eeEnabledSubject, e2EeEnabled);
6941
+ setCurrentValue(this.e2eeEnabledSubject, e2EeEnabled);
7174
6942
  };
7175
6943
  updateFromMemberRemoved = (event) => {
7176
6944
  this.updateFromCallResponse(event.call);
7177
- this.setCurrentValue(this.membersSubject, (members) => members.filter((m) => event.members.indexOf(m.user_id) === -1));
6945
+ setCurrentValue(this.membersSubject, (members) => members.filter((m) => event.members.indexOf(m.user_id) === -1));
7178
6946
  };
7179
6947
  updateFromMemberAdded = (event) => {
7180
6948
  this.updateFromCallResponse(event.call);
7181
- this.setCurrentValue(this.membersSubject, (members) => [
6949
+ setCurrentValue(this.membersSubject, (members) => [
7182
6950
  ...members,
7183
6951
  ...event.members,
7184
6952
  ]);
7185
6953
  };
7186
6954
  updateFromHLSBroadcastStopped = () => {
7187
- this.setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
6955
+ setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
7188
6956
  ...egress,
7189
6957
  broadcasting: false,
7190
6958
  hls: {
@@ -7194,7 +6962,7 @@ class CallState {
7194
6962
  }));
7195
6963
  };
7196
6964
  updateFromHLSBroadcastingFailed = () => {
7197
- this.setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
6965
+ setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
7198
6966
  ...egress,
7199
6967
  broadcasting: false,
7200
6968
  hls: {
@@ -7206,13 +6974,13 @@ class CallState {
7206
6974
  updateFromRecordingEvent = (type, running) => {
7207
6975
  // handle the legacy format, where `type` is absent in the emitted events
7208
6976
  if (type === undefined || type === 'composite') {
7209
- this.setCurrentValue(this.recordingSubject, running);
6977
+ setCurrentValue(this.recordingSubject, running);
7210
6978
  }
7211
6979
  else if (type === 'individual') {
7212
- this.setCurrentValue(this.individualRecordingSubject, running);
6980
+ setCurrentValue(this.individualRecordingSubject, running);
7213
6981
  }
7214
6982
  else if (type === 'raw') {
7215
- this.setCurrentValue(this.rawRecordingSubject, running);
6983
+ setCurrentValue(this.rawRecordingSubject, running);
7216
6984
  }
7217
6985
  else {
7218
6986
  ensureExhausted(type, 'Unknown recording type');
@@ -7229,7 +6997,7 @@ class CallState {
7229
6997
  this.setAnonymousParticipantCount(session.anonymous_participant_count || 0);
7230
6998
  };
7231
6999
  updateFromSessionParticipantCountUpdate = (event) => {
7232
- const s = this.setCurrentValue(this.sessionSubject, (session) => {
7000
+ const s = setCurrentValue(this.sessionSubject, (session) => {
7233
7001
  if (!session)
7234
7002
  return session;
7235
7003
  return {
@@ -7241,7 +7009,7 @@ class CallState {
7241
7009
  this.updateParticipantCountFromSession(s);
7242
7010
  };
7243
7011
  updateFromSessionParticipantLeft = (event) => {
7244
- const s = this.setCurrentValue(this.sessionSubject, (session) => {
7012
+ const s = setCurrentValue(this.sessionSubject, (session) => {
7245
7013
  if (!session)
7246
7014
  return session;
7247
7015
  const { participants, participants_count_by_role } = session;
@@ -7258,7 +7026,7 @@ class CallState {
7258
7026
  this.updateParticipantCountFromSession(s);
7259
7027
  };
7260
7028
  updateFromSessionParticipantJoined = (event) => {
7261
- const s = this.setCurrentValue(this.sessionSubject, (session) => {
7029
+ const s = setCurrentValue(this.sessionSubject, (session) => {
7262
7030
  if (!session)
7263
7031
  return session;
7264
7032
  const { participants, participants_count_by_role } = session;
@@ -7295,7 +7063,7 @@ class CallState {
7295
7063
  };
7296
7064
  updateMembers = (event) => {
7297
7065
  this.updateFromCallResponse(event.call);
7298
- this.setCurrentValue(this.membersSubject, (members) => members.map((member) => {
7066
+ setCurrentValue(this.membersSubject, (members) => members.map((member) => {
7299
7067
  const memberUpdate = event.members.find((m) => m.user_id === member.user_id);
7300
7068
  return memberUpdate ? memberUpdate : member;
7301
7069
  }));
@@ -7320,14 +7088,14 @@ class CallState {
7320
7088
  });
7321
7089
  };
7322
7090
  unblockUser = (event) => {
7323
- this.setCurrentValue(this.blockedUserIdsSubject, (current) => {
7091
+ setCurrentValue(this.blockedUserIdsSubject, (current) => {
7324
7092
  if (!current)
7325
7093
  return current;
7326
7094
  return current.filter((id) => id !== event.user.id);
7327
7095
  });
7328
7096
  };
7329
7097
  blockUser = (event) => {
7330
- this.setCurrentValue(this.blockedUserIdsSubject, (current) => [
7098
+ setCurrentValue(this.blockedUserIdsSubject, (current) => [
7331
7099
  ...(current || []),
7332
7100
  event.user.id,
7333
7101
  ]);
@@ -7338,7 +7106,7 @@ class CallState {
7338
7106
  }
7339
7107
  };
7340
7108
  updateFromClosedCaptions = (event) => {
7341
- this.setCurrentValue(this.closedCaptionsSubject, (queue) => {
7109
+ setCurrentValue(this.closedCaptionsSubject, (queue) => {
7342
7110
  const { closed_caption } = event;
7343
7111
  const keyOf = (c) => `${c.speaker_id}/${c.start_time}`;
7344
7112
  const currentKey = keyOf(closed_caption);
@@ -7350,7 +7118,7 @@ class CallState {
7350
7118
  // schedule the removal of the closed caption after the retention time
7351
7119
  if (visibilityDurationMs > 0) {
7352
7120
  const taskId = setTimeout(() => {
7353
- this.setCurrentValue(this.closedCaptionsSubject, (captions) => captions.filter((caption) => caption !== closed_caption));
7121
+ setCurrentValue(this.closedCaptionsSubject, (captions) => captions.filter((caption) => caption !== closed_caption));
7354
7122
  this.closedCaptionsTasks.delete(currentKey);
7355
7123
  }, visibilityDurationMs);
7356
7124
  this.closedCaptionsTasks.set(currentKey, taskId);
@@ -7445,6 +7213,19 @@ const getSdkName = (sdk) => {
7445
7213
  const getSdkVersion = (sdk) => {
7446
7214
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
7447
7215
  };
7216
+ /**
7217
+ * Returns the identifier the SFU receives in the `X-Stream-Client` header,
7218
+ * e.g. `stream-video-react-v1.43.1`. Matches the format the coordinator
7219
+ * already gets through the client's user agent.
7220
+ */
7221
+ const getStreamClientId = (sdk) => {
7222
+ const sdkPlatforms = {
7223
+ [SdkType.REACT]: 'react',
7224
+ [SdkType.REACT_NATIVE]: 'react-native',
7225
+ };
7226
+ const platform = (sdk && sdkPlatforms[sdk.type]) || 'js';
7227
+ return `stream-video-${platform}-v${getSdkVersion(sdk)}`;
7228
+ };
7448
7229
 
7449
7230
  /**
7450
7231
  * Creates a new StatsReporter instance that collects metrics about the ongoing call and reports them to the state store
@@ -9129,6 +8910,25 @@ class TransceiverCache {
9129
8910
  this.cache.push(bundle);
9130
8911
  this.transceiverOrder.push(bundle.transceiver);
9131
8912
  };
8913
+ /**
8914
+ * Retires a failed sender from the reusable cache.
8915
+ *
8916
+ * A transceiver that never reached an m-section also leaves the creation
8917
+ * history: once stopped, it is left out of every later offer, so counting
8918
+ * it would shift the `mid` index of every transceiver added after it. One
8919
+ * that is already associated keeps its slot, since its m-section survives
8920
+ * as a rejected one.
8921
+ */
8922
+ remove = (transceiver) => {
8923
+ const index = this.cache.findIndex((b) => b.transceiver === transceiver);
8924
+ if (index !== -1)
8925
+ this.cache.splice(index, 1);
8926
+ if (transceiver.mid)
8927
+ return;
8928
+ const orderIndex = this.transceiverOrder.indexOf(transceiver);
8929
+ if (orderIndex !== -1)
8930
+ this.transceiverOrder.splice(orderIndex, 1);
8931
+ };
9132
8932
  /**
9133
8933
  * Gets the transceiver for the given publish option.
9134
8934
  */
@@ -9300,7 +9100,9 @@ class Publisher extends BasePeerConnection {
9300
9100
  const sendEncodings = isSvcCodec(publishOption.codec?.name)
9301
9101
  ? toSvcEncodings(encodings)
9302
9102
  : encodings;
9303
- const transceiver = this.pc.addTransceiver(track, {
9103
+ // An E2EE sender must never have media before its transform is attached,
9104
+ // including when another track or an ICE restart negotiates this PC.
9105
+ const transceiver = this.pc.addTransceiver(this.e2ee ? track.kind : track, {
9304
9106
  direction: 'sendonly',
9305
9107
  sendEncodings,
9306
9108
  });
@@ -9310,16 +9112,63 @@ class Publisher extends BasePeerConnection {
9310
9112
  this.transceiverCache.add({ publishOption, transceiver, options });
9311
9113
  this.trackIdToTrackType.set(track.id, trackType);
9312
9114
  this.logger.debug(`Added ${TrackType[trackType]} transceiver`);
9313
- const params = transceiver.sender.getParameters();
9115
+ if (this.e2ee) {
9116
+ await this.initEncryptedSender(transceiver, track, publishOption);
9117
+ }
9118
+ else {
9119
+ await this.setDegradationPreference(transceiver.sender, publishOption);
9120
+ }
9121
+ await this.negotiate();
9122
+ };
9123
+ /**
9124
+ * Attaches the encryptor, applies sender parameters, then attaches the track.
9125
+ *
9126
+ * `encrypt` must run before the first await: Chrome short-circuits a sender
9127
+ * whose encoded streams aren't created in the task that created it, and the
9128
+ * track goes on last so the sender has no media until both steps succeed.
9129
+ */
9130
+ initEncryptedSender = async (transceiver, track, publishOption) => {
9131
+ try {
9132
+ const { sender } = transceiver;
9133
+ const codec = publishOption.codec?.name.toLowerCase();
9134
+ this.e2ee.encrypt(sender, codec, TrackType[publishOption.trackType]);
9135
+ this.logger.debug('E2EE encryptor attached to sender');
9136
+ await this.setDegradationPreference(sender, publishOption);
9137
+ await sender.replaceTrack(track);
9138
+ }
9139
+ catch (err) {
9140
+ this.retireTransceiver(transceiver, track);
9141
+ throw err;
9142
+ }
9143
+ };
9144
+ setDegradationPreference = async (sender, publishOption) => {
9145
+ const params = sender.getParameters();
9314
9146
  params.degradationPreference =
9315
9147
  toRTCDegradationPreference(publishOption.degradationPreference) ??
9316
9148
  'maintain-framerate';
9317
- await transceiver.sender.setParameters(params);
9318
- if (this.e2ee) {
9319
- this.e2ee.encrypt(transceiver.sender, publishOption.codec?.name.toLowerCase(), TrackType[publishOption.trackType]);
9320
- this.logger.debug('E2EE encryptor attached to sender');
9149
+ await sender.setParameters(params);
9150
+ };
9151
+ /**
9152
+ * Discards a sender whose E2EE initialization failed.
9153
+ *
9154
+ * A partially initialized sender may carry no transform, so publish() must
9155
+ * not reuse it: retiring it from the cache is what keeps a later publish or
9156
+ * ICE restart from putting cleartext on the wire. Cleanup cannot throw, so
9157
+ * the initialization failure is what reaches the caller.
9158
+ */
9159
+ retireTransceiver = (transceiver, track) => {
9160
+ this.transceiverCache.remove(transceiver);
9161
+ this.trackIdToTrackType.delete(track.id);
9162
+ // on React Native a clone shares its native source, so stopping it here
9163
+ // would stop the track the caller passed in. dispose() releases it.
9164
+ if (!isReactNative())
9165
+ this.stopTrack(track);
9166
+ try {
9167
+ transceiver.stop();
9168
+ }
9169
+ catch (err) {
9170
+ this.logger.debug('Failed to stop a retired transceiver', err);
9321
9171
  }
9322
- await this.negotiate();
9323
9172
  };
9324
9173
  /**
9325
9174
  * Updates the transceiver with the given track and track type.
@@ -10326,7 +10175,10 @@ class StreamSfuClient {
10326
10175
  this.rpc = createSignalClient({
10327
10176
  baseUrl: server.url,
10328
10177
  interceptors: [
10329
- withHeaders({ Authorization: `Bearer ${token}` }),
10178
+ withHeaders({
10179
+ Authorization: `Bearer ${token}`,
10180
+ 'X-Stream-Client': getStreamClientId(getSdkInfo()),
10181
+ }),
10330
10182
  this.tracer && withRequestTracer(this.tracer.trace),
10331
10183
  this.logger.getLogLevel() === 'trace' && withRequestLogger(this.logger),
10332
10184
  withTimeout(rpcRequestTimeout, this.tracer?.trace),
@@ -10629,98 +10481,404 @@ class StreamSfuClient {
10629
10481
  },
10630
10482
  }));
10631
10483
  };
10632
- send = async (message) => {
10633
- await this.signalReady(); // wait for the signal ws to be open
10634
- const msgJson = SfuRequest.toJson(message);
10635
- if (this.signalWs.readyState !== WebSocket.OPEN) {
10636
- this.logger.debug('Signal WS is not open. Skipping message', msgJson);
10484
+ send = async (message) => {
10485
+ await this.signalReady(); // wait for the signal ws to be open
10486
+ const msgJson = SfuRequest.toJson(message);
10487
+ if (this.signalWs.readyState !== WebSocket.OPEN) {
10488
+ this.logger.debug('Signal WS is not open. Skipping message', msgJson);
10489
+ return;
10490
+ }
10491
+ this.logger.debug(`Sending message to: ${this.edgeName}`, msgJson);
10492
+ this.signalWs.send(SfuRequest.toBinary(message));
10493
+ };
10494
+ keepAlive = () => {
10495
+ const timers = getTimers();
10496
+ timers.clearInterval(this.keepAliveInterval);
10497
+ this.keepAliveInterval = timers.setInterval(() => {
10498
+ this.ping().catch((e) => {
10499
+ this.logger.error('Error sending healthCheckRequest to SFU', e);
10500
+ });
10501
+ }, this.pingIntervalInMs);
10502
+ };
10503
+ scheduleConnectionCheck = () => {
10504
+ const timers = getTimers();
10505
+ timers.clearInterval(this.connectionCheckInterval);
10506
+ this.connectionCheckInterval = timers.setInterval(() => {
10507
+ if (this.lastMessageTimestamp) {
10508
+ const timeSinceLastMessage = Date.now() - this.lastMessageTimestamp;
10509
+ if (timeSinceLastMessage > this.unhealthyTimeoutInMs) {
10510
+ this.close(StreamSfuClient.ERROR_CONNECTION_UNHEALTHY, `SFU connection unhealthy. Didn't receive any message for ${this.unhealthyTimeoutInMs}ms`);
10511
+ }
10512
+ }
10513
+ }, this.connectionCheckIntervalInMs);
10514
+ };
10515
+ }
10516
+
10517
+ /**
10518
+ * Decides what a ringing call should do next, based on the current call state.
10519
+ *
10520
+ * The `call.accepted`, `call.rejected` and `call.missed` handlers and the ring
10521
+ * state poller both run this. They differ only in how the state got there: the
10522
+ * handlers rely on `CallState.updateFromEvent`, which runs before them, and the
10523
+ * poller applies the polled ring state itself.
10524
+ *
10525
+ * @param call the call to reconcile.
10526
+ * @param joinSource which of the two triggered this run, reported on the
10527
+ * caller's join: `ring-ws` for the event handlers, `ring-poll-api` for the
10528
+ * poller.
10529
+ * @returns whether the ring reached a terminal state. A failed join is not
10530
+ * terminal: the caller should keep trying while the ring is open.
10531
+ */
10532
+ const reconcileRingState = async (call, joinSource) => {
10533
+ if (call.state.callingState !== exports.CallingState.RINGING)
10534
+ return true;
10535
+ return call.isCreatedByMe
10536
+ ? reconcileAsCaller(call, joinSource)
10537
+ : reconcileAsCallee(call);
10538
+ };
10539
+ const reconcileAsCaller = async (call, joinSource) => {
10540
+ const { session, members, endedAt } = call.state;
10541
+ const currentUserId = call.currentUserId;
10542
+ // checked before `accepted_by`: an ended session cannot be joined
10543
+ if (endedAt || session?.ended_at) {
10544
+ call.logger.info('ring: the call has ended, leaving');
10545
+ // `leave` reports the remote end to callingx off `reason: 'ended'`
10546
+ return leave(call, {
10547
+ reject: false,
10548
+ reason: 'ended',
10549
+ message: 'ring: call ended',
10550
+ });
10551
+ }
10552
+ const acceptedBy = session?.accepted_by ?? {};
10553
+ if (Object.keys(acceptedBy).some((userId) => userId !== currentUserId)) {
10554
+ call.logger.info('ring: the call was accepted, joining');
10555
+ try {
10556
+ await call.join({ joinSource });
10557
+ }
10558
+ catch (err) {
10559
+ // `doJoin` restores the ringing state when a join fails, so the ring is
10560
+ // still open. Report it unsettled and let the next poll retry.
10561
+ call.logger.error('Failed to join an accepted call', err);
10562
+ return false;
10563
+ }
10564
+ return true;
10565
+ }
10566
+ const otherMembers = members
10567
+ .filter((member) => member.user_id !== currentUserId)
10568
+ .map((member) => member.user_id);
10569
+ if (otherMembers.length === 0)
10570
+ return false;
10571
+ const rejectedBy = session?.rejected_by ?? {};
10572
+ if (otherMembers.every((userId) => rejectedBy[userId])) {
10573
+ call.logger.info('ring: everyone rejected, leaving');
10574
+ return leave(call, {
10575
+ reject: true,
10576
+ reason: 'cancel',
10577
+ message: 'ring: everyone rejected',
10578
+ });
10579
+ }
10580
+ return false;
10581
+ };
10582
+ // the current user's own accept or reject, on this or another device, is
10583
+ // handled by `resolveOwnRingOutcome`, and `call.ended` by `watchCallEnded`.
10584
+ const reconcileAsCallee = async (call) => {
10585
+ const createdById = call.state.createdBy?.id;
10586
+ const rejectedBy = call.state.session?.rejected_by ?? {};
10587
+ if (createdById && rejectedBy[createdById]) {
10588
+ call.logger.info('ring: the caller cancelled, leaving');
10589
+ return leave(call, {
10590
+ reason: 'ended',
10591
+ message: 'ring: creator rejected',
10592
+ });
10593
+ }
10594
+ return false;
10595
+ };
10596
+ // `false` when the call could not be left, so the ring stays open for a retry.
10597
+ const leave = async (call, options) => {
10598
+ try {
10599
+ await call.leave(options);
10600
+ return true;
10601
+ }
10602
+ catch (err) {
10603
+ call.logger.error('Failed to leave a ringing call', err);
10604
+ return false;
10605
+ }
10606
+ };
10607
+
10608
+ // `doJoin` restores the previous calling state when a join fails, so JOINING can
10609
+ // go back to RINGING. Only a state the ring cannot return from ends the poller.
10610
+ const ringIsOver = (callingState) => callingState !== exports.CallingState.RINGING &&
10611
+ callingState !== exports.CallingState.JOINING;
10612
+ /**
10613
+ * Polls the coordinator for the outcome of a ring the current user started.
10614
+ *
10615
+ * `call.accepted`, `call.rejected` and `call.missed` are delivered best-effort,
10616
+ * with no store-and-forward, so a caller that drops one is left on a ringing
10617
+ * screen while the callee is already in the call. After a quiet period this
10618
+ * reads the ring state until a terminal outcome or the end of the ring window.
10619
+ */
10620
+ class RingStatePoller {
10621
+ logger = videoLoggerSystem.getLogger('RingStatePoller');
10622
+ call;
10623
+ startAfterMs;
10624
+ intervalMs;
10625
+ sessionId;
10626
+ deadlineAt = 0;
10627
+ idleTimeoutId;
10628
+ intervalId;
10629
+ stopped = false;
10630
+ wasPolling = false;
10631
+ inFlight = false;
10632
+ unsubscribe = [];
10633
+ constructor(call, options = {}) {
10634
+ this.call = call;
10635
+ this.startAfterMs = options.startAfterMs ?? 15_000;
10636
+ this.intervalMs = options.intervalMs ?? 5_000;
10637
+ }
10638
+ /**
10639
+ * Starts polling. Does nothing when the call has no session yet.
10640
+ */
10641
+ start = () => {
10642
+ if (this.stopped || this.sessionId)
10643
+ return;
10644
+ if (this.call.state.callingState !== exports.CallingState.RINGING)
10645
+ return;
10646
+ // captured once: `call.ended` clears the call's current session
10647
+ const sessionId = this.call.state.session?.id;
10648
+ if (!sessionId) {
10649
+ this.logger.warn('the call has no session');
10650
+ return;
10651
+ }
10652
+ this.sessionId = sessionId;
10653
+ const ring = this.call.state.settings?.ring;
10654
+ const maxDurationMs = ring?.auto_cancel_timeout_ms || ring?.missed_call_timeout_ms || 30_000;
10655
+ this.deadlineAt = Date.now() + maxDurationMs;
10656
+ // an incoming event proves the socket is alive, so the quiet period starts
10657
+ // over: in a group ring a single rejection does not settle the ring.
10658
+ this.unsubscribe.push(this.call.on('call.accepted', () => this.armIdleWindow()), this.call.on('call.rejected', () => this.armIdleWindow()), this.call.on('call.missed', () => this.armIdleWindow()),
10659
+ // the ring is over, whichever way it went. Joining an accepted call
10660
+ // never goes through `leave`, so this is the only signal for it.
10661
+ createSubscription(this.call.state.callingState$, (callingState) => {
10662
+ if (ringIsOver(callingState))
10663
+ this.stop();
10664
+ }));
10665
+ this.armIdleWindow();
10666
+ };
10667
+ /**
10668
+ * Pauses polling, keeping the captured session, the deadline and the event
10669
+ * subscriptions. Resuming does not extend the ring window.
10670
+ */
10671
+ pause = () => {
10672
+ if (this.stopped)
10673
+ return;
10674
+ this.wasPolling = this.intervalId !== undefined;
10675
+ const timers = getTimers();
10676
+ timers.clearTimeout(this.idleTimeoutId);
10677
+ timers.clearInterval(this.intervalId);
10678
+ this.idleTimeoutId = undefined;
10679
+ this.intervalId = undefined;
10680
+ };
10681
+ /**
10682
+ * Resumes a paused poller. Goes straight back to polling if it was already
10683
+ * past the quiet period, so a pause does not buy the ring another one.
10684
+ */
10685
+ resume = () => {
10686
+ if (this.stopped || !this.sessionId)
10687
+ return;
10688
+ if (this.idleTimeoutId !== undefined || this.intervalId !== undefined) {
10637
10689
  return;
10638
10690
  }
10639
- this.logger.debug(`Sending message to: ${this.edgeName}`, msgJson);
10640
- this.signalWs.send(SfuRequest.toBinary(message));
10691
+ if (!this.wasPolling) {
10692
+ this.armIdleWindow();
10693
+ return;
10694
+ }
10695
+ this.intervalId = getTimers().setInterval(this.runTick, this.intervalMs);
10696
+ this.runTick();
10641
10697
  };
10642
- keepAlive = () => {
10698
+ /**
10699
+ * Stops polling. The poller cannot be restarted.
10700
+ */
10701
+ stop = () => {
10702
+ if (this.stopped)
10703
+ return;
10704
+ this.stopped = true;
10643
10705
  const timers = getTimers();
10644
- timers.clearInterval(this.keepAliveInterval);
10645
- this.keepAliveInterval = timers.setInterval(() => {
10646
- this.ping().catch((e) => {
10647
- this.logger.error('Error sending healthCheckRequest to SFU', e);
10648
- });
10649
- }, this.pingIntervalInMs);
10706
+ timers.clearTimeout(this.idleTimeoutId);
10707
+ timers.clearInterval(this.intervalId);
10708
+ this.idleTimeoutId = undefined;
10709
+ this.intervalId = undefined;
10710
+ this.unsubscribe.forEach((off) => off());
10711
+ this.unsubscribe = [];
10650
10712
  };
10651
- scheduleConnectionCheck = () => {
10713
+ armIdleWindow = () => {
10714
+ if (this.stopped)
10715
+ return;
10652
10716
  const timers = getTimers();
10653
- timers.clearInterval(this.connectionCheckInterval);
10654
- this.connectionCheckInterval = timers.setInterval(() => {
10655
- if (this.lastMessageTimestamp) {
10656
- const timeSinceLastMessage = Date.now() - this.lastMessageTimestamp;
10657
- if (timeSinceLastMessage > this.unhealthyTimeoutInMs) {
10658
- this.close(StreamSfuClient.ERROR_CONNECTION_UNHEALTHY, `SFU connection unhealthy. Didn't receive any message for ${this.unhealthyTimeoutInMs}ms`);
10659
- }
10660
- }
10661
- }, this.connectionCheckIntervalInMs);
10717
+ timers.clearTimeout(this.idleTimeoutId);
10718
+ timers.clearInterval(this.intervalId);
10719
+ this.intervalId = undefined;
10720
+ this.idleTimeoutId = timers.setTimeout(() => {
10721
+ this.idleTimeoutId = undefined;
10722
+ if (this.stopped)
10723
+ return;
10724
+ this.intervalId = timers.setInterval(this.runTick, this.intervalMs);
10725
+ this.runTick();
10726
+ }, this.startAfterMs);
10662
10727
  };
10663
- }
10664
-
10665
- /**
10666
- * Event handler that watched the delivery of `call.accepted`.
10667
- * Once the event is received, the call is joined.
10668
- */
10669
- const watchCallAccepted = (call) => {
10670
- return async function onCallAccepted(event) {
10671
- // We want to discard the event if it's from the current user
10672
- if (event.user.id === call.currentUserId)
10673
- return;
10674
- const { state } = call;
10675
- if (event.call.created_by.id === call.currentUserId &&
10676
- state.callingState === exports.CallingState.RINGING) {
10677
- await call.join();
10678
- }
10728
+ runTick = () => {
10729
+ this.tick().catch((err) => {
10730
+ this.logger.warn('Failed to poll the ring state', err);
10731
+ });
10679
10732
  };
10680
- };
10681
- /**
10682
- * Event handler that watches delivery of `call.rejected` Websocket event.
10683
- * Once the event is received, the call is left.
10684
- */
10685
- const watchCallRejected = (call) => {
10686
- return async function onCallRejected(event) {
10687
- // We want to discard the event if it's from the current user
10688
- if (event.user.id === call.currentUserId)
10733
+ tick = async () => {
10734
+ if (this.stopped || this.inFlight || !this.sessionId)
10689
10735
  return;
10690
- const { call: eventCall } = event;
10691
- const { session: callSession } = eventCall;
10692
- if (!callSession) {
10693
- call.logger.warn('No call session provided. Ignoring call.rejected event.', event);
10736
+ const { callingState } = this.call.state;
10737
+ if (ringIsOver(callingState) || Date.now() >= this.deadlineAt) {
10738
+ this.stop();
10694
10739
  return;
10695
10740
  }
10696
- const rejectedBy = callSession.rejected_by;
10697
- const { members, callingState } = call.state;
10698
- if (callingState !== exports.CallingState.RINGING) {
10699
- call.logger.info('Call is not in ringing mode (it is either accepted or rejected already). Ignoring call.rejected event.', event);
10741
+ // mid-join: nothing to reconcile until we know whether it succeeded
10742
+ if (callingState !== exports.CallingState.RINGING)
10700
10743
  return;
10744
+ this.inFlight = true;
10745
+ try {
10746
+ const ringState = await this.call.getRingState(this.sessionId);
10747
+ if (this.stopped)
10748
+ return;
10749
+ this.call.state.updateFromRingState(ringState);
10750
+ if (await reconcileRingState(this.call, 'ring-poll-api'))
10751
+ this.stop();
10701
10752
  }
10702
- if (call.isCreatedByMe) {
10703
- const everyoneElseRejected = members
10704
- .filter((m) => m.user_id !== call.currentUserId)
10705
- .every((m) => rejectedBy[m.user_id]);
10706
- if (everyoneElseRejected) {
10707
- call.logger.info('everyone rejected, leaving the call');
10708
- await call.leave({
10709
- reject: true,
10710
- reason: 'cancel',
10711
- message: 'ring: everyone rejected',
10712
- });
10753
+ catch (err) {
10754
+ // a missing session, or one of another call, will never resolve
10755
+ const status = err instanceof ErrorFromResponse ? err.status : undefined;
10756
+ if (status === 400 || status === 404) {
10757
+ this.logger.warn('Stopped polling the ring state', err);
10758
+ this.stop();
10713
10759
  }
10714
- }
10715
- else {
10716
- if (rejectedBy[eventCall.created_by.id]) {
10717
- call.logger.info('call creator rejected, leaving call');
10718
- globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
10719
- await call.leave({ message: 'ring: creator rejected' });
10760
+ else {
10761
+ this.logger.debug('Failed to poll the ring state', err);
10720
10762
  }
10721
10763
  }
10764
+ finally {
10765
+ this.inFlight = false;
10766
+ }
10767
+ };
10768
+ }
10769
+
10770
+ /**
10771
+ * Drops a call that has been ringing for too long: the caller cancels after
10772
+ * `auto_cancel_timeout_ms`, a callee after `incoming_call_timeout_ms`.
10773
+ *
10774
+ * Either timeout being `0` means the call rings until something else settles it.
10775
+ */
10776
+ class RingTimeout {
10777
+ call;
10778
+ timeoutId;
10779
+ deadlineAt;
10780
+ stopped = false;
10781
+ constructor(call) {
10782
+ this.call = call;
10783
+ }
10784
+ /**
10785
+ * Schedules an auto-drop timeout based on the call settings.
10786
+ * Applicable only for ringing calls.
10787
+ */
10788
+ start = () => {
10789
+ if (this.stopped || this.timeoutId !== undefined)
10790
+ return;
10791
+ // ignore if the call is not ringing
10792
+ if (this.call.state.callingState !== exports.CallingState.RINGING)
10793
+ return;
10794
+ const ring = this.call.state.settings?.ring;
10795
+ if (!ring)
10796
+ return;
10797
+ const isCaller = this.call.isCreatedByMe;
10798
+ const timeoutMs = isCaller
10799
+ ? ring.auto_cancel_timeout_ms
10800
+ : ring.incoming_call_timeout_ms;
10801
+ // 0 means no auto-drop
10802
+ if (timeoutMs <= 0)
10803
+ return;
10804
+ const now = Date.now();
10805
+ this.deadlineAt ??= now + timeoutMs;
10806
+ const delayMs = Math.max(0, this.deadlineAt - now);
10807
+ const timers = getTimers();
10808
+ this.timeoutId = timers.setTimeout(() => {
10809
+ this.timeoutId = undefined;
10810
+ // A failed timeout-triggered leave can arm a fresh retry window.
10811
+ this.deadlineAt = undefined;
10812
+ if (this.stopped)
10813
+ return;
10814
+ // the call might have stopped ringing by this point, e.g. it was already
10815
+ // accepted and joined
10816
+ if (this.call.state.callingState !== exports.CallingState.RINGING)
10817
+ return;
10818
+ this.call
10819
+ .leave({
10820
+ reject: true,
10821
+ reason: 'timeout',
10822
+ message: `ringing timeout - ${isCaller
10823
+ ? 'no one accepted'
10824
+ : `user didn't interact with incoming call screen`}`,
10825
+ })
10826
+ .catch((err) => {
10827
+ videoLoggerSystem
10828
+ .getLogger('RingTimeout')
10829
+ .error('Failed to drop the call', err);
10830
+ });
10831
+ }, delayMs);
10832
+ };
10833
+ /**
10834
+ * Pauses the timeout while preserving its original deadline.
10835
+ */
10836
+ pause = () => {
10837
+ if (this.stopped || this.timeoutId === undefined)
10838
+ return;
10839
+ getTimers().clearTimeout(this.timeoutId);
10840
+ this.timeoutId = undefined;
10841
+ };
10842
+ /**
10843
+ * Cancels a scheduled auto-drop timeout. It cannot be armed again.
10844
+ */
10845
+ stop = () => {
10846
+ if (this.stopped)
10847
+ return;
10848
+ this.stopped = true;
10849
+ getTimers().clearTimeout(this.timeoutId);
10850
+ this.timeoutId = undefined;
10851
+ this.deadlineAt = undefined;
10852
+ };
10853
+ }
10854
+
10855
+ /**
10856
+ * Decides what a ringing call should do about the current user's own accept or
10857
+ * reject, which may have happened on another device.
10858
+ *
10859
+ * Accepting on *this* device also lands in `accepted_by`, so an acceptance only
10860
+ * means another device took the call when this one is still ringing.
10861
+ *
10862
+ * Only meaningful for a ringing call; the caller checks that.
10863
+ */
10864
+ const resolveOwnRingOutcome = ({ session, currentUserId, callingState, }) => {
10865
+ if (!currentUserId)
10866
+ return { settledByMe: false };
10867
+ const acceptedByMe = Boolean(session?.accepted_by[currentUserId]);
10868
+ const rejectedByMe = Boolean(session?.rejected_by[currentUserId]);
10869
+ if (!acceptedByMe && !rejectedByMe)
10870
+ return { settledByMe: false };
10871
+ const answeredElsewhere = acceptedByMe && callingState === exports.CallingState.RINGING;
10872
+ return {
10873
+ settledByMe: true,
10874
+ leaveReason: answeredElsewhere
10875
+ ? 'answeredElsewhere'
10876
+ : rejectedByMe
10877
+ ? 'rejected'
10878
+ : undefined,
10722
10879
  };
10723
10880
  };
10881
+
10724
10882
  /**
10725
10883
  * Event handler that watches the delivery of `call.ended` Websocket event.
10726
10884
  */
@@ -11121,14 +11279,18 @@ const registerEventHandlers = (call, dispatcher) => {
11121
11279
  * @param call the call to register event handlers for.
11122
11280
  */
11123
11281
  const registerRingingCallEventHandlers = (call) => {
11124
- const coordinatorRingEvents = {
11125
- 'call.accepted': watchCallAccepted(call),
11126
- 'call.rejected': watchCallRejected(call),
11282
+ const reconcile = () => {
11283
+ reconcileRingState(call, 'ring-ws').catch((err) => {
11284
+ call.logger.error('Failed to reconcile the ring state', err);
11285
+ });
11127
11286
  };
11128
- const eventHandlers = Object.keys(coordinatorRingEvents).map((event) => {
11129
- const eventName = event;
11130
- return call.on(eventName, coordinatorRingEvents[eventName]);
11131
- });
11287
+ // each event needs its own closure. `call.missed` is deliberately absent:
11288
+ // nothing in the reconciler acts on `missed_by`, so the auto-drop owns the
11289
+ // "nobody answered" case until the server-owned ring timeout lands.
11290
+ const eventHandlers = [
11291
+ call.on('call.accepted', () => reconcile()),
11292
+ call.on('call.rejected', () => reconcile()),
11293
+ ];
11132
11294
  return () => {
11133
11295
  eventHandlers.forEach((unsubscribe) => unsubscribe());
11134
11296
  };
@@ -15176,8 +15338,9 @@ class Call {
15176
15338
  statsReporter;
15177
15339
  sfuStatsReporter;
15178
15340
  lastStatsOptions;
15179
- dropTimeout;
15180
- clientStore;
15341
+ ringTimeout;
15342
+ ringStatePoller;
15343
+ clientState;
15181
15344
  streamClient;
15182
15345
  clientEventReporter;
15183
15346
  sfuClient;
@@ -15244,7 +15407,7 @@ class Call {
15244
15407
  * Use the [`StreamVideoClient.call`](./StreamVideoClient.md/#call)
15245
15408
  * method to construct a `Call` instance.
15246
15409
  */
15247
- constructor({ type, id, streamClient, clientEventReporter, members, ownCapabilities, sortParticipantsBy, clientStore, ringing = false, watching = false, }) {
15410
+ constructor({ type, id, streamClient, clientEventReporter, members, ownCapabilities, sortParticipantsBy, clientState, ringing = false, watching = false, }) {
15248
15411
  this.type = type;
15249
15412
  this.id = id;
15250
15413
  this.cid = `${type}:${id}`;
@@ -15252,7 +15415,7 @@ class Call {
15252
15415
  this.watching = watching;
15253
15416
  this.streamClient = streamClient;
15254
15417
  this.clientEventReporter = clientEventReporter;
15255
- this.clientStore = clientStore;
15418
+ this.clientState = clientState;
15256
15419
  this.streamClientBasePath = `/call/${this.type}/${this.id}`;
15257
15420
  this.logger = videoLoggerSystem.getLogger('Call');
15258
15421
  const callTypeConfig = CallTypes.get(type);
@@ -15354,27 +15517,24 @@ class Call {
15354
15517
  createSubscription(this.state.session$, (session) => {
15355
15518
  if (!this.ringing)
15356
15519
  return;
15357
- const receiverId = this.clientStore.connectedUser?.id;
15358
- if (!receiverId)
15359
- return;
15360
- const isAcceptedByMe = Boolean(session?.accepted_by[receiverId]);
15361
- const isRejectedByMe = Boolean(session?.rejected_by[receiverId]);
15362
- if (isAcceptedByMe || isRejectedByMe) {
15520
+ const { settledByMe, leaveReason } = resolveOwnRingOutcome({
15521
+ session,
15522
+ currentUserId: this.currentUserId,
15523
+ callingState: this.state.callingState,
15524
+ });
15525
+ if (settledByMe)
15363
15526
  this.cancelAutoDrop();
15364
- }
15365
- const isAcceptedElsewhere = isAcceptedByMe && this.state.callingState === exports.CallingState.RINGING;
15366
- if ((isAcceptedElsewhere || isRejectedByMe) &&
15367
- !hasPending(this.joinLeaveConcurrencyTag)) {
15368
- globalThis.streamRNVideoSDK?.callingX?.endCall(this, isAcceptedElsewhere ? 'answeredElsewhere' : 'rejected');
15369
- this.leave().catch(() => {
15370
- this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
15371
- });
15372
- }
15527
+ if (!leaveReason || hasPending(this.joinLeaveConcurrencyTag))
15528
+ return;
15529
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, leaveReason);
15530
+ this.leave().catch(() => {
15531
+ this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
15532
+ });
15373
15533
  }));
15374
15534
  };
15375
15535
  handleRingingCall = () => {
15376
15536
  const callSession = this.state.session;
15377
- const receiver_id = this.clientStore.connectedUser?.id;
15537
+ const receiver_id = this.clientState.connectedUser?.id;
15378
15538
  const ended_at = callSession?.ended_at;
15379
15539
  const created_by_id = this.state.createdBy?.id;
15380
15540
  if (this.currentUserId && created_by_id === this.currentUserId) {
@@ -15415,6 +15575,7 @@ class Call {
15415
15575
  this.state.setCallingState(exports.CallingState.RINGING);
15416
15576
  }
15417
15577
  this.scheduleAutoDrop();
15578
+ this.scheduleRingStatePolling();
15418
15579
  this.leaveCallHooks.add(registerRingingCallEventHandlers(this));
15419
15580
  }
15420
15581
  };
@@ -15501,6 +15662,12 @@ class Call {
15501
15662
  if (this.state.callingState === exports.CallingState.LEFT) {
15502
15663
  throw new Error('Cannot leave call that has already been left.');
15503
15664
  }
15665
+ // before the first await: the calling state stays RINGING well into the
15666
+ // teardown, so pause both watchdogs before they can race this leave. They
15667
+ // keep their deadlines, so a failed leave resumes them without handing the
15668
+ // ring another window.
15669
+ this.ringTimeout?.pause();
15670
+ this.ringStatePoller?.pause();
15504
15671
  await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
15505
15672
  const callingState = this.state.callingState;
15506
15673
  if (callingState === exports.CallingState.LEFT) {
@@ -15567,6 +15734,11 @@ class Call {
15567
15734
  this.audioBindingsWatchdog?.dispose();
15568
15735
  await this.dynascaleManager?.dispose();
15569
15736
  this.state.setCallingState(exports.CallingState.LEFT);
15737
+ // `ringingSubject` is cleared further down, so this still reads true for
15738
+ // a call that was ringing.
15739
+ if (this.ringing) {
15740
+ globalThis.streamRNVideoSDK?.ringingCallLifecycle?.onLeave(this);
15741
+ }
15570
15742
  this.state.setParticipants([]);
15571
15743
  this.state.dispose();
15572
15744
  // Reset reconnect-related accumulators so a future `call.join()` on the
@@ -15590,7 +15762,8 @@ class Call {
15590
15762
  this.unifiedSessionId = undefined;
15591
15763
  this.ringingSubject.next(false);
15592
15764
  this.cancelAutoDrop();
15593
- this.clientStore.unregisterCall(this);
15765
+ this.cancelRingStatePolling();
15766
+ this.clientState.unregisterCall(this);
15594
15767
  globalThis.streamRNVideoSDK?.callManager.stop({
15595
15768
  isRingingTypeCall: this.ringing,
15596
15769
  shouldStopCallManager: this.callManagerStarted,
@@ -15628,6 +15801,15 @@ class Call {
15628
15801
  this.logger.warn('Failed to dispose media engine', err);
15629
15802
  });
15630
15803
  }
15804
+ }).catch((err) => {
15805
+ if (!hasPending(this.joinLeaveConcurrencyTag) &&
15806
+ this.state.callingState === exports.CallingState.RINGING) {
15807
+ // resume, never re-arm: a fresh watchdog would restart the ring
15808
+ // window this leave was already most of the way through
15809
+ this.ringTimeout?.start();
15810
+ this.ringStatePoller?.resume();
15811
+ }
15812
+ throw err;
15631
15813
  });
15632
15814
  };
15633
15815
  /**
@@ -15640,7 +15822,7 @@ class Call {
15640
15822
  * Retrieves the current user ID.
15641
15823
  */
15642
15824
  get currentUserId() {
15643
- return this.clientStore.connectedUser?.id;
15825
+ return this.clientState.connectedUser?.id;
15644
15826
  }
15645
15827
  /**
15646
15828
  * A flag indicating whether self-subscription is enabled for the call.
@@ -15701,8 +15883,8 @@ class Call {
15701
15883
  this.ringingSubject.next(true);
15702
15884
  // we remove the instance from the calls list to enable the following filter in useCalls hook
15703
15885
  // const calls = useCalls().filter((c) => c.ringing);
15704
- const calls = this.clientStore.calls.filter((c) => c.cid !== this.cid);
15705
- this.clientStore.setCalls([this, ...calls]);
15886
+ const calls = this.clientState.calls.filter((c) => c.cid !== this.cid);
15887
+ this.clientState.setCalls([this, ...calls]);
15706
15888
  await this.applyDeviceConfig(settings, { publish: false });
15707
15889
  };
15708
15890
  /**
@@ -15725,15 +15907,18 @@ class Call {
15725
15907
  * @param params.video if set to true, in a ringing scenario, mobile SDKs will show "incoming video call", audio only otherwise.
15726
15908
  */
15727
15909
  get = async (params) => {
15910
+ const getLeaveGeneration = this.leaveGeneration;
15728
15911
  await this.setup();
15729
15912
  const response = await this.streamClient.get(this.streamClientBasePath, params);
15913
+ if (this.leaveGeneration !== getLeaveGeneration)
15914
+ return response;
15730
15915
  this.updateFromCallStateResponse(response);
15731
15916
  if (params?.ring) {
15732
15917
  this.ringingSubject.next(true);
15733
15918
  }
15734
15919
  if (this.streamClient._hasConnectionID()) {
15735
15920
  this.watching = true;
15736
- this.clientStore.registerOrUpdateCall(this);
15921
+ this.clientState.registerOrUpdateCall(this);
15737
15922
  }
15738
15923
  await this.applyDeviceConfig(response.call.settings, { publish: false });
15739
15924
  return response;
@@ -15752,7 +15937,7 @@ class Call {
15752
15937
  }
15753
15938
  if (this.streamClient._hasConnectionID()) {
15754
15939
  this.watching = true;
15755
- this.clientStore.registerOrUpdateCall(this);
15940
+ this.clientState.registerOrUpdateCall(this);
15756
15941
  }
15757
15942
  await this.applyDeviceConfig(response.call.settings, { publish: false });
15758
15943
  return response;
@@ -15778,6 +15963,21 @@ class Call {
15778
15963
  ring = async (data = {}) => {
15779
15964
  return this.streamClient.post(`${this.streamClientBasePath}/ring`, data);
15780
15965
  };
15966
+ /**
15967
+ * Returns who accepted, rejected or missed the ring for a call session.
15968
+ * Safe to poll: it performs no writes and emits no events.
15969
+ *
15970
+ * @param callSessionId the call session to read. Defaults to the current one.
15971
+ * Pass it explicitly to read a session that has already ended, as ending a
15972
+ * call clears its current session.
15973
+ */
15974
+ getRingState = async (callSessionId) => {
15975
+ const sessionId = callSessionId ?? this.state.session?.id;
15976
+ if (!sessionId) {
15977
+ throw new Error('Cannot read the ring state: the call has no session');
15978
+ }
15979
+ return this.streamClient.get(`${this.streamClientBasePath}/ring_state`, { call_session_id: sessionId });
15980
+ };
15781
15981
  /**
15782
15982
  * A shortcut for {@link Call.get} with `notify` parameter set to `true`.
15783
15983
  * Will send a `call.notification` event to the call members.
@@ -15816,42 +16016,80 @@ class Call {
15816
16016
  /**
15817
16017
  * Will start to watch for call related WebSocket events and initiate a call session with the server.
15818
16018
  *
16019
+ * One instance is one call flow: discard it after leaving, cancelling, or a
16020
+ * failed join, and create a fresh one for a later flow. Reconnection inside a
16021
+ * live call is handled here and needs no new instance.
16022
+ *
15819
16023
  * @returns a promise which resolves once the call join-flow has finished.
15820
16024
  */
15821
- join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
16025
+ join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, joinSource, ...data } = {}) => {
15822
16026
  const callingState = this.state.callingState;
16027
+ // Ahead of the failure boundary below on purpose: a duplicate join on a
16028
+ // live call is refused without tearing that call down.
15823
16029
  if ([exports.CallingState.JOINED, exports.CallingState.JOINING].includes(callingState)) {
15824
16030
  throw new Error(`Illegal State: call.join() shall be called only once`);
15825
16031
  }
16032
+ // snapshot before the first await: a leave() landing at any point from
16033
+ // here on supersedes this join, and the retry loop must bail out instead
16034
+ // of resurrecting a call that leave() already tore down.
16035
+ const joinLeaveGeneration = this.leaveGeneration;
16036
+ const supersededByLeave = () => this.leaveGeneration !== joinLeaveGeneration;
15826
16037
  // we need this to be set before the callingx.joinCall() is
15827
16038
  // called to avoid registering the test call in the CallKit/Telecom
15828
16039
  this.allowOwnTracksLoopback = allowOwnTracksLoopback;
15829
16040
  if (data?.ring) {
15830
16041
  this.ringingSubject.next(true);
15831
16042
  }
16043
+ // A ringing call is joined by the SDK rather than by app code, so React
16044
+ // Native prepares it from in here - there is no earlier point at which the
16045
+ // app holds the call. Read after `data.ring` above, which is what makes an
16046
+ // outgoing call ringing in the first place.
16047
+ const ringingLifecycle = this.ringing
16048
+ ? globalThis.streamRNVideoSDK?.ringingCallLifecycle
16049
+ : undefined;
15832
16050
  const callingX = globalThis.streamRNVideoSDK?.callingX;
15833
- if (callingX) {
15834
- // for Android/iOS, we need to start the call in the callingx library as soon as possible
15835
- await callingX.joinCall(this, this.clientStore.calls);
15836
- }
15837
- await this.setup();
15838
- this.clientEventReporter.registerCall(this.cid, {
15839
- callType: this.type,
15840
- callId: this.id,
15841
- getCallSessionId: () => this.state.session?.id ?? '',
15842
- getSfuId: () => this.credentials?.server.edge_name ?? '',
15843
- });
15844
- this.joinResponseTimeout = joinResponseTimeout;
15845
- this.rpcRequestTimeout = rpcRequestTimeout;
15846
- // we will count the number of join failures per SFU.
15847
- // once the number of failures reaches 2, we will piggyback on the `migrating_from`
15848
- // field to force the coordinator to provide us another SFU
15849
- const sfuJoinFailures = new Map();
15850
- const joinData = data;
15851
- maxJoinRetries = Math.max(maxJoinRetries, 1);
15852
16051
  try {
15853
- await this.clientEventReporter.withJoinLifecycle(this.cid, 'first-attempt', async () => {
16052
+ if (ringingLifecycle) {
16053
+ await ringingLifecycle.beforeJoin(this);
16054
+ if (supersededByLeave()) {
16055
+ this.logger.debug('Join superseded by leave; not joining');
16056
+ return;
16057
+ }
16058
+ }
16059
+ if (callingX) {
16060
+ // for Android/iOS, we need to start the call in the callingx library as soon as possible
16061
+ await callingX.joinCall(this, this.clientState.calls, supersededByLeave);
16062
+ if (supersededByLeave()) {
16063
+ this.logger.debug('Join superseded by leave; not setting up');
16064
+ return;
16065
+ }
16066
+ }
16067
+ await this.setup();
16068
+ if (supersededByLeave()) {
16069
+ this.logger.debug('Join superseded by leave; not registering');
16070
+ return;
16071
+ }
16072
+ this.clientEventReporter.registerCall(this.cid, {
16073
+ callType: this.type,
16074
+ callId: this.id,
16075
+ getCallSessionId: () => this.state.session?.id ?? '',
16076
+ getSfuId: () => this.credentials?.server.edge_name ?? '',
16077
+ });
16078
+ this.joinResponseTimeout = joinResponseTimeout;
16079
+ this.rpcRequestTimeout = rpcRequestTimeout;
16080
+ // we will count the number of join failures per SFU.
16081
+ // once the number of failures reaches 2, we will piggyback on the `migrating_from`
16082
+ // field to force the coordinator to provide us another SFU
16083
+ const sfuJoinFailures = new Map();
16084
+ const joinData = data;
16085
+ maxJoinRetries = Math.max(maxJoinRetries, 1);
16086
+ await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason: 'first-attempt', joinSource }, async () => {
15854
16087
  for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
16088
+ // Also covers a leave that lands during the backoff below.
16089
+ if (supersededByLeave()) {
16090
+ this.logger.debug('Join superseded by leave; not attempting');
16091
+ return;
16092
+ }
15855
16093
  try {
15856
16094
  this.logger.trace(`Joining call (${attempt})`, this.cid);
15857
16095
  await this.doJoin(data);
@@ -15860,6 +16098,10 @@ class Call {
15860
16098
  return;
15861
16099
  }
15862
16100
  catch (err) {
16101
+ if (supersededByLeave()) {
16102
+ this.logger.debug('Join superseded by leave; not retrying');
16103
+ return;
16104
+ }
15863
16105
  this.logger.warn(`Failed to join call (${attempt})`, this.cid);
15864
16106
  if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
15865
16107
  (err instanceof SfuJoinError && err.unrecoverable)) {
@@ -15889,6 +16131,9 @@ class Call {
15889
16131
  }
15890
16132
  catch (error) {
15891
16133
  callingX?.endCall(this, 'error');
16134
+ // Ends the failed ringing flow and releases what its setup installed.
16135
+ // Never rejects, so `error` is what the caller sees.
16136
+ await ringingLifecycle?.onJoinFailed(this);
15892
16137
  throw error;
15893
16138
  }
15894
16139
  });
@@ -15911,6 +16156,10 @@ class Call {
15911
16156
  // globals resolve to the call's factory. Idempotent across
15912
16157
  // reconnect/migration attempts.
15913
16158
  await this.ensureMediaFactory();
16159
+ if (supersededByLeave()) {
16160
+ this.logger.debug('Join superseded by leave; not wiring media');
16161
+ return;
16162
+ }
15914
16163
  const callingX = globalThis.streamRNVideoSDK?.callingX;
15915
16164
  if (callingX) {
15916
16165
  callingX.wireAudioEngineSubscription();
@@ -15926,11 +16175,17 @@ class Call {
15926
16175
  data?.migrating_from) {
15927
16176
  try {
15928
16177
  const joinResponse = await this.clientEventReporter.track(this.cid, 'CoordinatorJoin', () => this.doJoinRequest(data));
16178
+ if (!joinResponse || supersededByLeave()) {
16179
+ this.logger.debug('Join superseded by leave; not creating SFU client');
16180
+ return;
16181
+ }
15929
16182
  this.credentials = joinResponse.credentials;
15930
16183
  statsOptions = joinResponse.stats_options;
15931
16184
  this.lastStatsOptions = statsOptions;
15932
16185
  }
15933
16186
  catch (error) {
16187
+ if (supersededByLeave())
16188
+ return;
15934
16189
  // prevent triggering reconnect flow if the state is OFFLINE
15935
16190
  const avoidRestoreState = this.state.callingState === exports.CallingState.OFFLINE;
15936
16191
  if (!avoidRestoreState) {
@@ -15978,6 +16233,10 @@ class Call {
15978
16233
  getGenericSdp('recvonly', dangerouslyForceCodec, subscriberFmtpLine),
15979
16234
  getGenericSdp('sendonly', dangerouslyForceCodec, fmtpLine),
15980
16235
  ]);
16236
+ if (supersededByLeave()) {
16237
+ this.logger.debug('Join superseded by leave; not joining SFU');
16238
+ return;
16239
+ }
15981
16240
  const isReconnecting = this.reconnectStrategy !== WebsocketReconnectStrategy.UNSPECIFIED;
15982
16241
  const reconnectDetails = isReconnecting
15983
16242
  ? this.getReconnectDetails(previousSfuClient?.edgeName, previousSessionId)
@@ -16003,6 +16262,10 @@ class Call {
16003
16262
  capabilities,
16004
16263
  source: ParticipantSource.WEBRTC_UNSPECIFIED,
16005
16264
  }));
16265
+ if (supersededByLeave()) {
16266
+ this.logger.debug('Join superseded by leave; not applying SFU response');
16267
+ return;
16268
+ }
16006
16269
  this.currentPublishOptions = publishOptions;
16007
16270
  this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
16008
16271
  if (callState) {
@@ -16010,6 +16273,10 @@ class Call {
16010
16273
  }
16011
16274
  }
16012
16275
  catch (error) {
16276
+ if (supersededByLeave()) {
16277
+ this.logger.debug('Join superseded by leave; ignoring SFU failure');
16278
+ return;
16279
+ }
16013
16280
  this.logger.warn('Join SFU request failed', error);
16014
16281
  sfuClient.close(StreamSfuClient.JOIN_FAILED, 'Join request failed, connection considered unhealthy');
16015
16282
  // restore the previous call state if the join-flow fails
@@ -16017,8 +16284,8 @@ class Call {
16017
16284
  throw error;
16018
16285
  }
16019
16286
  }
16020
- // If the user left while this join was in flight, bail before re-setting JOINED and before
16021
- // peer-connection setup below (both run synchronously after this, so one check covers them).
16287
+ // If the user left while this join was in flight, bail before re-setting JOINED
16288
+ // or starting peer-connection setup below.
16022
16289
  if (supersededByLeave()) {
16023
16290
  this.logger.debug('Join superseded by leave; aborting join flow');
16024
16291
  return;
@@ -16047,6 +16314,10 @@ class Call {
16047
16314
  unifiedSessionId: this.unifiedSessionId,
16048
16315
  });
16049
16316
  }
16317
+ if (supersededByLeave()) {
16318
+ this.logger.debug('Join superseded by leave; not completing join flow');
16319
+ return;
16320
+ }
16050
16321
  // make sure we only track connection timing if we are not calling this method as part of a reconnection flow
16051
16322
  if (!performingRejoin && !performingFastReconnect && !performingMigration) {
16052
16323
  this.sfuStatsReporter?.sendConnectionTime((Date.now() - connectStartTime) / 1000);
@@ -16192,15 +16463,20 @@ class Call {
16192
16463
  * @internal
16193
16464
  */
16194
16465
  initPublisherAndSubscriber = async (opts) => {
16466
+ const joinLeaveGeneration = this.leaveGeneration;
16195
16467
  const { sfuClient, connectionConfig, clientDetails, statsOptions, publishOptions, closePreviousInstances, unifiedSessionId, } = opts;
16196
16468
  const { enable_rtc_stats: enableTracing, reporting_interval_ms: reportingIntervalMs, } = statsOptions;
16197
16469
  // Flush the previous reporter's final sample while its peer connections are
16198
16470
  // still alive, before we dispose them below. Awaits only the sampling step.
16199
16471
  await this.sfuStatsReporter?.flush();
16472
+ if (this.leaveGeneration !== joinLeaveGeneration)
16473
+ return;
16200
16474
  this.sfuStatsReporter?.stop();
16201
16475
  this.sfuStatsReporter = undefined;
16202
16476
  if (closePreviousInstances && this.subscriber) {
16203
16477
  await this.subscriber.dispose();
16478
+ if (this.leaveGeneration !== joinLeaveGeneration)
16479
+ return;
16204
16480
  this.state.removeAllOrphanedTracks();
16205
16481
  }
16206
16482
  const basePeerConnectionOptions = {
@@ -16243,6 +16519,8 @@ class Call {
16243
16519
  if (!isAnonymous) {
16244
16520
  if (closePreviousInstances && this.publisher) {
16245
16521
  await this.publisher.dispose();
16522
+ if (this.leaveGeneration !== joinLeaveGeneration)
16523
+ return;
16246
16524
  }
16247
16525
  this.publisher = new Publisher(basePeerConnectionOptions, publishOptions, {
16248
16526
  selfSubEnabled: this.allowOwnTracksLoopback,
@@ -16279,12 +16557,22 @@ class Call {
16279
16557
  *
16280
16558
  * @internal
16281
16559
  * @param data the join call data.
16560
+ * @returns The coordinator response, or undefined if leave superseded the request before it was sent.
16282
16561
  */
16283
16562
  doJoinRequest = async (data) => {
16563
+ const joinLeaveGeneration = this.leaveGeneration;
16284
16564
  const location = await this.streamClient.getLocationHint();
16565
+ if (this.leaveGeneration !== joinLeaveGeneration) {
16566
+ this.logger.debug('Join superseded by leave; not sending coordinator request');
16567
+ return;
16568
+ }
16285
16569
  const e2ee = !!this.e2eeManager;
16286
16570
  const request = { ...data, location, e2ee };
16287
16571
  const joinResponse = await this.streamClient.post(`${this.streamClientBasePath}/join`, request);
16572
+ if (this.leaveGeneration !== joinLeaveGeneration) {
16573
+ this.logger.debug('Join superseded by leave; not applying coordinator response');
16574
+ return joinResponse;
16575
+ }
16288
16576
  this.state.updateFromCallResponse(joinResponse.call);
16289
16577
  this.state.setMembers(joinResponse.members);
16290
16578
  this.state.setOwnCapabilities(joinResponse.own_capabilities);
@@ -16295,10 +16583,12 @@ class Call {
16295
16583
  if (!isReconnecting && this.ringing && !this.isCreatedByMe) {
16296
16584
  // signals other users that I have accepted the incoming call.
16297
16585
  await this.accept();
16586
+ if (this.leaveGeneration !== joinLeaveGeneration)
16587
+ return joinResponse;
16298
16588
  }
16299
16589
  if (this.streamClient._hasConnectionID()) {
16300
16590
  this.watching = true;
16301
- this.clientStore.registerOrUpdateCall(this);
16591
+ this.clientState.registerOrUpdateCall(this);
16302
16592
  }
16303
16593
  return joinResponse;
16304
16594
  };
@@ -16384,6 +16674,7 @@ class Call {
16384
16674
  if (callingState === exports.CallingState.JOINING ||
16385
16675
  callingState === exports.CallingState.RECONNECTING ||
16386
16676
  callingState === exports.CallingState.MIGRATING ||
16677
+ callingState === exports.CallingState.LEFT ||
16387
16678
  callingState === exports.CallingState.RECONNECTING_FAILED)
16388
16679
  return;
16389
16680
  // Drop redundant reconnect calls. If a reconnect is already queued or
@@ -16392,19 +16683,27 @@ class Call {
16392
16683
  // `POST /join` per entry) once the call is already healthy again.
16393
16684
  if (hasPending(this.reconnectConcurrencyTag))
16394
16685
  return;
16686
+ const reconnectLeaveGeneration = this.leaveGeneration;
16687
+ const supersededByLeave = () => this.leaveGeneration !== reconnectLeaveGeneration;
16395
16688
  return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
16689
+ if (supersededByLeave())
16690
+ return;
16396
16691
  const reconnectStartTime = Date.now();
16397
16692
  this.reconnectStrategy = strategy;
16398
16693
  this.reconnectReason = reason;
16399
16694
  const sfuRejoinFailures = new Map();
16400
16695
  const markAsReconnectingFailed = async () => {
16696
+ if (supersededByLeave())
16697
+ return;
16401
16698
  try {
16402
16699
  // attempt to fetch the call data from the server, as the call
16403
16700
  // state might have changed while we were reconnecting or were offline
16404
16701
  await this.get();
16405
16702
  }
16406
16703
  finally {
16407
- this.state.setCallingState(exports.CallingState.RECONNECTING_FAILED);
16704
+ if (!supersededByLeave()) {
16705
+ this.state.setCallingState(exports.CallingState.RECONNECTING_FAILED);
16706
+ }
16408
16707
  }
16409
16708
  };
16410
16709
  const giveUpAndLeave = async (message) => {
@@ -16460,6 +16759,8 @@ class Call {
16460
16759
  try {
16461
16760
  // wait until the network is available
16462
16761
  await this.networkAvailableTask?.promise;
16762
+ if (supersededByLeave())
16763
+ return;
16463
16764
  this.logger.info(`[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[this.reconnectStrategy]}`);
16464
16765
  switch (this.reconnectStrategy) {
16465
16766
  case WebsocketReconnectStrategy.UNSPECIFIED:
@@ -16499,6 +16800,8 @@ class Call {
16499
16800
  break; // do-while loop, reconnection worked, exit the loop
16500
16801
  }
16501
16802
  catch (error) {
16803
+ if (supersededByLeave())
16804
+ return;
16502
16805
  if (attemptedStrategy === WebsocketReconnectStrategy.REJOIN) {
16503
16806
  const failedSfu = this.credentials?.server.edge_name;
16504
16807
  if (failedSfu) {
@@ -16529,6 +16832,8 @@ class Call {
16529
16832
  }
16530
16833
  // exponential backoff with jitter, capped at 5 s
16531
16834
  await sleep(retryInterval(attempt));
16835
+ if (supersededByLeave())
16836
+ return;
16532
16837
  const wasMigrating = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
16533
16838
  const mustPerformRejoin = (Date.now() - reconnectStartTime) / 1000 >
16534
16839
  this.fastReconnectDeadlineSeconds;
@@ -16547,7 +16852,8 @@ class Call {
16547
16852
  this.reconnectStrategy = nextStrategy;
16548
16853
  this.logger.info(`[Reconnect] ${currentStrategy} (${this.reconnectAttempts}) failed. Attempting with ${WebsocketReconnectStrategy[nextStrategy]}`, error);
16549
16854
  }
16550
- } while (this.state.callingState !== exports.CallingState.JOINED &&
16855
+ } while (!supersededByLeave() &&
16856
+ this.state.callingState !== exports.CallingState.JOINED &&
16551
16857
  this.state.callingState !== exports.CallingState.RECONNECTING_FAILED &&
16552
16858
  this.state.callingState !== exports.CallingState.LEFT);
16553
16859
  this.logger.info('[Reconnect] Reconnection flow finished');
@@ -16558,10 +16864,13 @@ class Call {
16558
16864
  * @internal
16559
16865
  */
16560
16866
  reconnectFast = async () => {
16867
+ const reconnectLeaveGeneration = this.leaveGeneration;
16561
16868
  const reconnectStartTime = Date.now();
16562
16869
  this.reconnectStrategy = WebsocketReconnectStrategy.FAST;
16563
16870
  this.state.setCallingState(exports.CallingState.RECONNECTING);
16564
16871
  await this.doJoin(this.joinCallData);
16872
+ if (this.leaveGeneration !== reconnectLeaveGeneration)
16873
+ return;
16565
16874
  await this.get(); // fetch the latest call state, as it might have changed
16566
16875
  this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.FAST, (Date.now() - reconnectStartTime) / 1000);
16567
16876
  };
@@ -16570,14 +16879,19 @@ class Call {
16570
16879
  * @internal
16571
16880
  */
16572
16881
  reconnectRejoin = async () => {
16882
+ const reconnectLeaveGeneration = this.leaveGeneration;
16573
16883
  const reconnectStartTime = Date.now();
16574
16884
  this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
16575
16885
  this.state.setCallingState(exports.CallingState.RECONNECTING);
16576
16886
  const joinReason = this.reconnectReason === ReconnectReason.NETWORK_BACK_ONLINE
16577
16887
  ? 'network-available'
16578
16888
  : 'full-rejoin';
16579
- await this.clientEventReporter.withJoinLifecycle(this.cid, joinReason, () => this.doJoin(this.joinCallData));
16889
+ await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason }, () => this.doJoin(this.joinCallData));
16890
+ if (this.leaveGeneration !== reconnectLeaveGeneration)
16891
+ return;
16580
16892
  await this.restorePublishedTracks();
16893
+ if (this.leaveGeneration !== reconnectLeaveGeneration)
16894
+ return;
16581
16895
  this.restoreSubscribedTracks();
16582
16896
  this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
16583
16897
  };
@@ -16586,6 +16900,7 @@ class Call {
16586
16900
  * @internal
16587
16901
  */
16588
16902
  reconnectMigrate = async () => {
16903
+ const reconnectLeaveGeneration = this.leaveGeneration;
16589
16904
  const reconnectStartTime = Date.now();
16590
16905
  const currentSfuClient = this.sfuClient;
16591
16906
  if (!currentSfuClient) {
@@ -16599,26 +16914,32 @@ class Call {
16599
16914
  currentPublisher?.detachEventHandlers();
16600
16915
  const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
16601
16916
  try {
16602
- const currentSfu = currentSfuClient.edgeName;
16603
- await this.clientEventReporter.withJoinLifecycle(this.cid, 'migration', () => this.doJoin({
16604
- ...this.joinCallData,
16605
- migrating_from: currentSfu,
16606
- migrating_from_list: [currentSfu],
16607
- }));
16608
- }
16609
- finally {
16610
- // cleanup the migration_from field after the migration is complete or failed
16611
- // as we don't want to keep dirty data in the join call data
16612
- delete this.joinCallData?.migrating_from;
16613
- delete this.joinCallData?.migrating_from_list;
16614
- }
16615
- await this.restorePublishedTracks();
16616
- this.restoreSubscribedTracks();
16617
- try {
16917
+ try {
16918
+ const currentSfu = currentSfuClient.edgeName;
16919
+ await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason: 'migration' }, () => this.doJoin({
16920
+ ...this.joinCallData,
16921
+ migrating_from: currentSfu,
16922
+ migrating_from_list: [currentSfu],
16923
+ }));
16924
+ }
16925
+ finally {
16926
+ // cleanup the migration_from field after the migration is complete or failed
16927
+ // as we don't want to keep dirty data in the join call data
16928
+ delete this.joinCallData?.migrating_from;
16929
+ delete this.joinCallData?.migrating_from_list;
16930
+ }
16931
+ if (this.leaveGeneration !== reconnectLeaveGeneration)
16932
+ return;
16933
+ await this.restorePublishedTracks();
16934
+ if (this.leaveGeneration !== reconnectLeaveGeneration)
16935
+ return;
16936
+ this.restoreSubscribedTracks();
16618
16937
  // Wait for the migration to complete, then close the previous SFU client
16619
16938
  // and the peer connection instances. In case of failure, the migration
16620
16939
  // task would throw an error and REJOIN would be attempted.
16621
16940
  await migrationTask();
16941
+ if (this.leaveGeneration !== reconnectLeaveGeneration)
16942
+ return;
16622
16943
  // in MIGRATE, we can consider the call as joined only after
16623
16944
  // `participantMigrationComplete` event is received, signaled by
16624
16945
  // the `migrationTask`
@@ -16903,10 +17224,11 @@ class Call {
16903
17224
  * Must be called before {@link join} so the RTCPeerConnection can be
16904
17225
  * configured for E2EE.
16905
17226
  *
16906
- * The manager is kept across {@link leave} so a rejoin of this same instance
16907
- * stays encrypted: do not dispose it while this call may be joined again.
16908
- * A disposed manager throws from `encrypt`/`decrypt` rather than silently
16909
- * publishing nothing, so re-attach a fresh one instead of reusing it.
17227
+ * The application owns the manager's lifetime - the SDK never disposes it, and
17228
+ * closing the peer connections does not release it. Discard the manager
17229
+ * together with this call instance once its flow has ended, and create a fresh
17230
+ * pair for a later flow: a disposed manager throws from `encrypt`/`decrypt`
17231
+ * rather than silently publishing nothing.
16910
17232
  *
16911
17233
  * @param e2ee - Any `E2EEManager`. Use `EncryptionManager.create()` for the
16912
17234
  * built-in AES-GCM scheme, or pass your own implementation.
@@ -16954,7 +17276,7 @@ class Call {
16954
17276
  });
16955
17277
  };
16956
17278
  /**
16957
- * Will enhance the reported stats with additional participant-specific information (`callStatsReport$` state [store variable](./StreamVideoClient.md/#readonlystatestore)).
17279
+ * Will enhance the reported stats with additional participant-specific information (the `callStatsReport$` state variable).
16958
17280
  * This is usually helpful when detailed stats for a specific participant are needed.
16959
17281
  *
16960
17282
  * @param sessionId the sessionId to start reporting for.
@@ -17373,40 +17695,36 @@ class Call {
17373
17695
  */
17374
17696
  scheduleAutoDrop = () => {
17375
17697
  this.cancelAutoDrop();
17376
- const settings = this.state.settings;
17377
- if (!settings)
17378
- return;
17379
- // ignore if the call is not ringing
17380
- if (this.state.callingState !== exports.CallingState.RINGING)
17381
- return;
17382
- const timeoutInMs = this.isCreatedByMe
17383
- ? settings.ring.auto_cancel_timeout_ms
17384
- : settings.ring.incoming_call_timeout_ms;
17385
- // 0 means no auto-drop
17386
- if (timeoutInMs <= 0)
17387
- return;
17388
- this.dropTimeout = setTimeout(() => {
17389
- // the call might have stopped ringing by this point,
17390
- // e.g. it was already accepted and joined
17391
- if (this.state.callingState !== exports.CallingState.RINGING)
17392
- return;
17393
- this.leave({
17394
- reject: true,
17395
- reason: 'timeout',
17396
- message: `ringing timeout - ${this.isCreatedByMe
17397
- ? 'no one accepted'
17398
- : `user didn't interact with incoming call screen`}`,
17399
- }).catch((err) => {
17400
- this.logger.error('Failed to drop call', err);
17401
- });
17402
- }, timeoutInMs);
17698
+ this.ringTimeout = new RingTimeout(this);
17699
+ this.ringTimeout.start();
17403
17700
  };
17404
17701
  /**
17405
17702
  * Cancels a scheduled auto-drop timeout.
17406
17703
  */
17407
17704
  cancelAutoDrop = () => {
17408
- clearTimeout(this.dropTimeout);
17409
- this.dropTimeout = undefined;
17705
+ this.ringTimeout?.stop();
17706
+ this.ringTimeout = undefined;
17707
+ };
17708
+ /**
17709
+ * Starts polling for the ring outcome. Applicable only to ringing calls the
17710
+ * current user created.
17711
+ */
17712
+ scheduleRingStatePolling = () => {
17713
+ this.cancelRingStatePolling();
17714
+ if (!this.isCreatedByMe)
17715
+ return;
17716
+ const options = this.streamClient.options.ringStatePolling;
17717
+ if (options === false)
17718
+ return;
17719
+ this.ringStatePoller = new RingStatePoller(this, options);
17720
+ this.ringStatePoller.start();
17721
+ };
17722
+ /**
17723
+ * Cancels the ring state polling.
17724
+ */
17725
+ cancelRingStatePolling = () => {
17726
+ this.ringStatePoller?.stop();
17727
+ this.ringStatePoller = undefined;
17410
17728
  };
17411
17729
  /**
17412
17730
  * Retrieves the list of recordings for the current call or call session.
@@ -19077,11 +19395,11 @@ class StreamClient {
19077
19395
  return await this.wsConnection.connect(this.defaultWSTimeout);
19078
19396
  };
19079
19397
  getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
19080
- "2.0.0-beta.0";
19398
+ "2.0.0-beta.1";
19081
19399
  getUserAgent = () => {
19082
19400
  if (!this.cachedUserAgent) {
19083
19401
  const { clientAppIdentifier = {} } = this.options;
19084
- const { sdkName = 'js', sdkVersion = "2.0.0-beta.0", ...extras } = clientAppIdentifier;
19402
+ const { sdkName = 'js', sdkVersion = "2.0.0-beta.1", ...extras } = clientAppIdentifier;
19085
19403
  this.cachedUserAgent = [
19086
19404
  `stream-video-${sdkName}-v${sdkVersion}`,
19087
19405
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -19214,6 +19532,7 @@ class ClientEventReporter {
19214
19532
  callContexts = new Map();
19215
19533
  joinAttemptIds = new Map();
19216
19534
  joinReasons = new Map();
19535
+ joinSources = new Map();
19217
19536
  coordinatorPairs = new Map();
19218
19537
  wsPairs = new Map();
19219
19538
  peerConnectionPairs = new Map();
@@ -19328,6 +19647,7 @@ class ClientEventReporter {
19328
19647
  this.callContexts.delete(cid);
19329
19648
  this.joinAttemptIds.delete(cid);
19330
19649
  this.joinReasons.delete(cid);
19650
+ this.joinSources.delete(cid);
19331
19651
  this.coordinatorPairs.delete(cid);
19332
19652
  this.wsPairs.delete(cid);
19333
19653
  this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
@@ -19352,7 +19672,12 @@ class ClientEventReporter {
19352
19672
  this.logger.warn('Failed to start join correlation', err);
19353
19673
  }
19354
19674
  };
19355
- withJoinLifecycle = async (cid, joinReason, op) => {
19675
+ withJoinLifecycle = async (cid, options, op) => {
19676
+ const { joinReason, joinSource } = options;
19677
+ if (joinSource)
19678
+ this.joinSources.set(cid, joinSource);
19679
+ else
19680
+ this.joinSources.delete(cid);
19356
19681
  this.startCorrelation(cid, joinReason);
19357
19682
  try {
19358
19683
  return await op();
@@ -19361,6 +19686,9 @@ class ClientEventReporter {
19361
19686
  this.closeCallPairs(cid);
19362
19687
  throw err;
19363
19688
  }
19689
+ finally {
19690
+ this.joinSources.delete(cid);
19691
+ }
19364
19692
  };
19365
19693
  track = async (cid, stage, op) => {
19366
19694
  this.beginAttempt(cid, stage);
@@ -19484,6 +19812,7 @@ class ClientEventReporter {
19484
19812
  startedAt: Date.now(),
19485
19813
  joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
19486
19814
  joinReasonSnapshot: this.joinReasons.get(cid),
19815
+ joinSourceSnapshot: this.joinSources.get(cid),
19487
19816
  };
19488
19817
  this.coordinatorPairs.set(cid, pair);
19489
19818
  this.sendForCall(cid, {
@@ -19491,6 +19820,7 @@ class ClientEventReporter {
19491
19820
  ...(pair.joinReasonSnapshot && {
19492
19821
  join_reason: pair.joinReasonSnapshot,
19493
19822
  }),
19823
+ ...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
19494
19824
  event_type: 'initiated',
19495
19825
  });
19496
19826
  }
@@ -19505,6 +19835,7 @@ class ClientEventReporter {
19505
19835
  ...this.buildCommon(cid, 'CoordinatorJoin', pair),
19506
19836
  ...this.sessionIdField(cid),
19507
19837
  ...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
19838
+ ...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
19508
19839
  event_type: 'completed',
19509
19840
  outcome: 'success',
19510
19841
  retry_count_attempt: pair.attempts - 1,
@@ -19523,6 +19854,7 @@ class ClientEventReporter {
19523
19854
  ...this.buildCommon(cid, 'CoordinatorJoin', pair),
19524
19855
  ...this.sessionIdField(cid),
19525
19856
  ...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
19857
+ ...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
19526
19858
  event_type: 'completed',
19527
19859
  outcome: 'failure',
19528
19860
  retry_count_attempt: pair.attempts - 1,
@@ -19823,16 +20155,11 @@ const mapWsJoinError = (err) => {
19823
20155
  */
19824
20156
  class StreamVideoClient {
19825
20157
  /**
19826
- * A reactive store that exposes all the state variables reactively.
20158
+ * The reactive state of this client.
19827
20159
  * You can subscribe to changes of the different state variables.
19828
- * Our library is built in a way that all state changes are exposed in this store,
19829
- * o all UI changes in your application should be handled by subscribing to these variables.
19830
- *
19831
- * @deprecated use the `client.state` getter.
19832
20160
  */
19833
- readOnlyStateStore;
20161
+ state = new ClientState();
19834
20162
  logger;
19835
- writeableStateStore;
19836
20163
  streamClient;
19837
20164
  clientEventReporter;
19838
20165
  effectsRegistered = false;
@@ -19857,8 +20184,6 @@ class StreamVideoClient {
19857
20184
  streamClient: this.streamClient,
19858
20185
  enabled: clientOptions?.clientEventsReportingEnabled ?? true,
19859
20186
  });
19860
- this.writeableStateStore = new StreamVideoWriteableStateStore();
19861
- this.readOnlyStateStore = new StreamVideoReadOnlyStateStore(this.writeableStateStore);
19862
20187
  if (typeof apiKeyOrArgs !== 'string' && apiKeyOrArgs.user) {
19863
20188
  const user = apiKeyOrArgs.user;
19864
20189
  if (user.type === 'anonymous')
@@ -19895,19 +20220,13 @@ class StreamVideoClient {
19895
20220
  }
19896
20221
  StreamVideoClient._instances.set(instanceKey, this);
19897
20222
  };
19898
- /**
19899
- * Return the reactive state store, use this if you want to be notified about changes to the client state
19900
- */
19901
- get state() {
19902
- return this.readOnlyStateStore;
19903
- }
19904
20223
  registerEffects = () => {
19905
20224
  if (this.effectsRegistered)
19906
20225
  return;
19907
20226
  this.eventHandlersToUnregister.push(this.on('call.created', (event) => this.initCallFromEvent(event)), this.on('call.ring', (event) => this.initCallFromEvent(event)), this.on('connection.changed', (event) => {
19908
20227
  if (!event.online)
19909
20228
  return;
19910
- const callsToReWatch = this.writeableStateStore.calls
20229
+ const callsToReWatch = this.state.calls
19911
20230
  .filter((call) => call.watching)
19912
20231
  .map((call) => call.cid);
19913
20232
  if (callsToReWatch.length <= 0)
@@ -19927,7 +20246,7 @@ class StreamVideoClient {
19927
20246
  const concurrencyTag = getCallInitConcurrencyTag(e.call_cid);
19928
20247
  await withoutConcurrency(concurrencyTag, async () => {
19929
20248
  const ringing = e.type === 'call.ring';
19930
- let call = this.writeableStateStore.findCall(e.call.type, e.call.id);
20249
+ let call = this.state.findCall(e.call.type, e.call.id);
19931
20250
  if (call) {
19932
20251
  if (ringing) {
19933
20252
  if (this.shouldRejectCall(call.cid)) {
@@ -19953,7 +20272,7 @@ class StreamVideoClient {
19953
20272
  type: e.call.type,
19954
20273
  id: e.call.id,
19955
20274
  members: e.members,
19956
- clientStore: this.writeableStateStore,
20275
+ clientState: this.state,
19957
20276
  ringing,
19958
20277
  });
19959
20278
  if (ringing) {
@@ -19969,7 +20288,7 @@ class StreamVideoClient {
19969
20288
  }
19970
20289
  else {
19971
20290
  call.state.updateFromCallResponse(e.call);
19972
- this.writeableStateStore.registerCall(call);
20291
+ this.state.registerCall(call);
19973
20292
  this.logger.info(`New call created and registered: ${call.cid}`);
19974
20293
  }
19975
20294
  });
@@ -20000,7 +20319,7 @@ class StreamVideoClient {
20000
20319
  filter_conditions: { cid: { $in: callsToReWatch } },
20001
20320
  });
20002
20321
  for (const c of response.calls) {
20003
- const call = this.writeableStateStore.findCall(c.call.type, c.call.id);
20322
+ const call = this.state.findCall(c.call.type, c.call.id);
20004
20323
  if (call) {
20005
20324
  call.updateFromCallStateResponse(c);
20006
20325
  }
@@ -20021,8 +20340,6 @@ class StreamVideoClient {
20021
20340
  };
20022
20341
  /**
20023
20342
  * Connects the given user to the client.
20024
- * Only one user can connect at a time, if you want to change users, call `disconnectUser` before connecting a new user.
20025
- * If the connection is successful, the connected user [state variable](#readonlystatestore) will be updated accordingly.
20026
20343
  *
20027
20344
  * @param user the user to connect.
20028
20345
  * @param tokenOrProvider a token or a function that returns a token.
@@ -20066,24 +20383,40 @@ class StreamVideoClient {
20066
20383
  });
20067
20384
  // connectUserResponse will be void if connectUser called twice for the same user
20068
20385
  if (connectUserResponse?.me) {
20069
- this.writeableStateStore.setConnectedUser(connectUserResponse.me);
20386
+ this.state.setConnectedUser(connectUserResponse.me);
20070
20387
  }
20071
20388
  this.registerEffects();
20072
20389
  return connectUserResponse;
20073
20390
  };
20391
+ /**
20392
+ * Leaves every call this client still tracks. Runs while the coordinator
20393
+ * connection is alive, so that the leave requests each call sends reach the
20394
+ * backend.
20395
+ */
20396
+ leaveAllCalls = async () => {
20397
+ for (const call of this.state.calls) {
20398
+ if (call.state.callingState === exports.CallingState.LEFT)
20399
+ continue;
20400
+ this.logger.info(`User disconnected, leaving call: ${call.cid}`);
20401
+ await call
20402
+ .leave({ message: 'client.disconnectUser() called' })
20403
+ .catch((err) => {
20404
+ this.logger.error(`Error leaving call: ${call.cid}`, err);
20405
+ });
20406
+ }
20407
+ };
20074
20408
  /**
20075
20409
  * Disconnects the currently connected user from the client.
20076
- *
20077
- * If the connection is successfully disconnected, the connected user [state variable](#readonlystatestore) will be updated accordingly
20410
+ * Leaves every call the user is still in, and updates `client.state`accordingly.
20078
20411
  *
20079
20412
  * @param timeout Max number of ms, to wait for close event of websocket, before forcefully assuming successful disconnection.
20080
- * https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
20081
20413
  */
20082
20414
  disconnectUser = async (timeout) => {
20083
20415
  await withoutConcurrency(this.connectionConcurrencyTag, async () => {
20084
20416
  const { user, key } = this.streamClient;
20085
20417
  if (!user)
20086
20418
  return;
20419
+ await this.leaveAllCalls();
20087
20420
  await this.streamClient.disconnectUser(timeout);
20088
20421
  if (user.id) {
20089
20422
  StreamVideoClient._instances.delete(getInstanceKey(key, user));
@@ -20091,13 +20424,15 @@ class StreamVideoClient {
20091
20424
  this.eventHandlersToUnregister.forEach((unregister) => unregister());
20092
20425
  this.eventHandlersToUnregister = [];
20093
20426
  this.effectsRegistered = false;
20094
- this.writeableStateStore.setConnectedUser(undefined);
20427
+ this.state.setConnectedUser(undefined);
20095
20428
  });
20096
20429
  };
20097
20430
  /**
20098
20431
  * You can subscribe to WebSocket events provided by the API.
20099
20432
  * To remove a subscription, call the `off` method or, execute the returned unsubscribe function.
20100
- * Please note that subscribing to WebSocket events is an advanced use-case, for most use-cases it should be enough to watch for changes in the reactive [state store](#readonlystatestore).
20433
+ * Please note that subscribing to WebSocket events is an advanced use-case,
20434
+ * for most use-cases it should be enough to watch for changes in the reactive
20435
+ * `client.state`.
20101
20436
  *
20102
20437
  * @param eventName the event name or 'all'.
20103
20438
  * @param callback the callback which will be called when the event is emitted.
@@ -20124,7 +20459,7 @@ class StreamVideoClient {
20124
20459
  */
20125
20460
  call = (type, id, options = {}) => {
20126
20461
  const call = options.reuseInstance
20127
- ? this.writeableStateStore.findCall(type, id)
20462
+ ? this.state.findCall(type, id)
20128
20463
  : undefined;
20129
20464
  return (call ??
20130
20465
  new Call({
@@ -20132,7 +20467,7 @@ class StreamVideoClient {
20132
20467
  clientEventReporter: this.clientEventReporter,
20133
20468
  id: id,
20134
20469
  type: type,
20135
- clientStore: this.writeableStateStore,
20470
+ clientState: this.state,
20136
20471
  }));
20137
20472
  };
20138
20473
  /**
@@ -20162,7 +20497,7 @@ class StreamVideoClient {
20162
20497
  members: c.members,
20163
20498
  ownCapabilities: c.own_capabilities,
20164
20499
  watching: data.watch,
20165
- clientStore: this.writeableStateStore,
20500
+ clientState: this.state,
20166
20501
  });
20167
20502
  call.state.updateFromCallResponse(c.call);
20168
20503
  await call.applyDeviceConfig(c.call.settings, {
@@ -20171,7 +20506,7 @@ class StreamVideoClient {
20171
20506
  });
20172
20507
  if (data.watch) {
20173
20508
  await call.setup();
20174
- this.writeableStateStore.registerCall(call);
20509
+ this.state.registerCall(call);
20175
20510
  }
20176
20511
  calls.push(call);
20177
20512
  }
@@ -20270,7 +20605,7 @@ class StreamVideoClient {
20270
20605
  clientEventReporter: this.clientEventReporter,
20271
20606
  type: callType,
20272
20607
  id: callId,
20273
- clientStore: this.writeableStateStore,
20608
+ clientState: this.state,
20274
20609
  ringing: true,
20275
20610
  });
20276
20611
  await call.get();
@@ -20776,9 +21111,9 @@ class EncryptionManager extends TypedEventEmitter {
20776
21111
  }
20777
21112
  if ((this.piped ??= new WeakSet()).has(target))
20778
21113
  return;
20779
- this.piped.add(target);
20780
21114
  // @ts-expect-error createEncodedStreams is not in the standard typedefs
20781
21115
  const { readable, writable } = target.createEncodedStreams();
21116
+ this.piped.add(target);
20782
21117
  this.worker.postMessage({ type: 'cmd.setup_transform', ...options, readable, writable }, [readable, writable]);
20783
21118
  };
20784
21119
  /**
@@ -20888,6 +21223,7 @@ exports.CallType = CallType;
20888
21223
  exports.CallTypes = CallTypes;
20889
21224
  exports.CameraManager = CameraManager;
20890
21225
  exports.CameraManagerState = CameraManagerState;
21226
+ exports.ClientState = ClientState;
20891
21227
  exports.CreateDeviceRequestPushProviderEnum = CreateDeviceRequestPushProviderEnum;
20892
21228
  exports.DEFAULT_LOOPBACK_RECORDING_DURATION_MS = DEFAULT_LOOPBACK_RECORDING_DURATION_MS;
20893
21229
  exports.DeviceManager = DeviceManager;
@@ -20937,8 +21273,6 @@ exports.StartClosedCaptionsRequestLanguageEnum = StartClosedCaptionsRequestLangu
20937
21273
  exports.StartTranscriptionRequestLanguageEnum = StartTranscriptionRequestLanguageEnum;
20938
21274
  exports.StreamSfuClient = StreamSfuClient;
20939
21275
  exports.StreamVideoClient = StreamVideoClient;
20940
- exports.StreamVideoReadOnlyStateStore = StreamVideoReadOnlyStateStore;
20941
- exports.StreamVideoWriteableStateStore = StreamVideoWriteableStateStore;
20942
21276
  exports.TranscriptionSettingsRequestClosedCaptionModeEnum = TranscriptionSettingsRequestClosedCaptionModeEnum;
20943
21277
  exports.TranscriptionSettingsRequestLanguageEnum = TranscriptionSettingsRequestLanguageEnum;
20944
21278
  exports.TranscriptionSettingsRequestModeEnum = TranscriptionSettingsRequestModeEnum;
@@ -20946,6 +21280,7 @@ exports.TranscriptionSettingsResponseClosedCaptionModeEnum = TranscriptionSettin
20946
21280
  exports.TranscriptionSettingsResponseLanguageEnum = TranscriptionSettingsResponseLanguageEnum;
20947
21281
  exports.TranscriptionSettingsResponseModeEnum = TranscriptionSettingsResponseModeEnum;
20948
21282
  exports.TranslationSettingsLanguagesEnum = TranslationSettingsLanguagesEnum;
21283
+ exports.TypedEventEmitter = TypedEventEmitter;
20949
21284
  exports.UpdateUserPermissionsRequestGrantPermissionsEnum = UpdateUserPermissionsRequestGrantPermissionsEnum;
20950
21285
  exports.UpdateUserPermissionsRequestRevokePermissionsEnum = UpdateUserPermissionsRequestRevokePermissionsEnum;
20951
21286
  exports.VideoSettingsRequestCameraFacingEnum = VideoSettingsRequestCameraFacingEnum;