@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
@@ -7,7 +7,7 @@ import { TwirpFetchTransport, TwirpErrorCode } from '@protobuf-ts/twirp-transpor
7
7
  import * as scopedLogger from '@stream-io/logger';
8
8
  export { LogLevelEnum } from '@stream-io/logger';
9
9
  import { parse, write } from 'sdp-transform';
10
- import { Subject, Observable, combineLatest, BehaviorSubject, ReplaySubject, shareReplay, map, distinctUntilChanged, startWith, takeWhile, distinctUntilKeyChanged, fromEventPattern, concatMap, merge, from, fromEvent, tap, debounceTime, pairwise, firstValueFrom, of, Subscription } from 'rxjs';
10
+ import { Subject, Observable, BehaviorSubject, distinctUntilChanged, shareReplay, ReplaySubject, map, combineLatest, startWith, takeWhile, distinctUntilKeyChanged, fromEventPattern, concatMap, merge, from, fromEvent, tap, debounceTime, pairwise, firstValueFrom, of, Subscription } from 'rxjs';
11
11
  import { UAParser } from 'ua-parser-js';
12
12
  import { WorkerTimer } from '@stream-io/worker-timer';
13
13
 
@@ -5081,7 +5081,7 @@ const toIceCandidate = (iceTrickle) => {
5081
5081
  }
5082
5082
  };
5083
5083
 
5084
- const version = "2.0.0-beta.0";
5084
+ const version = "2.0.0-beta.1";
5085
5085
  const [major, minor, patch] = version.split('.');
5086
5086
  let sdkInfo = {
5087
5087
  type: SdkType.PLAIN_JAVASCRIPT,
@@ -5461,11 +5461,14 @@ const isFunctionPatch = (update) => typeof update === 'function';
5461
5461
  * @param observable$ the observable to get the value from.
5462
5462
  */
5463
5463
  const getCurrentValue = (observable$) => {
5464
+ if (observable$ instanceof BehaviorSubject) {
5465
+ return observable$.getValue();
5466
+ }
5464
5467
  let value;
5465
5468
  let err = undefined;
5466
- combineLatest([observable$])
5469
+ observable$
5467
5470
  .subscribe({
5468
- next: ([v]) => {
5471
+ next: (v) => {
5469
5472
  value = v;
5470
5473
  },
5471
5474
  error: (e) => {
@@ -5614,38 +5617,39 @@ var CallingState;
5614
5617
  CallingState["OFFLINE"] = "offline";
5615
5618
  })(CallingState || (CallingState = {}));
5616
5619
 
5617
- class StreamVideoWriteableStateStore {
5618
- /**
5619
- * A store keeping data of a successfully connected user over WS to the coordinator server.
5620
- */
5621
- connectedUserSubject = new BehaviorSubject(undefined);
5622
- /**
5623
- * A list of {@link Call} objects created/tracked by this client.
5624
- */
5625
- callsSubject = new BehaviorSubject([]);
5626
- constructor() {
5627
- this.connectedUserSubject.subscribe(async (user) => {
5628
- // leave all calls when the user disconnects.
5629
- if (!user) {
5630
- const logger = videoLoggerSystem.getLogger('client-state');
5631
- for (const call of this.calls) {
5632
- if (call.state.callingState === CallingState.LEFT)
5633
- continue;
5634
- logger.info(`User disconnected, leaving call: ${call.cid}`);
5635
- await call
5636
- .leave({ message: 'client.disconnectUser() called' })
5637
- .catch((err) => {
5638
- logger.error(`Error leaving call: ${call.cid}`, err);
5639
- });
5640
- }
5641
- }
5642
- });
5643
- }
5644
- /**
5645
- * The currently connected user.
5646
- */
5620
+ function subject(initialValue) {
5621
+ return new BehaviorSubject(initialValue);
5622
+ }
5623
+ /**
5624
+ * Creates an Observable from the given subject by piping it to the
5625
+ * `distinctUntilChanged()` operator.
5626
+ */
5627
+ const duc = (source, comparator) => source.pipe(distinctUntilChanged(comparator));
5628
+ /**
5629
+ * Multicasts the given Observable, replaying the latest computed value to
5630
+ * every new subscriber instead of re-running the pipeline for each of them.
5631
+ */
5632
+ const shared = (source) => source.pipe(shareReplay({ bufferSize: 1, refCount: true }));
5633
+
5634
+ /**
5635
+ * Holds the state of the client, and the calls it created or tracks.
5636
+ * You can subscribe to the exposed observables to get notified about changes.
5637
+ */
5638
+ class ClientState {
5639
+ logger = videoLoggerSystem.getLogger('ClientState');
5640
+ connectedUserSubject = subject();
5641
+ callsSubject = subject([]);
5642
+ /** Data describing a user successfully connected over WS to the coordinator. */
5643
+ connectedUser$ = this.connectedUserSubject.asObservable();
5644
+ /** A list of {@link Call} objects created/tracked by this client. */
5645
+ calls$ = this.callsSubject.asObservable();
5646
+ /** The currently connected user. */
5647
5647
  get connectedUser() {
5648
- return this.connectedUserSubject.getValue();
5648
+ return getCurrentValue(this.connectedUserSubject);
5649
+ }
5650
+ /** A list of {@link Call} objects created/tracked by this client. */
5651
+ get calls() {
5652
+ return getCurrentValue(this.callsSubject);
5649
5653
  }
5650
5654
  /**
5651
5655
  * Sets the currently connected user.
@@ -5656,22 +5660,19 @@ class StreamVideoWriteableStateStore {
5656
5660
  setConnectedUser = (user) => {
5657
5661
  return setCurrentValue(this.connectedUserSubject, user);
5658
5662
  };
5659
- /**
5660
- * A list of {@link Call} objects created/tracked by this client.
5661
- */
5662
- get calls() {
5663
- return getCurrentValue(this.callsSubject);
5664
- }
5665
5663
  /**
5666
5664
  * Sets the list of {@link Call} objects created/tracked by this client.
5667
- * @param calls
5665
+ *
5666
+ * @internal
5667
+ * @param calls the calls to set.
5668
5668
  */
5669
5669
  setCalls = (calls) => {
5670
5670
  return setCurrentValue(this.callsSubject, calls);
5671
5671
  };
5672
5672
  /**
5673
- * Adds a {@link Call} object to the list of {@link Call} objects created/tracked by this client.
5673
+ * Adds a {@link Call} object to the list of calls tracked by this client.
5674
5674
  *
5675
+ * @internal
5675
5676
  * @param call the call to add.
5676
5677
  */
5677
5678
  registerCall = (call) => {
@@ -5682,6 +5683,7 @@ class StreamVideoWriteableStateStore {
5682
5683
  /**
5683
5684
  * Registers a {@link Call} object if it doesn't exist, otherwise updates it.
5684
5685
  *
5686
+ * @internal
5685
5687
  * @param call the call to register or update.
5686
5688
  */
5687
5689
  registerOrUpdateCall = (call) => {
@@ -5693,17 +5695,17 @@ class StreamVideoWriteableStateStore {
5693
5695
  }
5694
5696
  };
5695
5697
  /**
5696
- * Removes a {@link Call} object from the list of {@link Call} objects created/tracked by this client.
5698
+ * Removes a {@link Call} object from the list of calls tracked by this client.
5697
5699
  *
5698
- * @param call the call to remove
5700
+ * @internal
5701
+ * @param call the call to remove.
5699
5702
  */
5700
5703
  unregisterCall = (call) => {
5701
- const logger = videoLoggerSystem.getLogger('client-state');
5702
- logger.trace(`Unregistering call: ${call.cid}`);
5704
+ this.logger.trace(`Unregistering call: ${call.cid}`);
5703
5705
  return this.setCalls((calls) => calls.filter((c) => c !== call));
5704
5706
  };
5705
5707
  /**
5706
- * Finds a {@link Call} object in the list of {@link Call} objects created/tracked by this client.
5708
+ * Finds a {@link Call} object in the list of calls tracked by this client.
5707
5709
  *
5708
5710
  * @param type the type of call to find.
5709
5711
  * @param id the id of the call to find.
@@ -5712,38 +5714,6 @@ class StreamVideoWriteableStateStore {
5712
5714
  return this.calls.find((c) => c.type === type && c.id === id);
5713
5715
  };
5714
5716
  }
5715
- /**
5716
- * A reactive store that exposes state variables in a reactive manner.
5717
- * You can subscribe to changes of the different state variables.
5718
- * This central store contains all the state variables related to [`StreamVideoClient`](./StreamVideClient.md) and [`Call`](./Call.md).
5719
- */
5720
- class StreamVideoReadOnlyStateStore {
5721
- /**
5722
- * Data describing a user successfully connected over WS to coordinator server.
5723
- */
5724
- connectedUser$;
5725
- /**
5726
- * A list of {@link Call} objects created/tracked by this client.
5727
- */
5728
- calls$;
5729
- constructor(store) {
5730
- // convert and expose subjects as observables
5731
- this.connectedUser$ = store.connectedUserSubject.asObservable();
5732
- this.calls$ = store.callsSubject.asObservable();
5733
- }
5734
- /**
5735
- * The current user connected over WS to the backend.
5736
- */
5737
- get connectedUser() {
5738
- return getCurrentValue(this.connectedUser$);
5739
- }
5740
- /**
5741
- * A list of {@link Call} objects created/tracked by this client.
5742
- */
5743
- get calls() {
5744
- return getCurrentValue(this.calls$);
5745
- }
5746
- }
5747
5717
 
5748
5718
  /**
5749
5719
  * Creates a new combined {@link Comparator<T>} which sorts items by the given comparators.
@@ -6076,9 +6046,7 @@ const paginatedLayoutSortPreset = combineComparators(pinned, ifInvisibleOrUnknow
6076
6046
  */
6077
6047
  const livestreamOrAudioRoomSortPreset = combineComparators(publishing, ifInvisibleOrUnknownBy(combineComparators(dominantSpeaker, speaking, reactionType('raised-hand'), withVideoIngressSource, publishingVideo, publishingAudio)), role('admin', 'host', 'speaker'));
6078
6048
 
6079
- /**
6080
- * Returns the default egress object - when no egress data is available.
6081
- */
6049
+ /** Returns the default egress object - when no egress data is available. */
6082
6050
  const defaultEgress = {
6083
6051
  broadcasting: false,
6084
6052
  hls: { playlist_url: '', status: '' },
@@ -6089,93 +6057,120 @@ const defaultEgress = {
6089
6057
  * @react You don't have to use this class directly, as we are exposing the state through Hooks.
6090
6058
  */
6091
6059
  class CallState {
6092
- backstageSubject = new BehaviorSubject(true);
6093
- blockedUserIdsSubject = new BehaviorSubject([]);
6094
- createdAtSubject = new BehaviorSubject(new Date());
6095
- endedAtSubject = new BehaviorSubject(undefined);
6096
- startsAtSubject = new BehaviorSubject(undefined);
6097
- updatedAtSubject = new BehaviorSubject(new Date());
6098
- createdBySubject = new BehaviorSubject(undefined);
6099
- customSubject = new BehaviorSubject({});
6100
- egressSubject = new BehaviorSubject(undefined);
6101
- ingressSubject = new BehaviorSubject(undefined);
6102
- recordingSubject = new BehaviorSubject(false);
6103
- individualRecordingSubject = new BehaviorSubject(false);
6104
- rawRecordingSubject = new BehaviorSubject(false);
6105
- sessionSubject = new BehaviorSubject(undefined);
6106
- settingsSubject = new BehaviorSubject(undefined);
6107
- transcribingSubject = new BehaviorSubject(false);
6108
- captioningSubject = new BehaviorSubject(false);
6109
- e2eeEnabledSubject = new BehaviorSubject(false);
6110
- endedBySubject = new BehaviorSubject(undefined);
6111
- thumbnailsSubject = new BehaviorSubject(undefined);
6112
- membersSubject = new BehaviorSubject([]);
6113
- ownCapabilitiesSubject = new BehaviorSubject([]);
6114
- callingStateSubject = new BehaviorSubject(CallingState.UNKNOWN);
6115
- startedAtSubject = new BehaviorSubject(undefined);
6116
- participantCountSubject = new BehaviorSubject(0);
6117
- anonymousParticipantCountSubject = new BehaviorSubject(0);
6118
- participantsSubject = new BehaviorSubject([]);
6119
- callStatsReportSubject = new BehaviorSubject(undefined);
6120
- closedCaptionsSubject = new BehaviorSubject([]);
6121
- // These are tracks that were delivered to the Subscriber's onTrack event
6122
- // that we couldn't associate with a participant yet.
6123
- // This happens when the participantJoined event hasn't been received yet.
6124
- // We keep these tracks around until we can associate them with a participant.
6125
- orphanedTracks = [];
6060
+ logger = videoLoggerSystem.getLogger('CallState');
6061
+ /** A list of comparators that are used to sort the participants. */
6062
+ sortParticipantsBy = defaultSortPreset;
6063
+ backstageSubject = subject(true);
6064
+ blockedUserIdsSubject = subject([]);
6065
+ createdAtSubject = subject(new Date());
6066
+ endedAtSubject = subject();
6067
+ startsAtSubject = subject();
6068
+ updatedAtSubject = subject(new Date());
6069
+ createdBySubject = subject();
6070
+ customSubject = subject({});
6071
+ egressSubject = subject();
6072
+ ingressSubject = subject();
6073
+ recordingSubject = subject(false);
6074
+ individualRecordingSubject = subject(false);
6075
+ rawRecordingSubject = subject(false);
6076
+ sessionSubject = subject();
6077
+ settingsSubject = subject();
6078
+ transcribingSubject = subject(false);
6079
+ captioningSubject = subject(false);
6080
+ e2eeEnabledSubject = subject(false);
6081
+ endedBySubject = subject();
6082
+ thumbnailsSubject = subject();
6083
+ membersSubject = subject([]);
6084
+ ownCapabilitiesSubject = subject([]);
6085
+ callingStateSubject = subject(CallingState.UNKNOWN);
6086
+ startedAtSubject = subject();
6087
+ participantCountSubject = subject(0);
6088
+ anonymousParticipantCountSubject = subject(0);
6089
+ participantsSubject = subject([]);
6090
+ callStatsReportSubject = subject();
6091
+ closedCaptionsSubject = subject([]);
6126
6092
  callGrantsSubject = new ReplaySubject(1);
6127
- // Derived state
6093
+ /** The backstage state. */
6094
+ backstage$ = duc(this.backstageSubject);
6095
+ /** Will provide the list of blocked user IDs. */
6096
+ blockedUserIds$ = duc(this.blockedUserIdsSubject, isShallowArrayEqual);
6097
+ /** Will provide the time when this call has been created. */
6098
+ createdAt$ = this.createdAtSubject.asObservable();
6099
+ /** Will provide the time when this call has been ended. */
6100
+ endedAt$ = this.endedAtSubject.asObservable();
6101
+ /** Will provide the time when this call has been scheduled to start. */
6102
+ startsAt$ = this.startsAtSubject.asObservable();
6103
+ /** Will provide the time when this call has been updated. */
6104
+ updatedAt$ = this.updatedAtSubject.asObservable();
6105
+ /** Will provide the user who created this call. */
6106
+ createdBy$ = this.createdBySubject.asObservable();
6107
+ /** Will provide the custom data of this call. */
6108
+ custom$ = this.customSubject.asObservable();
6109
+ /** Will provide the egress data of this call. */
6110
+ egress$ = this.egressSubject.asObservable();
6111
+ /** Will provide the ingress data of this call. */
6112
+ ingress$ = this.ingressSubject.asObservable();
6113
+ /** Will provide the recording state of this call. */
6114
+ recording$ = duc(this.recordingSubject);
6115
+ /** Will provide the recording state of this call. */
6116
+ individualRecording$ = duc(this.individualRecordingSubject);
6117
+ /** Will provide the recording state of this call. */
6118
+ rawRecording$ = duc(this.rawRecordingSubject);
6119
+ /** Will provide the session data of this call. */
6120
+ session$ = this.sessionSubject.asObservable();
6121
+ /** Will provide the settings of this call. */
6122
+ settings$ = this.settingsSubject.asObservable();
6123
+ /** Will provide the transcribing state of this call. */
6124
+ transcribing$ = duc(this.transcribingSubject);
6125
+ /** Will provide the closed captioning state of this call. */
6126
+ captioning$ = duc(this.captioningSubject);
6127
+ /**
6128
+ * Whether end-to-end encryption is active for this call, as reported by the
6129
+ * SFU in the join response.
6130
+ */
6131
+ e2eeEnabled$ = duc(this.e2eeEnabledSubject);
6132
+ /** Will provide the user who ended this call. */
6133
+ endedBy$ = this.endedBySubject.asObservable();
6134
+ /** Will provide the thumbnails of this call. */
6135
+ thumbnails$ = this.thumbnailsSubject.asObservable();
6136
+ /** The list of members in the current call. */
6137
+ members$ = this.membersSubject.asObservable();
6138
+ /** The calling state. */
6139
+ callingState$ = duc(this.callingStateSubject);
6128
6140
  /**
6129
6141
  * The time the call session actually started.
6130
6142
  * Useful for displaying the call duration.
6131
6143
  */
6132
- startedAt$;
6144
+ startedAt$ = this.startedAtSubject.asObservable();
6133
6145
  /**
6134
6146
  * The server-side counted number of participants connected to the current call.
6135
6147
  * This number includes the anonymous participants as well.
6136
6148
  */
6137
- participantCount$;
6149
+ participantCount$ = duc(this.participantCountSubject);
6138
6150
  /**
6139
6151
  * The server-side counted number of anonymous participants connected to the current call.
6140
6152
  * This number excludes the regular participants.
6141
6153
  */
6142
- anonymousParticipantCount$;
6154
+ anonymousParticipantCount$ = duc(this.anonymousParticipantCountSubject);
6143
6155
  /**
6144
6156
  * All participants of the current call (this includes the current user and other participants as well),
6145
6157
  * unsorted. This observable only updates when participants join or leave the call.
6146
6158
  */
6147
- rawParticipants$;
6148
- /**
6149
- * All participants of the current call (this includes the current user and other participants as well),
6150
- * sorted according to the current `sortByParticipantsBy` setting
6151
- */
6152
- participants$;
6153
- /**
6154
- * Remote participants of the current call (this includes every participant except the logged-in user).
6155
- */
6156
- remoteParticipants$;
6157
- /**
6158
- * The local participant of the current call (the logged-in user).
6159
- */
6160
- localParticipant$;
6161
- /**
6162
- * Pinned participants of the current call.
6163
- */
6164
- pinnedParticipants$;
6165
- /**
6166
- * The currently elected dominant speaker in the current call.
6167
- */
6168
- dominantSpeaker$;
6169
- /**
6170
- * Emits true whenever there is an active screen sharing session within
6171
- * the current call. Useful for displaying a "screen sharing" indicator and
6172
- * switching the layout to a screen sharing layout.
6173
- *
6174
- * The actual screen sharing track isn't exposed here, but can be retrieved
6175
- * from the list of call participants. We also don't want to be limiting
6176
- * to the number of share screen tracks are displayed in a call.
6177
- */
6178
- hasOngoingScreenShare$;
6159
+ rawParticipants$ = shared(this.participantsSubject.asObservable());
6160
+ /** All participants of the current call sorted according to the current `sortByParticipantsBy` setting. */
6161
+ participants$ = shared(this.participantsSubject.asObservable().pipe(
6162
+ // maintain stable-sort by mutating the participants stored in the original subject
6163
+ map((ps) => ps.sort(this.sortParticipantsBy))));
6164
+ /** The local participant of the current call. */
6165
+ localParticipant$ = shared(this.participants$.pipe(map((ps) => ps.find((p) => p.isLocalParticipant))));
6166
+ /** Remote participants of the current call (all except the local participant). */
6167
+ remoteParticipants$ = shared(this.participants$.pipe(map((ps) => ps.filter((p) => !p.isLocalParticipant))));
6168
+ /** Pinned participants of the current call. */
6169
+ pinnedParticipants$ = shared(this.participants$.pipe(map((ps) => ps.filter((p) => !!p.pin))));
6170
+ /** The currently elected dominant speaker in the current call. */
6171
+ dominantSpeaker$ = shared(this.participants$.pipe(map((ps) => ps.find((p) => p.isDominantSpeaker))));
6172
+ /** Emits true whenever there is an active screen sharing session within the current call. */
6173
+ hasOngoingScreenShare$ = shared(this.participants$.pipe(map((ps) => ps.some((p) => hasScreenShare(p))), distinctUntilChanged()));
6179
6174
  /**
6180
6175
  * The latest stats report of the current call.
6181
6176
  * When stats gathering is enabled, this observable will emit a new value
@@ -6184,232 +6179,66 @@ class CallState {
6184
6179
  * Consumers of this observable can implement their own batching logic
6185
6180
  * in case they want to show historical stats data.
6186
6181
  */
6187
- callStatsReport$;
6188
- /**
6189
- * The list of members in the current call.
6190
- */
6191
- members$;
6192
- /**
6193
- * The list of capabilities of the current user.
6194
- */
6195
- ownCapabilities$;
6196
- /**
6197
- * The calling state.
6198
- */
6199
- callingState$;
6200
- /**
6201
- * The backstage state.
6202
- */
6203
- backstage$;
6204
- /**
6205
- * Will provide the list of blocked user IDs.
6206
- */
6207
- blockedUserIds$;
6208
- /**
6209
- * Will provide the time when this call has been created.
6210
- */
6211
- createdAt$;
6212
- /**
6213
- * Will provide the time when this call has been ended.
6214
- */
6215
- endedAt$;
6216
- /**
6217
- * Will provide the time when this call has been scheduled to start.
6218
- */
6219
- startsAt$;
6220
- /**
6221
- * Will provide the time when this call has been updated.
6222
- */
6223
- updatedAt$;
6224
- /**
6225
- * Will provide the user who created this call.
6226
- */
6227
- createdBy$;
6228
- /**
6229
- * Will provide the custom data of this call.
6230
- */
6231
- custom$;
6232
- /**
6233
- * Will provide the egress data of this call.
6234
- */
6235
- egress$;
6236
- /**
6237
- * Will provide the ingress data of this call.
6238
- */
6239
- ingress$;
6240
- /**
6241
- * Will provide the recording state of this call.
6242
- */
6243
- recording$;
6244
- /**
6245
- * Will provide the recording state of this call.
6246
- */
6247
- individualRecording$;
6248
- /**
6249
- * Will provide the recording state of this call.
6250
- */
6251
- rawRecording$;
6252
- /**
6253
- * Will provide the session data of this call.
6254
- */
6255
- session$;
6256
- /**
6257
- * Will provide the settings of this call.
6258
- */
6259
- settings$;
6260
- /**
6261
- * Will provide the transcribing state of this call.
6262
- */
6263
- transcribing$;
6264
- /**
6265
- * Will provide the closed captioning state of this call.
6266
- */
6267
- captioning$;
6268
- /**
6269
- * Whether end-to-end encryption is active for this call, as reported by the
6270
- * SFU in the join response.
6271
- */
6272
- e2eeEnabled$;
6273
- /**
6274
- * Will provide the user who ended this call.
6275
- */
6276
- endedBy$;
6277
- /**
6278
- * Will provide the thumbnails of this call.
6279
- */
6280
- thumbnails$;
6281
- /**
6282
- * The queue of closed captions.
6283
- */
6284
- closedCaptions$;
6285
- logger = videoLoggerSystem.getLogger('CallState');
6286
- /**
6287
- * A list of comparators that are used to sort the participants.
6288
- */
6289
- sortParticipantsBy = defaultSortPreset;
6290
- /**
6291
- * The closed captions configuration.
6292
- */
6182
+ callStatsReport$ = this.callStatsReportSubject.asObservable();
6183
+ /** The queue of closed captions. */
6184
+ closedCaptions$ = this.closedCaptionsSubject.asObservable();
6185
+ /** The list of capabilities of the current user. */
6186
+ ownCapabilities$ = shared(combineLatest([
6187
+ this.ownCapabilitiesSubject,
6188
+ this.callGrantsSubject.pipe(startWith(undefined)),
6189
+ ]).pipe(map(([capabilities, grants]) => {
6190
+ if (!grants)
6191
+ return capabilities;
6192
+ const { canPublishAudio, canPublishVideo, canScreenshare } = grants;
6193
+ const update = {
6194
+ [OwnCapability.SEND_AUDIO]: canPublishAudio,
6195
+ [OwnCapability.SEND_VIDEO]: canPublishVideo,
6196
+ [OwnCapability.SCREENSHARE]: canScreenshare,
6197
+ };
6198
+ const nextCapabilities = [...capabilities];
6199
+ for (const _capability in update) {
6200
+ const capability = _capability;
6201
+ const allowed = update[capability];
6202
+ // grants take precedence over capabilities, reconstruct the capabilities
6203
+ if (allowed && !nextCapabilities.includes(capability)) {
6204
+ nextCapabilities.push(capability);
6205
+ }
6206
+ else if (!allowed && nextCapabilities.includes(capability)) {
6207
+ const index = nextCapabilities.indexOf(capability);
6208
+ nextCapabilities.splice(index, 1);
6209
+ }
6210
+ }
6211
+ return nextCapabilities;
6212
+ }), distinctUntilChanged(isShallowArrayEqual)));
6213
+ // These are tracks that were delivered to the Subscriber's onTrack event
6214
+ // that we couldn't associate with a participant yet.
6215
+ // This happens when the participantJoined event hasn't been received yet.
6216
+ // We keep these tracks around until we can associate them with a participant.
6217
+ orphanedTracks = [];
6218
+ /** The closed captions configuration. */
6293
6219
  closedCaptionsSettings;
6294
6220
  closedCaptionsTasks = new Map();
6295
6221
  eventHandlers;
6296
- /**
6297
- * Creates a new instance of the CallState class.
6298
- *
6299
- */
6222
+ /** Creates a new instance of the CallState class. */
6300
6223
  constructor() {
6301
- this.rawParticipants$ = this.participantsSubject
6302
- .asObservable()
6303
- .pipe(shareReplay({ bufferSize: 1, refCount: true }));
6304
- this.participants$ = this.participantsSubject.asObservable().pipe(
6305
- // maintain stable-sort by mutating the participants stored
6306
- // in the original subject
6307
- map((ps) => ps.sort(this.sortParticipantsBy)), shareReplay({ bufferSize: 1, refCount: true }));
6308
- this.localParticipant$ = this.participants$.pipe(map((participants) => participants.find((p) => p.isLocalParticipant)), shareReplay({ bufferSize: 1, refCount: true }));
6309
- this.remoteParticipants$ = this.participants$.pipe(map((participants) => participants.filter((p) => !p.isLocalParticipant)), shareReplay({ bufferSize: 1, refCount: true }));
6310
- this.pinnedParticipants$ = this.participants$.pipe(map((participants) => participants.filter((p) => !!p.pin)), shareReplay({ bufferSize: 1, refCount: true }));
6311
- this.dominantSpeaker$ = this.participants$.pipe(map((participants) => participants.find((p) => p.isDominantSpeaker)), shareReplay({ bufferSize: 1, refCount: true }));
6312
- this.hasOngoingScreenShare$ = this.participants$.pipe(map((participants) => participants.some((p) => hasScreenShare(p))), distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: true }));
6313
- // dates
6314
- this.createdAt$ = this.createdAtSubject.asObservable();
6315
- this.endedAt$ = this.endedAtSubject.asObservable();
6316
- this.startsAt$ = this.startsAtSubject.asObservable();
6317
- this.startedAt$ = this.startedAtSubject.asObservable();
6318
- this.updatedAt$ = this.updatedAtSubject.asObservable();
6319
- this.callStatsReport$ = this.callStatsReportSubject.asObservable();
6320
- this.members$ = this.membersSubject.asObservable();
6321
- // complex objects should work as streams of data
6322
- this.createdBy$ = this.createdBySubject.asObservable();
6323
- this.custom$ = this.customSubject.asObservable();
6324
- this.egress$ = this.egressSubject.asObservable();
6325
- this.ingress$ = this.ingressSubject.asObservable();
6326
- this.session$ = this.sessionSubject.asObservable();
6327
- this.settings$ = this.settingsSubject.asObservable();
6328
- this.endedBy$ = this.endedBySubject.asObservable();
6329
- this.thumbnails$ = this.thumbnailsSubject.asObservable();
6330
- this.closedCaptions$ = this.closedCaptionsSubject.asObservable();
6331
- /**
6332
- * Creates an Observable from the given subject by piping to the
6333
- * `distinctUntilChanged()` operator.
6334
- */
6335
- const duc = (subject, comparator) => subject.pipe(distinctUntilChanged(comparator));
6336
- // primitive values should only emit once the value they hold changes
6337
- this.anonymousParticipantCount$ = duc(this.anonymousParticipantCountSubject);
6338
- this.blockedUserIds$ = duc(this.blockedUserIdsSubject, isShallowArrayEqual);
6339
- this.backstage$ = duc(this.backstageSubject);
6340
- this.callingState$ = duc(this.callingStateSubject);
6341
- this.ownCapabilities$ = combineLatest([
6342
- this.ownCapabilitiesSubject,
6343
- this.callGrantsSubject.pipe(startWith(undefined)),
6344
- ]).pipe(map(([capabilities, grants]) => {
6345
- if (!grants)
6346
- return capabilities;
6347
- const { canPublishAudio, canPublishVideo, canScreenshare } = grants;
6348
- const update = {
6349
- [OwnCapability.SEND_AUDIO]: canPublishAudio,
6350
- [OwnCapability.SEND_VIDEO]: canPublishVideo,
6351
- [OwnCapability.SCREENSHARE]: canScreenshare,
6352
- };
6353
- const nextCapabilities = [...capabilities];
6354
- for (const _capability in update) {
6355
- const capability = _capability;
6356
- const allowed = update[capability];
6357
- // grants take precedence over capabilities, reconstruct the capabilities
6358
- if (allowed && !nextCapabilities.includes(capability)) {
6359
- nextCapabilities.push(capability);
6360
- }
6361
- else if (!allowed && nextCapabilities.includes(capability)) {
6362
- const index = nextCapabilities.indexOf(capability);
6363
- nextCapabilities.splice(index, 1);
6364
- }
6365
- }
6366
- return nextCapabilities;
6367
- }), distinctUntilChanged(isShallowArrayEqual), shareReplay({ bufferSize: 1, refCount: true }));
6368
- this.participantCount$ = duc(this.participantCountSubject);
6369
- this.recording$ = duc(this.recordingSubject);
6370
- this.individualRecording$ = duc(this.individualRecordingSubject);
6371
- this.rawRecording$ = duc(this.rawRecordingSubject);
6372
- this.transcribing$ = duc(this.transcribingSubject);
6373
- this.captioning$ = duc(this.captioningSubject);
6374
- this.e2eeEnabled$ = duc(this.e2eeEnabledSubject);
6375
6224
  this.eventHandlers = {
6376
- // these events are not updating the call state:
6377
- 'call.frame_recording_ready': undefined,
6378
- 'call.kicked_user': undefined,
6379
- 'call.moderation_blur': undefined,
6380
- 'call.moderation_warning': undefined,
6381
- 'call.permission_request': undefined,
6382
- 'call.recording_ready': undefined,
6383
- 'call.rtmp_broadcast_failed': undefined,
6384
- 'call.rtmp_broadcast_started': undefined,
6385
- 'call.rtmp_broadcast_stopped': undefined,
6386
- 'call.stats_report_ready': undefined,
6387
- 'call.transcription_ready': undefined,
6388
- 'call.user_feedback_submitted': undefined,
6389
- 'call.user_muted': undefined,
6390
- 'connection.error': undefined,
6391
- 'connection.ok': undefined,
6392
- 'health.check': undefined,
6393
- 'user.updated': undefined,
6394
- custom: undefined,
6395
- // events that update call state:
6396
6225
  'call.accepted': (e) => this.updateFromCallResponse(e.call),
6397
6226
  'call.blocked_user': this.blockUser,
6398
6227
  'call.closed_caption': this.updateFromClosedCaptions,
6399
6228
  'call.closed_captions_failed': () => {
6400
- this.setCurrentValue(this.captioningSubject, false);
6229
+ setCurrentValue(this.captioningSubject, false);
6401
6230
  },
6402
6231
  'call.closed_captions_started': () => {
6403
- this.setCurrentValue(this.captioningSubject, true);
6232
+ setCurrentValue(this.captioningSubject, true);
6404
6233
  },
6405
6234
  'call.closed_captions_stopped': () => {
6406
- this.setCurrentValue(this.captioningSubject, false);
6235
+ setCurrentValue(this.captioningSubject, false);
6407
6236
  },
6408
6237
  'call.created': (e) => this.updateFromCallResponse(e.call),
6409
6238
  'call.deleted': (e) => this.updateFromCallResponse(e.call),
6410
6239
  'call.ended': (e) => {
6411
6240
  this.updateFromCallResponse(e.call);
6412
- this.setCurrentValue(this.endedBySubject, e.user);
6241
+ setCurrentValue(this.endedBySubject, e.user);
6413
6242
  },
6414
6243
  'call.frame_recording_failed': (e) => {
6415
6244
  this.updateFromCallResponse(e.call);
@@ -6454,21 +6283,19 @@ class CallState {
6454
6283
  'call.session_participant_left': this.updateFromSessionParticipantLeft,
6455
6284
  'call.session_started': (e) => this.updateFromCallResponse(e.call),
6456
6285
  'call.transcription_started': () => {
6457
- this.setCurrentValue(this.transcribingSubject, true);
6286
+ setCurrentValue(this.transcribingSubject, true);
6458
6287
  },
6459
6288
  'call.transcription_stopped': () => {
6460
- this.setCurrentValue(this.transcribingSubject, false);
6289
+ setCurrentValue(this.transcribingSubject, false);
6461
6290
  },
6462
6291
  'call.transcription_failed': () => {
6463
- this.setCurrentValue(this.transcribingSubject, false);
6292
+ setCurrentValue(this.transcribingSubject, false);
6464
6293
  },
6465
6294
  'call.unblocked_user': this.unblockUser,
6466
6295
  'call.updated': (e) => this.updateFromCallResponse(e.call),
6467
6296
  };
6468
6297
  }
6469
- /**
6470
- * Runs the cleanup tasks.
6471
- */
6298
+ /** Runs the cleanup tasks. */
6472
6299
  dispose = () => {
6473
6300
  for (const [ccKey, taskId] of this.closedCaptionsTasks.entries()) {
6474
6301
  clearTimeout(taskId);
@@ -6485,39 +6312,18 @@ class CallState {
6485
6312
  setSortParticipantsBy = (comparator) => {
6486
6313
  this.sortParticipantsBy = comparator;
6487
6314
  // trigger re-sorting of participants
6488
- this.setCurrentValue(this.participantsSubject, (ps) => ps);
6315
+ setCurrentValue(this.participantsSubject, (ps) => ps);
6489
6316
  };
6490
- /**
6491
- * Returns the comparator currently used to sort the participants.
6492
- */
6317
+ /** Returns the comparator currently used to sort the participants. */
6493
6318
  getSortParticipantsBy = () => {
6494
6319
  return this.sortParticipantsBy;
6495
6320
  };
6496
- /**
6497
- * Gets the current value of an observable, or undefined if the observable has
6498
- * not emitted a value yet.
6499
- *
6500
- * @param observable$ the observable to get the value from.
6501
- */
6502
- getCurrentValue = getCurrentValue;
6503
- /**
6504
- * Updates the value of the provided Subject.
6505
- * An `update` can either be a new value or a function which takes
6506
- * the current value and returns a new value.
6507
- *
6508
- * @internal
6509
- *
6510
- * @param subject the subject to update.
6511
- * @param update the update to apply to the subject.
6512
- * @return the updated value.
6513
- */
6514
- setCurrentValue = setCurrentValue;
6515
6321
  /**
6516
6322
  * The server-side counted number of participants connected to the current call.
6517
6323
  * This number includes the anonymous participants as well.
6518
6324
  */
6519
6325
  get participantCount() {
6520
- return this.getCurrentValue(this.participantCount$);
6326
+ return getCurrentValue(this.participantCountSubject);
6521
6327
  }
6522
6328
  /**
6523
6329
  * Sets the number of participants in the current call.
@@ -6526,14 +6332,14 @@ class CallState {
6526
6332
  * @param count the number of participants.
6527
6333
  */
6528
6334
  setParticipantCount = (count) => {
6529
- return this.setCurrentValue(this.participantCountSubject, count);
6335
+ return setCurrentValue(this.participantCountSubject, count);
6530
6336
  };
6531
6337
  /**
6532
6338
  * The time the call session actually started.
6533
6339
  * Useful for displaying the call duration.
6534
6340
  */
6535
6341
  get startedAt() {
6536
- return this.getCurrentValue(this.startedAt$);
6342
+ return getCurrentValue(this.startedAtSubject);
6537
6343
  }
6538
6344
  /**
6539
6345
  * Sets the time the call session actually started.
@@ -6542,13 +6348,11 @@ class CallState {
6542
6348
  * @param startedAt the time the call session actually started.
6543
6349
  */
6544
6350
  setStartedAt = (startedAt) => {
6545
- return this.setCurrentValue(this.startedAtSubject, startedAt);
6351
+ return setCurrentValue(this.startedAtSubject, startedAt);
6546
6352
  };
6547
- /**
6548
- * Returns whether closed captions are enabled in the current call.
6549
- */
6353
+ /** Returns whether closed captions are enabled in the current call. */
6550
6354
  get captioning() {
6551
- return this.getCurrentValue(this.captioning$);
6355
+ return getCurrentValue(this.captioningSubject);
6552
6356
  }
6553
6357
  /**
6554
6358
  * Sets the closed captioning state of the current call.
@@ -6564,7 +6368,7 @@ class CallState {
6564
6368
  * This number includes the anonymous participants as well.
6565
6369
  */
6566
6370
  get anonymousParticipantCount() {
6567
- return this.getCurrentValue(this.anonymousParticipantCount$);
6371
+ return getCurrentValue(this.anonymousParticipantCountSubject);
6568
6372
  }
6569
6373
  /**
6570
6374
  * Sets the number of anonymous participants in the current call.
@@ -6573,19 +6377,15 @@ class CallState {
6573
6377
  * @param count the number of anonymous participants.
6574
6378
  */
6575
6379
  setAnonymousParticipantCount = (count) => {
6576
- return this.setCurrentValue(this.anonymousParticipantCountSubject, count);
6380
+ return setCurrentValue(this.anonymousParticipantCountSubject, count);
6577
6381
  };
6578
- /**
6579
- * The list of participants in the current call.
6580
- */
6382
+ /** The list of participants in the current call. */
6581
6383
  get participants() {
6582
- return this.getCurrentValue(this.participants$);
6384
+ return getCurrentValue(this.participants$);
6583
6385
  }
6584
- /**
6585
- * The stable list of participants in the current call, unsorted.
6586
- */
6386
+ /** The stable list of participants in the current call, unsorted. */
6587
6387
  get rawParticipants() {
6588
- return this.getCurrentValue(this.rawParticipants$);
6388
+ return getCurrentValue(this.rawParticipants$);
6589
6389
  }
6590
6390
  /**
6591
6391
  * Returns the current participants array directly from the BehaviorSubject.
@@ -6606,43 +6406,31 @@ class CallState {
6606
6406
  * @param participants the list of participants.
6607
6407
  */
6608
6408
  setParticipants = (participants) => {
6609
- return this.setCurrentValue(this.participantsSubject, participants);
6409
+ return setCurrentValue(this.participantsSubject, participants);
6610
6410
  };
6611
- /**
6612
- * The local participant in the current call.
6613
- */
6411
+ /** The local participant in the current call. */
6614
6412
  get localParticipant() {
6615
- return this.getCurrentValue(this.localParticipant$);
6413
+ return getCurrentValue(this.localParticipant$);
6616
6414
  }
6617
- /**
6618
- * The list of remote participants in the current call.
6619
- */
6415
+ /** The list of remote participants in the current call. */
6620
6416
  get remoteParticipants() {
6621
- return this.getCurrentValue(this.remoteParticipants$);
6417
+ return getCurrentValue(this.remoteParticipants$);
6622
6418
  }
6623
- /**
6624
- * The dominant speaker in the current call.
6625
- */
6419
+ /** The dominant speaker in the current call. */
6626
6420
  get dominantSpeaker() {
6627
- return this.getCurrentValue(this.dominantSpeaker$);
6421
+ return getCurrentValue(this.dominantSpeaker$);
6628
6422
  }
6629
- /**
6630
- * The list of pinned participants in the current call.
6631
- */
6423
+ /** The list of pinned participants in the current call. */
6632
6424
  get pinnedParticipants() {
6633
- return this.getCurrentValue(this.pinnedParticipants$);
6425
+ return getCurrentValue(this.pinnedParticipants$);
6634
6426
  }
6635
- /**
6636
- * Tell if there is an ongoing screen share in this call.
6637
- */
6427
+ /** Tell if there is an ongoing screen share in this call. */
6638
6428
  get hasOngoingScreenShare() {
6639
- return this.getCurrentValue(this.hasOngoingScreenShare$);
6429
+ return getCurrentValue(this.hasOngoingScreenShare$);
6640
6430
  }
6641
- /**
6642
- * The calling state.
6643
- */
6431
+ /** The calling state. */
6644
6432
  get callingState() {
6645
- return this.getCurrentValue(this.callingState$);
6433
+ return getCurrentValue(this.callingStateSubject);
6646
6434
  }
6647
6435
  /**
6648
6436
  * Sets the calling state.
@@ -6651,13 +6439,11 @@ class CallState {
6651
6439
  * @param state the new calling state.
6652
6440
  */
6653
6441
  setCallingState = (state) => {
6654
- return this.setCurrentValue(this.callingStateSubject, state);
6442
+ return setCurrentValue(this.callingStateSubject, state);
6655
6443
  };
6656
- /**
6657
- * The call stats report.
6658
- */
6444
+ /** The call stats report. */
6659
6445
  get callStatsReport() {
6660
- return this.getCurrentValue(this.callStatsReport$);
6446
+ return getCurrentValue(this.callStatsReportSubject);
6661
6447
  }
6662
6448
  /**
6663
6449
  * Returns whether the call stats report is being observed or not.
@@ -6673,13 +6459,11 @@ class CallState {
6673
6459
  * @param report the report to set.
6674
6460
  */
6675
6461
  setCallStatsReport = (report) => {
6676
- return this.setCurrentValue(this.callStatsReportSubject, report);
6462
+ return setCurrentValue(this.callStatsReportSubject, report);
6677
6463
  };
6678
- /**
6679
- * The members of the current call.
6680
- */
6464
+ /** The members of the current call. */
6681
6465
  get members() {
6682
- return this.getCurrentValue(this.members$);
6466
+ return getCurrentValue(this.membersSubject);
6683
6467
  }
6684
6468
  /**
6685
6469
  * Sets the members of the current call.
@@ -6688,13 +6472,11 @@ class CallState {
6688
6472
  * @param members the members to set.
6689
6473
  */
6690
6474
  setMembers = (members) => {
6691
- this.setCurrentValue(this.membersSubject, members);
6475
+ setCurrentValue(this.membersSubject, members);
6692
6476
  };
6693
- /**
6694
- * The capabilities of the current user for the current call.
6695
- */
6477
+ /** The capabilities of the current user for the current call. */
6696
6478
  get ownCapabilities() {
6697
- return this.getCurrentValue(this.ownCapabilities$);
6479
+ return getCurrentValue(this.ownCapabilities$);
6698
6480
  }
6699
6481
  /**
6700
6482
  * Sets the own capabilities.
@@ -6703,7 +6485,7 @@ class CallState {
6703
6485
  * @param capabilities the capabilities to set.
6704
6486
  */
6705
6487
  setOwnCapabilities = (capabilities) => {
6706
- return this.setCurrentValue(this.ownCapabilitiesSubject, capabilities);
6488
+ return setCurrentValue(this.ownCapabilitiesSubject, capabilities);
6707
6489
  };
6708
6490
  /**
6709
6491
  * Sets the call grants (used for own capabilities).
@@ -6712,141 +6494,105 @@ class CallState {
6712
6494
  * @param grants the grants to set.
6713
6495
  */
6714
6496
  setCallGrants = (grants) => {
6715
- return this.setCurrentValue(this.callGrantsSubject, grants);
6497
+ return setCurrentValue(this.callGrantsSubject, grants);
6716
6498
  };
6717
- /**
6718
- * The backstage state.
6719
- */
6499
+ /** The backstage state. */
6720
6500
  get backstage() {
6721
- return this.getCurrentValue(this.backstage$);
6501
+ return getCurrentValue(this.backstageSubject);
6722
6502
  }
6723
6503
  /**
6724
6504
  * Sets the backstage state.
6725
6505
  * @param backstage the backstage state.
6726
6506
  */
6727
6507
  setBackstage = (backstage) => {
6728
- return this.setCurrentValue(this.backstageSubject, backstage);
6508
+ return setCurrentValue(this.backstageSubject, backstage);
6729
6509
  };
6730
- /**
6731
- * Will provide the list of blocked user IDs.
6732
- */
6510
+ /** Will provide the list of blocked user IDs. */
6733
6511
  get blockedUserIds() {
6734
- return this.getCurrentValue(this.blockedUserIds$);
6512
+ return getCurrentValue(this.blockedUserIdsSubject);
6735
6513
  }
6736
- /**
6737
- * Will provide the time when this call has been created.
6738
- */
6514
+ /** Will provide the time when this call has been created. */
6739
6515
  get createdAt() {
6740
- return this.getCurrentValue(this.createdAt$);
6516
+ return getCurrentValue(this.createdAtSubject);
6741
6517
  }
6742
- /**
6743
- * Will provide the time when this call has been ended.
6744
- */
6518
+ /** Will provide the time when this call has been ended. */
6745
6519
  get endedAt() {
6746
- return this.getCurrentValue(this.endedAt$);
6520
+ return getCurrentValue(this.endedAtSubject);
6747
6521
  }
6748
6522
  /**
6749
6523
  * Sets the time when this call has been ended.
6750
6524
  * @param endedAt the time when this call has been ended.
6751
6525
  */
6752
6526
  setEndedAt = (endedAt) => {
6753
- return this.setCurrentValue(this.endedAtSubject, endedAt);
6527
+ return setCurrentValue(this.endedAtSubject, endedAt);
6754
6528
  };
6755
- /**
6756
- * Will provide the time when this call has been scheduled to start.
6757
- */
6529
+ /** Will provide the time when this call has been scheduled to start. */
6758
6530
  get startsAt() {
6759
- return this.getCurrentValue(this.startsAt$);
6531
+ return getCurrentValue(this.startsAtSubject);
6760
6532
  }
6761
- /**
6762
- * Will provide the time when this call has been updated.
6763
- */
6533
+ /** Will provide the time when this call has been updated. */
6764
6534
  get updatedAt() {
6765
- return this.getCurrentValue(this.updatedAt$);
6535
+ return getCurrentValue(this.updatedAtSubject);
6766
6536
  }
6767
- /**
6768
- * Will provide the user who created this call.
6769
- */
6537
+ /** Will provide the user who created this call. */
6770
6538
  get createdBy() {
6771
- return this.getCurrentValue(this.createdBy$);
6539
+ return getCurrentValue(this.createdBySubject);
6772
6540
  }
6773
- /**
6774
- * Will provide the custom data of this call.
6775
- */
6541
+ /** Will provide the custom data of this call. */
6776
6542
  get custom() {
6777
- return this.getCurrentValue(this.custom$);
6543
+ return getCurrentValue(this.customSubject);
6778
6544
  }
6779
6545
  /**
6780
6546
  * Will provide the egress data of this call.
6781
6547
  */
6782
6548
  get egress() {
6783
- return this.getCurrentValue(this.egress$);
6549
+ return getCurrentValue(this.egressSubject);
6784
6550
  }
6785
- /**
6786
- * Will provide the ingress data of this call.
6787
- */
6551
+ /** Will provide the ingress data of this call. */
6788
6552
  get ingress() {
6789
- return this.getCurrentValue(this.ingress$);
6553
+ return getCurrentValue(this.ingressSubject);
6790
6554
  }
6791
- /**
6792
- * Will provide the composite recording state of this call.
6793
- */
6555
+ /** Will provide the composite recording state of this call. */
6794
6556
  get recording() {
6795
- return this.getCurrentValue(this.recording$);
6557
+ return getCurrentValue(this.recordingSubject);
6796
6558
  }
6797
- /**
6798
- * Will provide the individual recording state of this call.
6799
- */
6559
+ /** Will provide the individual recording state of this call. */
6800
6560
  get individualRecording() {
6801
- return this.getCurrentValue(this.individualRecording$);
6561
+ return getCurrentValue(this.individualRecordingSubject);
6802
6562
  }
6803
- /**
6804
- * Will provide the raw recording state of this call.
6805
- */
6563
+ /** Will provide the raw recording state of this call. */
6806
6564
  get rawRecording() {
6807
- return this.getCurrentValue(this.rawRecording$);
6565
+ return getCurrentValue(this.rawRecordingSubject);
6808
6566
  }
6809
- /**
6810
- * Will provide the session data of this call.
6811
- */
6567
+ /** Will provide the session data of this call. */
6812
6568
  get session() {
6813
- return this.getCurrentValue(this.session$);
6569
+ return getCurrentValue(this.sessionSubject);
6814
6570
  }
6815
- /**
6816
- * Will provide the settings of this call.
6817
- */
6571
+ /** Will provide the settings of this call. */
6818
6572
  get settings() {
6819
- return this.getCurrentValue(this.settings$);
6573
+ return getCurrentValue(this.settingsSubject);
6820
6574
  }
6821
- /**
6822
- * Will provide the transcribing state of this call.
6823
- */
6575
+ /** Will provide the transcribing state of this call. */
6824
6576
  get transcribing() {
6825
- return this.getCurrentValue(this.transcribing$);
6577
+ return getCurrentValue(this.transcribingSubject);
6826
6578
  }
6827
- /**
6828
- * Whether end-to-end encryption is active for this call.
6829
- */
6579
+ /** Whether end-to-end encryption is active for this call. */
6830
6580
  get e2eeEnabled() {
6831
- return this.getCurrentValue(this.e2eeEnabled$);
6581
+ return getCurrentValue(this.e2eeEnabledSubject);
6832
6582
  }
6833
- /**
6834
- * Will provide the user who ended this call.
6835
- */
6583
+ /** Will provide the user who ended this call. */
6836
6584
  get endedBy() {
6837
- return this.getCurrentValue(this.endedBy$);
6585
+ return getCurrentValue(this.endedBySubject);
6838
6586
  }
6839
6587
  /**
6840
6588
  * Will provide the thumbnails of this call, if enabled in the call settings.
6841
6589
  */
6842
6590
  get thumbnails() {
6843
- return this.getCurrentValue(this.thumbnails$);
6591
+ return getCurrentValue(this.thumbnailsSubject);
6844
6592
  }
6845
- /**
6846
- * Returns the current queue of closed captions.
6847
- */
6593
+ /** Returns the current queue of closed captions. */
6848
6594
  get closedCaptions() {
6849
- return this.getCurrentValue(this.closedCaptions$);
6595
+ return getCurrentValue(this.closedCaptionsSubject);
6850
6596
  }
6851
6597
  /**
6852
6598
  * Will try to find the participant with the given sessionId in the current call.
@@ -6857,9 +6603,7 @@ class CallState {
6857
6603
  findParticipantBySessionId = (sessionId) => {
6858
6604
  return this.participants.find((p) => p.sessionId === sessionId);
6859
6605
  };
6860
- /**
6861
- * Returns a new lookup table of participants indexed by their session ID.
6862
- */
6606
+ /** Returns a new lookup table of participants indexed by their session ID. */
6863
6607
  getParticipantLookupBySessionId = () => {
6864
6608
  return this.participants.reduce((lookupTable, participant) => {
6865
6609
  lookupTable[participant.sessionId] = participant;
@@ -7094,25 +6838,49 @@ class CallState {
7094
6838
  */
7095
6839
  updateFromCallResponse = (call) => {
7096
6840
  this.setBackstage(call.backstage);
7097
- this.setCurrentValue(this.blockedUserIdsSubject, call.blocked_user_ids);
7098
- this.setCurrentValue(this.createdAtSubject, new Date(call.created_at));
7099
- this.setCurrentValue(this.updatedAtSubject, new Date(call.updated_at));
7100
- this.setCurrentValue(this.startsAtSubject, call.starts_at ? new Date(call.starts_at) : undefined);
6841
+ setCurrentValue(this.blockedUserIdsSubject, call.blocked_user_ids);
6842
+ setCurrentValue(this.createdAtSubject, new Date(call.created_at));
6843
+ setCurrentValue(this.updatedAtSubject, new Date(call.updated_at));
6844
+ setCurrentValue(this.startsAtSubject, call.starts_at ? new Date(call.starts_at) : undefined);
7101
6845
  this.setEndedAt(call.ended_at ? new Date(call.ended_at) : undefined);
7102
- this.setCurrentValue(this.createdBySubject, call.created_by);
7103
- this.setCurrentValue(this.customSubject, call.custom);
7104
- this.setCurrentValue(this.egressSubject, call.egress);
7105
- this.setCurrentValue(this.ingressSubject, call.ingress);
6846
+ setCurrentValue(this.createdBySubject, call.created_by);
6847
+ setCurrentValue(this.customSubject, call.custom);
6848
+ setCurrentValue(this.egressSubject, call.egress);
6849
+ setCurrentValue(this.ingressSubject, call.ingress);
7106
6850
  const { individual_recording, composite_recording, raw_recording } = call.egress;
7107
- this.setCurrentValue(this.recordingSubject, call.recording || composite_recording?.status === 'running');
7108
- this.setCurrentValue(this.individualRecordingSubject, individual_recording?.status === 'running');
7109
- this.setCurrentValue(this.rawRecordingSubject, raw_recording?.status === 'running');
7110
- const s = this.setCurrentValue(this.sessionSubject, call.session);
6851
+ setCurrentValue(this.recordingSubject, call.recording || composite_recording?.status === 'running');
6852
+ setCurrentValue(this.individualRecordingSubject, individual_recording?.status === 'running');
6853
+ setCurrentValue(this.rawRecordingSubject, raw_recording?.status === 'running');
6854
+ const s = setCurrentValue(this.sessionSubject, call.session);
7111
6855
  this.updateParticipantCountFromSession(s);
7112
- this.setCurrentValue(this.settingsSubject, call.settings);
7113
- this.setCurrentValue(this.transcribingSubject, call.transcribing);
7114
- this.setCurrentValue(this.captioningSubject, call.captioning);
7115
- this.setCurrentValue(this.thumbnailsSubject, call.thumbnails);
6856
+ setCurrentValue(this.settingsSubject, call.settings);
6857
+ setCurrentValue(this.transcribingSubject, call.transcribing);
6858
+ setCurrentValue(this.captioningSubject, call.captioning);
6859
+ setCurrentValue(this.thumbnailsSubject, call.thumbnails);
6860
+ };
6861
+ /**
6862
+ * Merges a polled ring state into the current session. Only the ring fields
6863
+ * are touched: the endpoint does not return the session roster.
6864
+ *
6865
+ * @internal
6866
+ *
6867
+ * @param ringState the ring state as returned by the coordinator.
6868
+ */
6869
+ updateFromRingState = (ringState) => {
6870
+ setCurrentValue(this.sessionSubject, (session) => {
6871
+ if (!session || session.id !== ringState.session_id)
6872
+ return session;
6873
+ return {
6874
+ ...session,
6875
+ accepted_by: ringState.accepted_by,
6876
+ rejected_by: ringState.rejected_by,
6877
+ missed_by: ringState.missed_by,
6878
+ ended_at: ringState.session_ended_at ?? session.ended_at,
6879
+ };
6880
+ });
6881
+ if (ringState.call_ended_at) {
6882
+ this.setEndedAt(new Date(ringState.call_ended_at));
6883
+ }
7116
6884
  };
7117
6885
  /**
7118
6886
  * Updates the call state with the data received from the SFU server.
@@ -7150,21 +6918,21 @@ class CallState {
7150
6918
  this.setAnonymousParticipantCount(participantCount?.anonymous || 0);
7151
6919
  this.setStartedAt(startedAt ? Timestamp.toDate(startedAt) : new Date());
7152
6920
  this.setServerSidePins(pins);
7153
- this.setCurrentValue(this.e2eeEnabledSubject, e2EeEnabled);
6921
+ setCurrentValue(this.e2eeEnabledSubject, e2EeEnabled);
7154
6922
  };
7155
6923
  updateFromMemberRemoved = (event) => {
7156
6924
  this.updateFromCallResponse(event.call);
7157
- this.setCurrentValue(this.membersSubject, (members) => members.filter((m) => event.members.indexOf(m.user_id) === -1));
6925
+ setCurrentValue(this.membersSubject, (members) => members.filter((m) => event.members.indexOf(m.user_id) === -1));
7158
6926
  };
7159
6927
  updateFromMemberAdded = (event) => {
7160
6928
  this.updateFromCallResponse(event.call);
7161
- this.setCurrentValue(this.membersSubject, (members) => [
6929
+ setCurrentValue(this.membersSubject, (members) => [
7162
6930
  ...members,
7163
6931
  ...event.members,
7164
6932
  ]);
7165
6933
  };
7166
6934
  updateFromHLSBroadcastStopped = () => {
7167
- this.setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
6935
+ setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
7168
6936
  ...egress,
7169
6937
  broadcasting: false,
7170
6938
  hls: {
@@ -7174,7 +6942,7 @@ class CallState {
7174
6942
  }));
7175
6943
  };
7176
6944
  updateFromHLSBroadcastingFailed = () => {
7177
- this.setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
6945
+ setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
7178
6946
  ...egress,
7179
6947
  broadcasting: false,
7180
6948
  hls: {
@@ -7186,13 +6954,13 @@ class CallState {
7186
6954
  updateFromRecordingEvent = (type, running) => {
7187
6955
  // handle the legacy format, where `type` is absent in the emitted events
7188
6956
  if (type === undefined || type === 'composite') {
7189
- this.setCurrentValue(this.recordingSubject, running);
6957
+ setCurrentValue(this.recordingSubject, running);
7190
6958
  }
7191
6959
  else if (type === 'individual') {
7192
- this.setCurrentValue(this.individualRecordingSubject, running);
6960
+ setCurrentValue(this.individualRecordingSubject, running);
7193
6961
  }
7194
6962
  else if (type === 'raw') {
7195
- this.setCurrentValue(this.rawRecordingSubject, running);
6963
+ setCurrentValue(this.rawRecordingSubject, running);
7196
6964
  }
7197
6965
  else {
7198
6966
  ensureExhausted(type, 'Unknown recording type');
@@ -7209,7 +6977,7 @@ class CallState {
7209
6977
  this.setAnonymousParticipantCount(session.anonymous_participant_count || 0);
7210
6978
  };
7211
6979
  updateFromSessionParticipantCountUpdate = (event) => {
7212
- const s = this.setCurrentValue(this.sessionSubject, (session) => {
6980
+ const s = setCurrentValue(this.sessionSubject, (session) => {
7213
6981
  if (!session)
7214
6982
  return session;
7215
6983
  return {
@@ -7221,7 +6989,7 @@ class CallState {
7221
6989
  this.updateParticipantCountFromSession(s);
7222
6990
  };
7223
6991
  updateFromSessionParticipantLeft = (event) => {
7224
- const s = this.setCurrentValue(this.sessionSubject, (session) => {
6992
+ const s = setCurrentValue(this.sessionSubject, (session) => {
7225
6993
  if (!session)
7226
6994
  return session;
7227
6995
  const { participants, participants_count_by_role } = session;
@@ -7238,7 +7006,7 @@ class CallState {
7238
7006
  this.updateParticipantCountFromSession(s);
7239
7007
  };
7240
7008
  updateFromSessionParticipantJoined = (event) => {
7241
- const s = this.setCurrentValue(this.sessionSubject, (session) => {
7009
+ const s = setCurrentValue(this.sessionSubject, (session) => {
7242
7010
  if (!session)
7243
7011
  return session;
7244
7012
  const { participants, participants_count_by_role } = session;
@@ -7275,7 +7043,7 @@ class CallState {
7275
7043
  };
7276
7044
  updateMembers = (event) => {
7277
7045
  this.updateFromCallResponse(event.call);
7278
- this.setCurrentValue(this.membersSubject, (members) => members.map((member) => {
7046
+ setCurrentValue(this.membersSubject, (members) => members.map((member) => {
7279
7047
  const memberUpdate = event.members.find((m) => m.user_id === member.user_id);
7280
7048
  return memberUpdate ? memberUpdate : member;
7281
7049
  }));
@@ -7300,14 +7068,14 @@ class CallState {
7300
7068
  });
7301
7069
  };
7302
7070
  unblockUser = (event) => {
7303
- this.setCurrentValue(this.blockedUserIdsSubject, (current) => {
7071
+ setCurrentValue(this.blockedUserIdsSubject, (current) => {
7304
7072
  if (!current)
7305
7073
  return current;
7306
7074
  return current.filter((id) => id !== event.user.id);
7307
7075
  });
7308
7076
  };
7309
7077
  blockUser = (event) => {
7310
- this.setCurrentValue(this.blockedUserIdsSubject, (current) => [
7078
+ setCurrentValue(this.blockedUserIdsSubject, (current) => [
7311
7079
  ...(current || []),
7312
7080
  event.user.id,
7313
7081
  ]);
@@ -7318,7 +7086,7 @@ class CallState {
7318
7086
  }
7319
7087
  };
7320
7088
  updateFromClosedCaptions = (event) => {
7321
- this.setCurrentValue(this.closedCaptionsSubject, (queue) => {
7089
+ setCurrentValue(this.closedCaptionsSubject, (queue) => {
7322
7090
  const { closed_caption } = event;
7323
7091
  const keyOf = (c) => `${c.speaker_id}/${c.start_time}`;
7324
7092
  const currentKey = keyOf(closed_caption);
@@ -7330,7 +7098,7 @@ class CallState {
7330
7098
  // schedule the removal of the closed caption after the retention time
7331
7099
  if (visibilityDurationMs > 0) {
7332
7100
  const taskId = setTimeout(() => {
7333
- this.setCurrentValue(this.closedCaptionsSubject, (captions) => captions.filter((caption) => caption !== closed_caption));
7101
+ setCurrentValue(this.closedCaptionsSubject, (captions) => captions.filter((caption) => caption !== closed_caption));
7334
7102
  this.closedCaptionsTasks.delete(currentKey);
7335
7103
  }, visibilityDurationMs);
7336
7104
  this.closedCaptionsTasks.set(currentKey, taskId);
@@ -7425,6 +7193,19 @@ const getSdkName = (sdk) => {
7425
7193
  const getSdkVersion = (sdk) => {
7426
7194
  return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
7427
7195
  };
7196
+ /**
7197
+ * Returns the identifier the SFU receives in the `X-Stream-Client` header,
7198
+ * e.g. `stream-video-react-v1.43.1`. Matches the format the coordinator
7199
+ * already gets through the client's user agent.
7200
+ */
7201
+ const getStreamClientId = (sdk) => {
7202
+ const sdkPlatforms = {
7203
+ [SdkType.REACT]: 'react',
7204
+ [SdkType.REACT_NATIVE]: 'react-native',
7205
+ };
7206
+ const platform = (sdk && sdkPlatforms[sdk.type]) || 'js';
7207
+ return `stream-video-${platform}-v${getSdkVersion(sdk)}`;
7208
+ };
7428
7209
 
7429
7210
  /**
7430
7211
  * Creates a new StatsReporter instance that collects metrics about the ongoing call and reports them to the state store
@@ -9109,6 +8890,25 @@ class TransceiverCache {
9109
8890
  this.cache.push(bundle);
9110
8891
  this.transceiverOrder.push(bundle.transceiver);
9111
8892
  };
8893
+ /**
8894
+ * Retires a failed sender from the reusable cache.
8895
+ *
8896
+ * A transceiver that never reached an m-section also leaves the creation
8897
+ * history: once stopped, it is left out of every later offer, so counting
8898
+ * it would shift the `mid` index of every transceiver added after it. One
8899
+ * that is already associated keeps its slot, since its m-section survives
8900
+ * as a rejected one.
8901
+ */
8902
+ remove = (transceiver) => {
8903
+ const index = this.cache.findIndex((b) => b.transceiver === transceiver);
8904
+ if (index !== -1)
8905
+ this.cache.splice(index, 1);
8906
+ if (transceiver.mid)
8907
+ return;
8908
+ const orderIndex = this.transceiverOrder.indexOf(transceiver);
8909
+ if (orderIndex !== -1)
8910
+ this.transceiverOrder.splice(orderIndex, 1);
8911
+ };
9112
8912
  /**
9113
8913
  * Gets the transceiver for the given publish option.
9114
8914
  */
@@ -9280,7 +9080,9 @@ class Publisher extends BasePeerConnection {
9280
9080
  const sendEncodings = isSvcCodec(publishOption.codec?.name)
9281
9081
  ? toSvcEncodings(encodings)
9282
9082
  : encodings;
9283
- const transceiver = this.pc.addTransceiver(track, {
9083
+ // An E2EE sender must never have media before its transform is attached,
9084
+ // including when another track or an ICE restart negotiates this PC.
9085
+ const transceiver = this.pc.addTransceiver(this.e2ee ? track.kind : track, {
9284
9086
  direction: 'sendonly',
9285
9087
  sendEncodings,
9286
9088
  });
@@ -9290,16 +9092,63 @@ class Publisher extends BasePeerConnection {
9290
9092
  this.transceiverCache.add({ publishOption, transceiver, options });
9291
9093
  this.trackIdToTrackType.set(track.id, trackType);
9292
9094
  this.logger.debug(`Added ${TrackType[trackType]} transceiver`);
9293
- const params = transceiver.sender.getParameters();
9095
+ if (this.e2ee) {
9096
+ await this.initEncryptedSender(transceiver, track, publishOption);
9097
+ }
9098
+ else {
9099
+ await this.setDegradationPreference(transceiver.sender, publishOption);
9100
+ }
9101
+ await this.negotiate();
9102
+ };
9103
+ /**
9104
+ * Attaches the encryptor, applies sender parameters, then attaches the track.
9105
+ *
9106
+ * `encrypt` must run before the first await: Chrome short-circuits a sender
9107
+ * whose encoded streams aren't created in the task that created it, and the
9108
+ * track goes on last so the sender has no media until both steps succeed.
9109
+ */
9110
+ initEncryptedSender = async (transceiver, track, publishOption) => {
9111
+ try {
9112
+ const { sender } = transceiver;
9113
+ const codec = publishOption.codec?.name.toLowerCase();
9114
+ this.e2ee.encrypt(sender, codec, TrackType[publishOption.trackType]);
9115
+ this.logger.debug('E2EE encryptor attached to sender');
9116
+ await this.setDegradationPreference(sender, publishOption);
9117
+ await sender.replaceTrack(track);
9118
+ }
9119
+ catch (err) {
9120
+ this.retireTransceiver(transceiver, track);
9121
+ throw err;
9122
+ }
9123
+ };
9124
+ setDegradationPreference = async (sender, publishOption) => {
9125
+ const params = sender.getParameters();
9294
9126
  params.degradationPreference =
9295
9127
  toRTCDegradationPreference(publishOption.degradationPreference) ??
9296
9128
  'maintain-framerate';
9297
- await transceiver.sender.setParameters(params);
9298
- if (this.e2ee) {
9299
- this.e2ee.encrypt(transceiver.sender, publishOption.codec?.name.toLowerCase(), TrackType[publishOption.trackType]);
9300
- this.logger.debug('E2EE encryptor attached to sender');
9129
+ await sender.setParameters(params);
9130
+ };
9131
+ /**
9132
+ * Discards a sender whose E2EE initialization failed.
9133
+ *
9134
+ * A partially initialized sender may carry no transform, so publish() must
9135
+ * not reuse it: retiring it from the cache is what keeps a later publish or
9136
+ * ICE restart from putting cleartext on the wire. Cleanup cannot throw, so
9137
+ * the initialization failure is what reaches the caller.
9138
+ */
9139
+ retireTransceiver = (transceiver, track) => {
9140
+ this.transceiverCache.remove(transceiver);
9141
+ this.trackIdToTrackType.delete(track.id);
9142
+ // on React Native a clone shares its native source, so stopping it here
9143
+ // would stop the track the caller passed in. dispose() releases it.
9144
+ if (!isReactNative())
9145
+ this.stopTrack(track);
9146
+ try {
9147
+ transceiver.stop();
9148
+ }
9149
+ catch (err) {
9150
+ this.logger.debug('Failed to stop a retired transceiver', err);
9301
9151
  }
9302
- await this.negotiate();
9303
9152
  };
9304
9153
  /**
9305
9154
  * Updates the transceiver with the given track and track type.
@@ -10306,7 +10155,10 @@ class StreamSfuClient {
10306
10155
  this.rpc = createSignalClient({
10307
10156
  baseUrl: server.url,
10308
10157
  interceptors: [
10309
- withHeaders({ Authorization: `Bearer ${token}` }),
10158
+ withHeaders({
10159
+ Authorization: `Bearer ${token}`,
10160
+ 'X-Stream-Client': getStreamClientId(getSdkInfo()),
10161
+ }),
10310
10162
  this.tracer && withRequestTracer(this.tracer.trace),
10311
10163
  this.logger.getLogLevel() === 'trace' && withRequestLogger(this.logger),
10312
10164
  withTimeout(rpcRequestTimeout, this.tracer?.trace),
@@ -10609,98 +10461,404 @@ class StreamSfuClient {
10609
10461
  },
10610
10462
  }));
10611
10463
  };
10612
- send = async (message) => {
10613
- await this.signalReady(); // wait for the signal ws to be open
10614
- const msgJson = SfuRequest.toJson(message);
10615
- if (this.signalWs.readyState !== WebSocket.OPEN) {
10616
- this.logger.debug('Signal WS is not open. Skipping message', msgJson);
10464
+ send = async (message) => {
10465
+ await this.signalReady(); // wait for the signal ws to be open
10466
+ const msgJson = SfuRequest.toJson(message);
10467
+ if (this.signalWs.readyState !== WebSocket.OPEN) {
10468
+ this.logger.debug('Signal WS is not open. Skipping message', msgJson);
10469
+ return;
10470
+ }
10471
+ this.logger.debug(`Sending message to: ${this.edgeName}`, msgJson);
10472
+ this.signalWs.send(SfuRequest.toBinary(message));
10473
+ };
10474
+ keepAlive = () => {
10475
+ const timers = getTimers();
10476
+ timers.clearInterval(this.keepAliveInterval);
10477
+ this.keepAliveInterval = timers.setInterval(() => {
10478
+ this.ping().catch((e) => {
10479
+ this.logger.error('Error sending healthCheckRequest to SFU', e);
10480
+ });
10481
+ }, this.pingIntervalInMs);
10482
+ };
10483
+ scheduleConnectionCheck = () => {
10484
+ const timers = getTimers();
10485
+ timers.clearInterval(this.connectionCheckInterval);
10486
+ this.connectionCheckInterval = timers.setInterval(() => {
10487
+ if (this.lastMessageTimestamp) {
10488
+ const timeSinceLastMessage = Date.now() - this.lastMessageTimestamp;
10489
+ if (timeSinceLastMessage > this.unhealthyTimeoutInMs) {
10490
+ this.close(StreamSfuClient.ERROR_CONNECTION_UNHEALTHY, `SFU connection unhealthy. Didn't receive any message for ${this.unhealthyTimeoutInMs}ms`);
10491
+ }
10492
+ }
10493
+ }, this.connectionCheckIntervalInMs);
10494
+ };
10495
+ }
10496
+
10497
+ /**
10498
+ * Decides what a ringing call should do next, based on the current call state.
10499
+ *
10500
+ * The `call.accepted`, `call.rejected` and `call.missed` handlers and the ring
10501
+ * state poller both run this. They differ only in how the state got there: the
10502
+ * handlers rely on `CallState.updateFromEvent`, which runs before them, and the
10503
+ * poller applies the polled ring state itself.
10504
+ *
10505
+ * @param call the call to reconcile.
10506
+ * @param joinSource which of the two triggered this run, reported on the
10507
+ * caller's join: `ring-ws` for the event handlers, `ring-poll-api` for the
10508
+ * poller.
10509
+ * @returns whether the ring reached a terminal state. A failed join is not
10510
+ * terminal: the caller should keep trying while the ring is open.
10511
+ */
10512
+ const reconcileRingState = async (call, joinSource) => {
10513
+ if (call.state.callingState !== CallingState.RINGING)
10514
+ return true;
10515
+ return call.isCreatedByMe
10516
+ ? reconcileAsCaller(call, joinSource)
10517
+ : reconcileAsCallee(call);
10518
+ };
10519
+ const reconcileAsCaller = async (call, joinSource) => {
10520
+ const { session, members, endedAt } = call.state;
10521
+ const currentUserId = call.currentUserId;
10522
+ // checked before `accepted_by`: an ended session cannot be joined
10523
+ if (endedAt || session?.ended_at) {
10524
+ call.logger.info('ring: the call has ended, leaving');
10525
+ // `leave` reports the remote end to callingx off `reason: 'ended'`
10526
+ return leave(call, {
10527
+ reject: false,
10528
+ reason: 'ended',
10529
+ message: 'ring: call ended',
10530
+ });
10531
+ }
10532
+ const acceptedBy = session?.accepted_by ?? {};
10533
+ if (Object.keys(acceptedBy).some((userId) => userId !== currentUserId)) {
10534
+ call.logger.info('ring: the call was accepted, joining');
10535
+ try {
10536
+ await call.join({ joinSource });
10537
+ }
10538
+ catch (err) {
10539
+ // `doJoin` restores the ringing state when a join fails, so the ring is
10540
+ // still open. Report it unsettled and let the next poll retry.
10541
+ call.logger.error('Failed to join an accepted call', err);
10542
+ return false;
10543
+ }
10544
+ return true;
10545
+ }
10546
+ const otherMembers = members
10547
+ .filter((member) => member.user_id !== currentUserId)
10548
+ .map((member) => member.user_id);
10549
+ if (otherMembers.length === 0)
10550
+ return false;
10551
+ const rejectedBy = session?.rejected_by ?? {};
10552
+ if (otherMembers.every((userId) => rejectedBy[userId])) {
10553
+ call.logger.info('ring: everyone rejected, leaving');
10554
+ return leave(call, {
10555
+ reject: true,
10556
+ reason: 'cancel',
10557
+ message: 'ring: everyone rejected',
10558
+ });
10559
+ }
10560
+ return false;
10561
+ };
10562
+ // the current user's own accept or reject, on this or another device, is
10563
+ // handled by `resolveOwnRingOutcome`, and `call.ended` by `watchCallEnded`.
10564
+ const reconcileAsCallee = async (call) => {
10565
+ const createdById = call.state.createdBy?.id;
10566
+ const rejectedBy = call.state.session?.rejected_by ?? {};
10567
+ if (createdById && rejectedBy[createdById]) {
10568
+ call.logger.info('ring: the caller cancelled, leaving');
10569
+ return leave(call, {
10570
+ reason: 'ended',
10571
+ message: 'ring: creator rejected',
10572
+ });
10573
+ }
10574
+ return false;
10575
+ };
10576
+ // `false` when the call could not be left, so the ring stays open for a retry.
10577
+ const leave = async (call, options) => {
10578
+ try {
10579
+ await call.leave(options);
10580
+ return true;
10581
+ }
10582
+ catch (err) {
10583
+ call.logger.error('Failed to leave a ringing call', err);
10584
+ return false;
10585
+ }
10586
+ };
10587
+
10588
+ // `doJoin` restores the previous calling state when a join fails, so JOINING can
10589
+ // go back to RINGING. Only a state the ring cannot return from ends the poller.
10590
+ const ringIsOver = (callingState) => callingState !== CallingState.RINGING &&
10591
+ callingState !== CallingState.JOINING;
10592
+ /**
10593
+ * Polls the coordinator for the outcome of a ring the current user started.
10594
+ *
10595
+ * `call.accepted`, `call.rejected` and `call.missed` are delivered best-effort,
10596
+ * with no store-and-forward, so a caller that drops one is left on a ringing
10597
+ * screen while the callee is already in the call. After a quiet period this
10598
+ * reads the ring state until a terminal outcome or the end of the ring window.
10599
+ */
10600
+ class RingStatePoller {
10601
+ logger = videoLoggerSystem.getLogger('RingStatePoller');
10602
+ call;
10603
+ startAfterMs;
10604
+ intervalMs;
10605
+ sessionId;
10606
+ deadlineAt = 0;
10607
+ idleTimeoutId;
10608
+ intervalId;
10609
+ stopped = false;
10610
+ wasPolling = false;
10611
+ inFlight = false;
10612
+ unsubscribe = [];
10613
+ constructor(call, options = {}) {
10614
+ this.call = call;
10615
+ this.startAfterMs = options.startAfterMs ?? 15_000;
10616
+ this.intervalMs = options.intervalMs ?? 5_000;
10617
+ }
10618
+ /**
10619
+ * Starts polling. Does nothing when the call has no session yet.
10620
+ */
10621
+ start = () => {
10622
+ if (this.stopped || this.sessionId)
10623
+ return;
10624
+ if (this.call.state.callingState !== CallingState.RINGING)
10625
+ return;
10626
+ // captured once: `call.ended` clears the call's current session
10627
+ const sessionId = this.call.state.session?.id;
10628
+ if (!sessionId) {
10629
+ this.logger.warn('the call has no session');
10630
+ return;
10631
+ }
10632
+ this.sessionId = sessionId;
10633
+ const ring = this.call.state.settings?.ring;
10634
+ const maxDurationMs = ring?.auto_cancel_timeout_ms || ring?.missed_call_timeout_ms || 30_000;
10635
+ this.deadlineAt = Date.now() + maxDurationMs;
10636
+ // an incoming event proves the socket is alive, so the quiet period starts
10637
+ // over: in a group ring a single rejection does not settle the ring.
10638
+ this.unsubscribe.push(this.call.on('call.accepted', () => this.armIdleWindow()), this.call.on('call.rejected', () => this.armIdleWindow()), this.call.on('call.missed', () => this.armIdleWindow()),
10639
+ // the ring is over, whichever way it went. Joining an accepted call
10640
+ // never goes through `leave`, so this is the only signal for it.
10641
+ createSubscription(this.call.state.callingState$, (callingState) => {
10642
+ if (ringIsOver(callingState))
10643
+ this.stop();
10644
+ }));
10645
+ this.armIdleWindow();
10646
+ };
10647
+ /**
10648
+ * Pauses polling, keeping the captured session, the deadline and the event
10649
+ * subscriptions. Resuming does not extend the ring window.
10650
+ */
10651
+ pause = () => {
10652
+ if (this.stopped)
10653
+ return;
10654
+ this.wasPolling = this.intervalId !== undefined;
10655
+ const timers = getTimers();
10656
+ timers.clearTimeout(this.idleTimeoutId);
10657
+ timers.clearInterval(this.intervalId);
10658
+ this.idleTimeoutId = undefined;
10659
+ this.intervalId = undefined;
10660
+ };
10661
+ /**
10662
+ * Resumes a paused poller. Goes straight back to polling if it was already
10663
+ * past the quiet period, so a pause does not buy the ring another one.
10664
+ */
10665
+ resume = () => {
10666
+ if (this.stopped || !this.sessionId)
10667
+ return;
10668
+ if (this.idleTimeoutId !== undefined || this.intervalId !== undefined) {
10617
10669
  return;
10618
10670
  }
10619
- this.logger.debug(`Sending message to: ${this.edgeName}`, msgJson);
10620
- this.signalWs.send(SfuRequest.toBinary(message));
10671
+ if (!this.wasPolling) {
10672
+ this.armIdleWindow();
10673
+ return;
10674
+ }
10675
+ this.intervalId = getTimers().setInterval(this.runTick, this.intervalMs);
10676
+ this.runTick();
10621
10677
  };
10622
- keepAlive = () => {
10678
+ /**
10679
+ * Stops polling. The poller cannot be restarted.
10680
+ */
10681
+ stop = () => {
10682
+ if (this.stopped)
10683
+ return;
10684
+ this.stopped = true;
10623
10685
  const timers = getTimers();
10624
- timers.clearInterval(this.keepAliveInterval);
10625
- this.keepAliveInterval = timers.setInterval(() => {
10626
- this.ping().catch((e) => {
10627
- this.logger.error('Error sending healthCheckRequest to SFU', e);
10628
- });
10629
- }, this.pingIntervalInMs);
10686
+ timers.clearTimeout(this.idleTimeoutId);
10687
+ timers.clearInterval(this.intervalId);
10688
+ this.idleTimeoutId = undefined;
10689
+ this.intervalId = undefined;
10690
+ this.unsubscribe.forEach((off) => off());
10691
+ this.unsubscribe = [];
10630
10692
  };
10631
- scheduleConnectionCheck = () => {
10693
+ armIdleWindow = () => {
10694
+ if (this.stopped)
10695
+ return;
10632
10696
  const timers = getTimers();
10633
- timers.clearInterval(this.connectionCheckInterval);
10634
- this.connectionCheckInterval = timers.setInterval(() => {
10635
- if (this.lastMessageTimestamp) {
10636
- const timeSinceLastMessage = Date.now() - this.lastMessageTimestamp;
10637
- if (timeSinceLastMessage > this.unhealthyTimeoutInMs) {
10638
- this.close(StreamSfuClient.ERROR_CONNECTION_UNHEALTHY, `SFU connection unhealthy. Didn't receive any message for ${this.unhealthyTimeoutInMs}ms`);
10639
- }
10640
- }
10641
- }, this.connectionCheckIntervalInMs);
10697
+ timers.clearTimeout(this.idleTimeoutId);
10698
+ timers.clearInterval(this.intervalId);
10699
+ this.intervalId = undefined;
10700
+ this.idleTimeoutId = timers.setTimeout(() => {
10701
+ this.idleTimeoutId = undefined;
10702
+ if (this.stopped)
10703
+ return;
10704
+ this.intervalId = timers.setInterval(this.runTick, this.intervalMs);
10705
+ this.runTick();
10706
+ }, this.startAfterMs);
10642
10707
  };
10643
- }
10644
-
10645
- /**
10646
- * Event handler that watched the delivery of `call.accepted`.
10647
- * Once the event is received, the call is joined.
10648
- */
10649
- const watchCallAccepted = (call) => {
10650
- return async function onCallAccepted(event) {
10651
- // We want to discard the event if it's from the current user
10652
- if (event.user.id === call.currentUserId)
10653
- return;
10654
- const { state } = call;
10655
- if (event.call.created_by.id === call.currentUserId &&
10656
- state.callingState === CallingState.RINGING) {
10657
- await call.join();
10658
- }
10708
+ runTick = () => {
10709
+ this.tick().catch((err) => {
10710
+ this.logger.warn('Failed to poll the ring state', err);
10711
+ });
10659
10712
  };
10660
- };
10661
- /**
10662
- * Event handler that watches delivery of `call.rejected` Websocket event.
10663
- * Once the event is received, the call is left.
10664
- */
10665
- const watchCallRejected = (call) => {
10666
- return async function onCallRejected(event) {
10667
- // We want to discard the event if it's from the current user
10668
- if (event.user.id === call.currentUserId)
10713
+ tick = async () => {
10714
+ if (this.stopped || this.inFlight || !this.sessionId)
10669
10715
  return;
10670
- const { call: eventCall } = event;
10671
- const { session: callSession } = eventCall;
10672
- if (!callSession) {
10673
- call.logger.warn('No call session provided. Ignoring call.rejected event.', event);
10716
+ const { callingState } = this.call.state;
10717
+ if (ringIsOver(callingState) || Date.now() >= this.deadlineAt) {
10718
+ this.stop();
10674
10719
  return;
10675
10720
  }
10676
- const rejectedBy = callSession.rejected_by;
10677
- const { members, callingState } = call.state;
10678
- if (callingState !== CallingState.RINGING) {
10679
- call.logger.info('Call is not in ringing mode (it is either accepted or rejected already). Ignoring call.rejected event.', event);
10721
+ // mid-join: nothing to reconcile until we know whether it succeeded
10722
+ if (callingState !== CallingState.RINGING)
10680
10723
  return;
10724
+ this.inFlight = true;
10725
+ try {
10726
+ const ringState = await this.call.getRingState(this.sessionId);
10727
+ if (this.stopped)
10728
+ return;
10729
+ this.call.state.updateFromRingState(ringState);
10730
+ if (await reconcileRingState(this.call, 'ring-poll-api'))
10731
+ this.stop();
10681
10732
  }
10682
- if (call.isCreatedByMe) {
10683
- const everyoneElseRejected = members
10684
- .filter((m) => m.user_id !== call.currentUserId)
10685
- .every((m) => rejectedBy[m.user_id]);
10686
- if (everyoneElseRejected) {
10687
- call.logger.info('everyone rejected, leaving the call');
10688
- await call.leave({
10689
- reject: true,
10690
- reason: 'cancel',
10691
- message: 'ring: everyone rejected',
10692
- });
10733
+ catch (err) {
10734
+ // a missing session, or one of another call, will never resolve
10735
+ const status = err instanceof ErrorFromResponse ? err.status : undefined;
10736
+ if (status === 400 || status === 404) {
10737
+ this.logger.warn('Stopped polling the ring state', err);
10738
+ this.stop();
10693
10739
  }
10694
- }
10695
- else {
10696
- if (rejectedBy[eventCall.created_by.id]) {
10697
- call.logger.info('call creator rejected, leaving call');
10698
- globalThis.streamRNVideoSDK?.callingX?.endCall(call, 'remote');
10699
- await call.leave({ message: 'ring: creator rejected' });
10740
+ else {
10741
+ this.logger.debug('Failed to poll the ring state', err);
10700
10742
  }
10701
10743
  }
10744
+ finally {
10745
+ this.inFlight = false;
10746
+ }
10747
+ };
10748
+ }
10749
+
10750
+ /**
10751
+ * Drops a call that has been ringing for too long: the caller cancels after
10752
+ * `auto_cancel_timeout_ms`, a callee after `incoming_call_timeout_ms`.
10753
+ *
10754
+ * Either timeout being `0` means the call rings until something else settles it.
10755
+ */
10756
+ class RingTimeout {
10757
+ call;
10758
+ timeoutId;
10759
+ deadlineAt;
10760
+ stopped = false;
10761
+ constructor(call) {
10762
+ this.call = call;
10763
+ }
10764
+ /**
10765
+ * Schedules an auto-drop timeout based on the call settings.
10766
+ * Applicable only for ringing calls.
10767
+ */
10768
+ start = () => {
10769
+ if (this.stopped || this.timeoutId !== undefined)
10770
+ return;
10771
+ // ignore if the call is not ringing
10772
+ if (this.call.state.callingState !== CallingState.RINGING)
10773
+ return;
10774
+ const ring = this.call.state.settings?.ring;
10775
+ if (!ring)
10776
+ return;
10777
+ const isCaller = this.call.isCreatedByMe;
10778
+ const timeoutMs = isCaller
10779
+ ? ring.auto_cancel_timeout_ms
10780
+ : ring.incoming_call_timeout_ms;
10781
+ // 0 means no auto-drop
10782
+ if (timeoutMs <= 0)
10783
+ return;
10784
+ const now = Date.now();
10785
+ this.deadlineAt ??= now + timeoutMs;
10786
+ const delayMs = Math.max(0, this.deadlineAt - now);
10787
+ const timers = getTimers();
10788
+ this.timeoutId = timers.setTimeout(() => {
10789
+ this.timeoutId = undefined;
10790
+ // A failed timeout-triggered leave can arm a fresh retry window.
10791
+ this.deadlineAt = undefined;
10792
+ if (this.stopped)
10793
+ return;
10794
+ // the call might have stopped ringing by this point, e.g. it was already
10795
+ // accepted and joined
10796
+ if (this.call.state.callingState !== CallingState.RINGING)
10797
+ return;
10798
+ this.call
10799
+ .leave({
10800
+ reject: true,
10801
+ reason: 'timeout',
10802
+ message: `ringing timeout - ${isCaller
10803
+ ? 'no one accepted'
10804
+ : `user didn't interact with incoming call screen`}`,
10805
+ })
10806
+ .catch((err) => {
10807
+ videoLoggerSystem
10808
+ .getLogger('RingTimeout')
10809
+ .error('Failed to drop the call', err);
10810
+ });
10811
+ }, delayMs);
10812
+ };
10813
+ /**
10814
+ * Pauses the timeout while preserving its original deadline.
10815
+ */
10816
+ pause = () => {
10817
+ if (this.stopped || this.timeoutId === undefined)
10818
+ return;
10819
+ getTimers().clearTimeout(this.timeoutId);
10820
+ this.timeoutId = undefined;
10821
+ };
10822
+ /**
10823
+ * Cancels a scheduled auto-drop timeout. It cannot be armed again.
10824
+ */
10825
+ stop = () => {
10826
+ if (this.stopped)
10827
+ return;
10828
+ this.stopped = true;
10829
+ getTimers().clearTimeout(this.timeoutId);
10830
+ this.timeoutId = undefined;
10831
+ this.deadlineAt = undefined;
10832
+ };
10833
+ }
10834
+
10835
+ /**
10836
+ * Decides what a ringing call should do about the current user's own accept or
10837
+ * reject, which may have happened on another device.
10838
+ *
10839
+ * Accepting on *this* device also lands in `accepted_by`, so an acceptance only
10840
+ * means another device took the call when this one is still ringing.
10841
+ *
10842
+ * Only meaningful for a ringing call; the caller checks that.
10843
+ */
10844
+ const resolveOwnRingOutcome = ({ session, currentUserId, callingState, }) => {
10845
+ if (!currentUserId)
10846
+ return { settledByMe: false };
10847
+ const acceptedByMe = Boolean(session?.accepted_by[currentUserId]);
10848
+ const rejectedByMe = Boolean(session?.rejected_by[currentUserId]);
10849
+ if (!acceptedByMe && !rejectedByMe)
10850
+ return { settledByMe: false };
10851
+ const answeredElsewhere = acceptedByMe && callingState === CallingState.RINGING;
10852
+ return {
10853
+ settledByMe: true,
10854
+ leaveReason: answeredElsewhere
10855
+ ? 'answeredElsewhere'
10856
+ : rejectedByMe
10857
+ ? 'rejected'
10858
+ : undefined,
10702
10859
  };
10703
10860
  };
10861
+
10704
10862
  /**
10705
10863
  * Event handler that watches the delivery of `call.ended` Websocket event.
10706
10864
  */
@@ -11101,14 +11259,18 @@ const registerEventHandlers = (call, dispatcher) => {
11101
11259
  * @param call the call to register event handlers for.
11102
11260
  */
11103
11261
  const registerRingingCallEventHandlers = (call) => {
11104
- const coordinatorRingEvents = {
11105
- 'call.accepted': watchCallAccepted(call),
11106
- 'call.rejected': watchCallRejected(call),
11262
+ const reconcile = () => {
11263
+ reconcileRingState(call, 'ring-ws').catch((err) => {
11264
+ call.logger.error('Failed to reconcile the ring state', err);
11265
+ });
11107
11266
  };
11108
- const eventHandlers = Object.keys(coordinatorRingEvents).map((event) => {
11109
- const eventName = event;
11110
- return call.on(eventName, coordinatorRingEvents[eventName]);
11111
- });
11267
+ // each event needs its own closure. `call.missed` is deliberately absent:
11268
+ // nothing in the reconciler acts on `missed_by`, so the auto-drop owns the
11269
+ // "nobody answered" case until the server-owned ring timeout lands.
11270
+ const eventHandlers = [
11271
+ call.on('call.accepted', () => reconcile()),
11272
+ call.on('call.rejected', () => reconcile()),
11273
+ ];
11112
11274
  return () => {
11113
11275
  eventHandlers.forEach((unsubscribe) => unsubscribe());
11114
11276
  };
@@ -15156,8 +15318,9 @@ class Call {
15156
15318
  statsReporter;
15157
15319
  sfuStatsReporter;
15158
15320
  lastStatsOptions;
15159
- dropTimeout;
15160
- clientStore;
15321
+ ringTimeout;
15322
+ ringStatePoller;
15323
+ clientState;
15161
15324
  streamClient;
15162
15325
  clientEventReporter;
15163
15326
  sfuClient;
@@ -15224,7 +15387,7 @@ class Call {
15224
15387
  * Use the [`StreamVideoClient.call`](./StreamVideoClient.md/#call)
15225
15388
  * method to construct a `Call` instance.
15226
15389
  */
15227
- constructor({ type, id, streamClient, clientEventReporter, members, ownCapabilities, sortParticipantsBy, clientStore, ringing = false, watching = false, }) {
15390
+ constructor({ type, id, streamClient, clientEventReporter, members, ownCapabilities, sortParticipantsBy, clientState, ringing = false, watching = false, }) {
15228
15391
  this.type = type;
15229
15392
  this.id = id;
15230
15393
  this.cid = `${type}:${id}`;
@@ -15232,7 +15395,7 @@ class Call {
15232
15395
  this.watching = watching;
15233
15396
  this.streamClient = streamClient;
15234
15397
  this.clientEventReporter = clientEventReporter;
15235
- this.clientStore = clientStore;
15398
+ this.clientState = clientState;
15236
15399
  this.streamClientBasePath = `/call/${this.type}/${this.id}`;
15237
15400
  this.logger = videoLoggerSystem.getLogger('Call');
15238
15401
  const callTypeConfig = CallTypes.get(type);
@@ -15334,27 +15497,24 @@ class Call {
15334
15497
  createSubscription(this.state.session$, (session) => {
15335
15498
  if (!this.ringing)
15336
15499
  return;
15337
- const receiverId = this.clientStore.connectedUser?.id;
15338
- if (!receiverId)
15339
- return;
15340
- const isAcceptedByMe = Boolean(session?.accepted_by[receiverId]);
15341
- const isRejectedByMe = Boolean(session?.rejected_by[receiverId]);
15342
- if (isAcceptedByMe || isRejectedByMe) {
15500
+ const { settledByMe, leaveReason } = resolveOwnRingOutcome({
15501
+ session,
15502
+ currentUserId: this.currentUserId,
15503
+ callingState: this.state.callingState,
15504
+ });
15505
+ if (settledByMe)
15343
15506
  this.cancelAutoDrop();
15344
- }
15345
- const isAcceptedElsewhere = isAcceptedByMe && this.state.callingState === CallingState.RINGING;
15346
- if ((isAcceptedElsewhere || isRejectedByMe) &&
15347
- !hasPending(this.joinLeaveConcurrencyTag)) {
15348
- globalThis.streamRNVideoSDK?.callingX?.endCall(this, isAcceptedElsewhere ? 'answeredElsewhere' : 'rejected');
15349
- this.leave().catch(() => {
15350
- this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
15351
- });
15352
- }
15507
+ if (!leaveReason || hasPending(this.joinLeaveConcurrencyTag))
15508
+ return;
15509
+ globalThis.streamRNVideoSDK?.callingX?.endCall(this, leaveReason);
15510
+ this.leave().catch(() => {
15511
+ this.logger.error('Could not leave a call that was accepted or rejected elsewhere');
15512
+ });
15353
15513
  }));
15354
15514
  };
15355
15515
  handleRingingCall = () => {
15356
15516
  const callSession = this.state.session;
15357
- const receiver_id = this.clientStore.connectedUser?.id;
15517
+ const receiver_id = this.clientState.connectedUser?.id;
15358
15518
  const ended_at = callSession?.ended_at;
15359
15519
  const created_by_id = this.state.createdBy?.id;
15360
15520
  if (this.currentUserId && created_by_id === this.currentUserId) {
@@ -15395,6 +15555,7 @@ class Call {
15395
15555
  this.state.setCallingState(CallingState.RINGING);
15396
15556
  }
15397
15557
  this.scheduleAutoDrop();
15558
+ this.scheduleRingStatePolling();
15398
15559
  this.leaveCallHooks.add(registerRingingCallEventHandlers(this));
15399
15560
  }
15400
15561
  };
@@ -15481,6 +15642,12 @@ class Call {
15481
15642
  if (this.state.callingState === CallingState.LEFT) {
15482
15643
  throw new Error('Cannot leave call that has already been left.');
15483
15644
  }
15645
+ // before the first await: the calling state stays RINGING well into the
15646
+ // teardown, so pause both watchdogs before they can race this leave. They
15647
+ // keep their deadlines, so a failed leave resumes them without handing the
15648
+ // ring another window.
15649
+ this.ringTimeout?.pause();
15650
+ this.ringStatePoller?.pause();
15484
15651
  await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => {
15485
15652
  const callingState = this.state.callingState;
15486
15653
  if (callingState === CallingState.LEFT) {
@@ -15547,6 +15714,11 @@ class Call {
15547
15714
  this.audioBindingsWatchdog?.dispose();
15548
15715
  await this.dynascaleManager?.dispose();
15549
15716
  this.state.setCallingState(CallingState.LEFT);
15717
+ // `ringingSubject` is cleared further down, so this still reads true for
15718
+ // a call that was ringing.
15719
+ if (this.ringing) {
15720
+ globalThis.streamRNVideoSDK?.ringingCallLifecycle?.onLeave(this);
15721
+ }
15550
15722
  this.state.setParticipants([]);
15551
15723
  this.state.dispose();
15552
15724
  // Reset reconnect-related accumulators so a future `call.join()` on the
@@ -15570,7 +15742,8 @@ class Call {
15570
15742
  this.unifiedSessionId = undefined;
15571
15743
  this.ringingSubject.next(false);
15572
15744
  this.cancelAutoDrop();
15573
- this.clientStore.unregisterCall(this);
15745
+ this.cancelRingStatePolling();
15746
+ this.clientState.unregisterCall(this);
15574
15747
  globalThis.streamRNVideoSDK?.callManager.stop({
15575
15748
  isRingingTypeCall: this.ringing,
15576
15749
  shouldStopCallManager: this.callManagerStarted,
@@ -15608,6 +15781,15 @@ class Call {
15608
15781
  this.logger.warn('Failed to dispose media engine', err);
15609
15782
  });
15610
15783
  }
15784
+ }).catch((err) => {
15785
+ if (!hasPending(this.joinLeaveConcurrencyTag) &&
15786
+ this.state.callingState === CallingState.RINGING) {
15787
+ // resume, never re-arm: a fresh watchdog would restart the ring
15788
+ // window this leave was already most of the way through
15789
+ this.ringTimeout?.start();
15790
+ this.ringStatePoller?.resume();
15791
+ }
15792
+ throw err;
15611
15793
  });
15612
15794
  };
15613
15795
  /**
@@ -15620,7 +15802,7 @@ class Call {
15620
15802
  * Retrieves the current user ID.
15621
15803
  */
15622
15804
  get currentUserId() {
15623
- return this.clientStore.connectedUser?.id;
15805
+ return this.clientState.connectedUser?.id;
15624
15806
  }
15625
15807
  /**
15626
15808
  * A flag indicating whether self-subscription is enabled for the call.
@@ -15681,8 +15863,8 @@ class Call {
15681
15863
  this.ringingSubject.next(true);
15682
15864
  // we remove the instance from the calls list to enable the following filter in useCalls hook
15683
15865
  // const calls = useCalls().filter((c) => c.ringing);
15684
- const calls = this.clientStore.calls.filter((c) => c.cid !== this.cid);
15685
- this.clientStore.setCalls([this, ...calls]);
15866
+ const calls = this.clientState.calls.filter((c) => c.cid !== this.cid);
15867
+ this.clientState.setCalls([this, ...calls]);
15686
15868
  await this.applyDeviceConfig(settings, { publish: false });
15687
15869
  };
15688
15870
  /**
@@ -15705,15 +15887,18 @@ class Call {
15705
15887
  * @param params.video if set to true, in a ringing scenario, mobile SDKs will show "incoming video call", audio only otherwise.
15706
15888
  */
15707
15889
  get = async (params) => {
15890
+ const getLeaveGeneration = this.leaveGeneration;
15708
15891
  await this.setup();
15709
15892
  const response = await this.streamClient.get(this.streamClientBasePath, params);
15893
+ if (this.leaveGeneration !== getLeaveGeneration)
15894
+ return response;
15710
15895
  this.updateFromCallStateResponse(response);
15711
15896
  if (params?.ring) {
15712
15897
  this.ringingSubject.next(true);
15713
15898
  }
15714
15899
  if (this.streamClient._hasConnectionID()) {
15715
15900
  this.watching = true;
15716
- this.clientStore.registerOrUpdateCall(this);
15901
+ this.clientState.registerOrUpdateCall(this);
15717
15902
  }
15718
15903
  await this.applyDeviceConfig(response.call.settings, { publish: false });
15719
15904
  return response;
@@ -15732,7 +15917,7 @@ class Call {
15732
15917
  }
15733
15918
  if (this.streamClient._hasConnectionID()) {
15734
15919
  this.watching = true;
15735
- this.clientStore.registerOrUpdateCall(this);
15920
+ this.clientState.registerOrUpdateCall(this);
15736
15921
  }
15737
15922
  await this.applyDeviceConfig(response.call.settings, { publish: false });
15738
15923
  return response;
@@ -15758,6 +15943,21 @@ class Call {
15758
15943
  ring = async (data = {}) => {
15759
15944
  return this.streamClient.post(`${this.streamClientBasePath}/ring`, data);
15760
15945
  };
15946
+ /**
15947
+ * Returns who accepted, rejected or missed the ring for a call session.
15948
+ * Safe to poll: it performs no writes and emits no events.
15949
+ *
15950
+ * @param callSessionId the call session to read. Defaults to the current one.
15951
+ * Pass it explicitly to read a session that has already ended, as ending a
15952
+ * call clears its current session.
15953
+ */
15954
+ getRingState = async (callSessionId) => {
15955
+ const sessionId = callSessionId ?? this.state.session?.id;
15956
+ if (!sessionId) {
15957
+ throw new Error('Cannot read the ring state: the call has no session');
15958
+ }
15959
+ return this.streamClient.get(`${this.streamClientBasePath}/ring_state`, { call_session_id: sessionId });
15960
+ };
15761
15961
  /**
15762
15962
  * A shortcut for {@link Call.get} with `notify` parameter set to `true`.
15763
15963
  * Will send a `call.notification` event to the call members.
@@ -15796,42 +15996,80 @@ class Call {
15796
15996
  /**
15797
15997
  * Will start to watch for call related WebSocket events and initiate a call session with the server.
15798
15998
  *
15999
+ * One instance is one call flow: discard it after leaving, cancelling, or a
16000
+ * failed join, and create a fresh one for a later flow. Reconnection inside a
16001
+ * live call is handled here and needs no new instance.
16002
+ *
15799
16003
  * @returns a promise which resolves once the call join-flow has finished.
15800
16004
  */
15801
- join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, ...data } = {}) => {
16005
+ join = singleFlight(async ({ maxJoinRetries = 3, joinResponseTimeout, rpcRequestTimeout, allowOwnTracksLoopback = false, joinSource, ...data } = {}) => {
15802
16006
  const callingState = this.state.callingState;
16007
+ // Ahead of the failure boundary below on purpose: a duplicate join on a
16008
+ // live call is refused without tearing that call down.
15803
16009
  if ([CallingState.JOINED, CallingState.JOINING].includes(callingState)) {
15804
16010
  throw new Error(`Illegal State: call.join() shall be called only once`);
15805
16011
  }
16012
+ // snapshot before the first await: a leave() landing at any point from
16013
+ // here on supersedes this join, and the retry loop must bail out instead
16014
+ // of resurrecting a call that leave() already tore down.
16015
+ const joinLeaveGeneration = this.leaveGeneration;
16016
+ const supersededByLeave = () => this.leaveGeneration !== joinLeaveGeneration;
15806
16017
  // we need this to be set before the callingx.joinCall() is
15807
16018
  // called to avoid registering the test call in the CallKit/Telecom
15808
16019
  this.allowOwnTracksLoopback = allowOwnTracksLoopback;
15809
16020
  if (data?.ring) {
15810
16021
  this.ringingSubject.next(true);
15811
16022
  }
16023
+ // A ringing call is joined by the SDK rather than by app code, so React
16024
+ // Native prepares it from in here - there is no earlier point at which the
16025
+ // app holds the call. Read after `data.ring` above, which is what makes an
16026
+ // outgoing call ringing in the first place.
16027
+ const ringingLifecycle = this.ringing
16028
+ ? globalThis.streamRNVideoSDK?.ringingCallLifecycle
16029
+ : undefined;
15812
16030
  const callingX = globalThis.streamRNVideoSDK?.callingX;
15813
- if (callingX) {
15814
- // for Android/iOS, we need to start the call in the callingx library as soon as possible
15815
- await callingX.joinCall(this, this.clientStore.calls);
15816
- }
15817
- await this.setup();
15818
- this.clientEventReporter.registerCall(this.cid, {
15819
- callType: this.type,
15820
- callId: this.id,
15821
- getCallSessionId: () => this.state.session?.id ?? '',
15822
- getSfuId: () => this.credentials?.server.edge_name ?? '',
15823
- });
15824
- this.joinResponseTimeout = joinResponseTimeout;
15825
- this.rpcRequestTimeout = rpcRequestTimeout;
15826
- // we will count the number of join failures per SFU.
15827
- // once the number of failures reaches 2, we will piggyback on the `migrating_from`
15828
- // field to force the coordinator to provide us another SFU
15829
- const sfuJoinFailures = new Map();
15830
- const joinData = data;
15831
- maxJoinRetries = Math.max(maxJoinRetries, 1);
15832
16031
  try {
15833
- await this.clientEventReporter.withJoinLifecycle(this.cid, 'first-attempt', async () => {
16032
+ if (ringingLifecycle) {
16033
+ await ringingLifecycle.beforeJoin(this);
16034
+ if (supersededByLeave()) {
16035
+ this.logger.debug('Join superseded by leave; not joining');
16036
+ return;
16037
+ }
16038
+ }
16039
+ if (callingX) {
16040
+ // for Android/iOS, we need to start the call in the callingx library as soon as possible
16041
+ await callingX.joinCall(this, this.clientState.calls, supersededByLeave);
16042
+ if (supersededByLeave()) {
16043
+ this.logger.debug('Join superseded by leave; not setting up');
16044
+ return;
16045
+ }
16046
+ }
16047
+ await this.setup();
16048
+ if (supersededByLeave()) {
16049
+ this.logger.debug('Join superseded by leave; not registering');
16050
+ return;
16051
+ }
16052
+ this.clientEventReporter.registerCall(this.cid, {
16053
+ callType: this.type,
16054
+ callId: this.id,
16055
+ getCallSessionId: () => this.state.session?.id ?? '',
16056
+ getSfuId: () => this.credentials?.server.edge_name ?? '',
16057
+ });
16058
+ this.joinResponseTimeout = joinResponseTimeout;
16059
+ this.rpcRequestTimeout = rpcRequestTimeout;
16060
+ // we will count the number of join failures per SFU.
16061
+ // once the number of failures reaches 2, we will piggyback on the `migrating_from`
16062
+ // field to force the coordinator to provide us another SFU
16063
+ const sfuJoinFailures = new Map();
16064
+ const joinData = data;
16065
+ maxJoinRetries = Math.max(maxJoinRetries, 1);
16066
+ await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason: 'first-attempt', joinSource }, async () => {
15834
16067
  for (let attempt = 0; attempt < maxJoinRetries; attempt++) {
16068
+ // Also covers a leave that lands during the backoff below.
16069
+ if (supersededByLeave()) {
16070
+ this.logger.debug('Join superseded by leave; not attempting');
16071
+ return;
16072
+ }
15835
16073
  try {
15836
16074
  this.logger.trace(`Joining call (${attempt})`, this.cid);
15837
16075
  await this.doJoin(data);
@@ -15840,6 +16078,10 @@ class Call {
15840
16078
  return;
15841
16079
  }
15842
16080
  catch (err) {
16081
+ if (supersededByLeave()) {
16082
+ this.logger.debug('Join superseded by leave; not retrying');
16083
+ return;
16084
+ }
15843
16085
  this.logger.warn(`Failed to join call (${attempt})`, this.cid);
15844
16086
  if ((err instanceof ErrorFromResponse && err.unrecoverable) ||
15845
16087
  (err instanceof SfuJoinError && err.unrecoverable)) {
@@ -15869,6 +16111,9 @@ class Call {
15869
16111
  }
15870
16112
  catch (error) {
15871
16113
  callingX?.endCall(this, 'error');
16114
+ // Ends the failed ringing flow and releases what its setup installed.
16115
+ // Never rejects, so `error` is what the caller sees.
16116
+ await ringingLifecycle?.onJoinFailed(this);
15872
16117
  throw error;
15873
16118
  }
15874
16119
  });
@@ -15891,6 +16136,10 @@ class Call {
15891
16136
  // globals resolve to the call's factory. Idempotent across
15892
16137
  // reconnect/migration attempts.
15893
16138
  await this.ensureMediaFactory();
16139
+ if (supersededByLeave()) {
16140
+ this.logger.debug('Join superseded by leave; not wiring media');
16141
+ return;
16142
+ }
15894
16143
  const callingX = globalThis.streamRNVideoSDK?.callingX;
15895
16144
  if (callingX) {
15896
16145
  callingX.wireAudioEngineSubscription();
@@ -15906,11 +16155,17 @@ class Call {
15906
16155
  data?.migrating_from) {
15907
16156
  try {
15908
16157
  const joinResponse = await this.clientEventReporter.track(this.cid, 'CoordinatorJoin', () => this.doJoinRequest(data));
16158
+ if (!joinResponse || supersededByLeave()) {
16159
+ this.logger.debug('Join superseded by leave; not creating SFU client');
16160
+ return;
16161
+ }
15909
16162
  this.credentials = joinResponse.credentials;
15910
16163
  statsOptions = joinResponse.stats_options;
15911
16164
  this.lastStatsOptions = statsOptions;
15912
16165
  }
15913
16166
  catch (error) {
16167
+ if (supersededByLeave())
16168
+ return;
15914
16169
  // prevent triggering reconnect flow if the state is OFFLINE
15915
16170
  const avoidRestoreState = this.state.callingState === CallingState.OFFLINE;
15916
16171
  if (!avoidRestoreState) {
@@ -15958,6 +16213,10 @@ class Call {
15958
16213
  getGenericSdp('recvonly', dangerouslyForceCodec, subscriberFmtpLine),
15959
16214
  getGenericSdp('sendonly', dangerouslyForceCodec, fmtpLine),
15960
16215
  ]);
16216
+ if (supersededByLeave()) {
16217
+ this.logger.debug('Join superseded by leave; not joining SFU');
16218
+ return;
16219
+ }
15961
16220
  const isReconnecting = this.reconnectStrategy !== WebsocketReconnectStrategy.UNSPECIFIED;
15962
16221
  const reconnectDetails = isReconnecting
15963
16222
  ? this.getReconnectDetails(previousSfuClient?.edgeName, previousSessionId)
@@ -15983,6 +16242,10 @@ class Call {
15983
16242
  capabilities,
15984
16243
  source: ParticipantSource.WEBRTC_UNSPECIFIED,
15985
16244
  }));
16245
+ if (supersededByLeave()) {
16246
+ this.logger.debug('Join superseded by leave; not applying SFU response');
16247
+ return;
16248
+ }
15986
16249
  this.currentPublishOptions = publishOptions;
15987
16250
  this.fastReconnectDeadlineSeconds = fastReconnectDeadlineSeconds;
15988
16251
  if (callState) {
@@ -15990,6 +16253,10 @@ class Call {
15990
16253
  }
15991
16254
  }
15992
16255
  catch (error) {
16256
+ if (supersededByLeave()) {
16257
+ this.logger.debug('Join superseded by leave; ignoring SFU failure');
16258
+ return;
16259
+ }
15993
16260
  this.logger.warn('Join SFU request failed', error);
15994
16261
  sfuClient.close(StreamSfuClient.JOIN_FAILED, 'Join request failed, connection considered unhealthy');
15995
16262
  // restore the previous call state if the join-flow fails
@@ -15997,8 +16264,8 @@ class Call {
15997
16264
  throw error;
15998
16265
  }
15999
16266
  }
16000
- // If the user left while this join was in flight, bail before re-setting JOINED and before
16001
- // peer-connection setup below (both run synchronously after this, so one check covers them).
16267
+ // If the user left while this join was in flight, bail before re-setting JOINED
16268
+ // or starting peer-connection setup below.
16002
16269
  if (supersededByLeave()) {
16003
16270
  this.logger.debug('Join superseded by leave; aborting join flow');
16004
16271
  return;
@@ -16027,6 +16294,10 @@ class Call {
16027
16294
  unifiedSessionId: this.unifiedSessionId,
16028
16295
  });
16029
16296
  }
16297
+ if (supersededByLeave()) {
16298
+ this.logger.debug('Join superseded by leave; not completing join flow');
16299
+ return;
16300
+ }
16030
16301
  // make sure we only track connection timing if we are not calling this method as part of a reconnection flow
16031
16302
  if (!performingRejoin && !performingFastReconnect && !performingMigration) {
16032
16303
  this.sfuStatsReporter?.sendConnectionTime((Date.now() - connectStartTime) / 1000);
@@ -16172,15 +16443,20 @@ class Call {
16172
16443
  * @internal
16173
16444
  */
16174
16445
  initPublisherAndSubscriber = async (opts) => {
16446
+ const joinLeaveGeneration = this.leaveGeneration;
16175
16447
  const { sfuClient, connectionConfig, clientDetails, statsOptions, publishOptions, closePreviousInstances, unifiedSessionId, } = opts;
16176
16448
  const { enable_rtc_stats: enableTracing, reporting_interval_ms: reportingIntervalMs, } = statsOptions;
16177
16449
  // Flush the previous reporter's final sample while its peer connections are
16178
16450
  // still alive, before we dispose them below. Awaits only the sampling step.
16179
16451
  await this.sfuStatsReporter?.flush();
16452
+ if (this.leaveGeneration !== joinLeaveGeneration)
16453
+ return;
16180
16454
  this.sfuStatsReporter?.stop();
16181
16455
  this.sfuStatsReporter = undefined;
16182
16456
  if (closePreviousInstances && this.subscriber) {
16183
16457
  await this.subscriber.dispose();
16458
+ if (this.leaveGeneration !== joinLeaveGeneration)
16459
+ return;
16184
16460
  this.state.removeAllOrphanedTracks();
16185
16461
  }
16186
16462
  const basePeerConnectionOptions = {
@@ -16223,6 +16499,8 @@ class Call {
16223
16499
  if (!isAnonymous) {
16224
16500
  if (closePreviousInstances && this.publisher) {
16225
16501
  await this.publisher.dispose();
16502
+ if (this.leaveGeneration !== joinLeaveGeneration)
16503
+ return;
16226
16504
  }
16227
16505
  this.publisher = new Publisher(basePeerConnectionOptions, publishOptions, {
16228
16506
  selfSubEnabled: this.allowOwnTracksLoopback,
@@ -16259,12 +16537,22 @@ class Call {
16259
16537
  *
16260
16538
  * @internal
16261
16539
  * @param data the join call data.
16540
+ * @returns The coordinator response, or undefined if leave superseded the request before it was sent.
16262
16541
  */
16263
16542
  doJoinRequest = async (data) => {
16543
+ const joinLeaveGeneration = this.leaveGeneration;
16264
16544
  const location = await this.streamClient.getLocationHint();
16545
+ if (this.leaveGeneration !== joinLeaveGeneration) {
16546
+ this.logger.debug('Join superseded by leave; not sending coordinator request');
16547
+ return;
16548
+ }
16265
16549
  const e2ee = !!this.e2eeManager;
16266
16550
  const request = { ...data, location, e2ee };
16267
16551
  const joinResponse = await this.streamClient.post(`${this.streamClientBasePath}/join`, request);
16552
+ if (this.leaveGeneration !== joinLeaveGeneration) {
16553
+ this.logger.debug('Join superseded by leave; not applying coordinator response');
16554
+ return joinResponse;
16555
+ }
16268
16556
  this.state.updateFromCallResponse(joinResponse.call);
16269
16557
  this.state.setMembers(joinResponse.members);
16270
16558
  this.state.setOwnCapabilities(joinResponse.own_capabilities);
@@ -16275,10 +16563,12 @@ class Call {
16275
16563
  if (!isReconnecting && this.ringing && !this.isCreatedByMe) {
16276
16564
  // signals other users that I have accepted the incoming call.
16277
16565
  await this.accept();
16566
+ if (this.leaveGeneration !== joinLeaveGeneration)
16567
+ return joinResponse;
16278
16568
  }
16279
16569
  if (this.streamClient._hasConnectionID()) {
16280
16570
  this.watching = true;
16281
- this.clientStore.registerOrUpdateCall(this);
16571
+ this.clientState.registerOrUpdateCall(this);
16282
16572
  }
16283
16573
  return joinResponse;
16284
16574
  };
@@ -16364,6 +16654,7 @@ class Call {
16364
16654
  if (callingState === CallingState.JOINING ||
16365
16655
  callingState === CallingState.RECONNECTING ||
16366
16656
  callingState === CallingState.MIGRATING ||
16657
+ callingState === CallingState.LEFT ||
16367
16658
  callingState === CallingState.RECONNECTING_FAILED)
16368
16659
  return;
16369
16660
  // Drop redundant reconnect calls. If a reconnect is already queued or
@@ -16372,19 +16663,27 @@ class Call {
16372
16663
  // `POST /join` per entry) once the call is already healthy again.
16373
16664
  if (hasPending(this.reconnectConcurrencyTag))
16374
16665
  return;
16666
+ const reconnectLeaveGeneration = this.leaveGeneration;
16667
+ const supersededByLeave = () => this.leaveGeneration !== reconnectLeaveGeneration;
16375
16668
  return withoutConcurrency(this.reconnectConcurrencyTag, async () => {
16669
+ if (supersededByLeave())
16670
+ return;
16376
16671
  const reconnectStartTime = Date.now();
16377
16672
  this.reconnectStrategy = strategy;
16378
16673
  this.reconnectReason = reason;
16379
16674
  const sfuRejoinFailures = new Map();
16380
16675
  const markAsReconnectingFailed = async () => {
16676
+ if (supersededByLeave())
16677
+ return;
16381
16678
  try {
16382
16679
  // attempt to fetch the call data from the server, as the call
16383
16680
  // state might have changed while we were reconnecting or were offline
16384
16681
  await this.get();
16385
16682
  }
16386
16683
  finally {
16387
- this.state.setCallingState(CallingState.RECONNECTING_FAILED);
16684
+ if (!supersededByLeave()) {
16685
+ this.state.setCallingState(CallingState.RECONNECTING_FAILED);
16686
+ }
16388
16687
  }
16389
16688
  };
16390
16689
  const giveUpAndLeave = async (message) => {
@@ -16440,6 +16739,8 @@ class Call {
16440
16739
  try {
16441
16740
  // wait until the network is available
16442
16741
  await this.networkAvailableTask?.promise;
16742
+ if (supersededByLeave())
16743
+ return;
16443
16744
  this.logger.info(`[Reconnect] Reconnecting with strategy ${WebsocketReconnectStrategy[this.reconnectStrategy]}`);
16444
16745
  switch (this.reconnectStrategy) {
16445
16746
  case WebsocketReconnectStrategy.UNSPECIFIED:
@@ -16479,6 +16780,8 @@ class Call {
16479
16780
  break; // do-while loop, reconnection worked, exit the loop
16480
16781
  }
16481
16782
  catch (error) {
16783
+ if (supersededByLeave())
16784
+ return;
16482
16785
  if (attemptedStrategy === WebsocketReconnectStrategy.REJOIN) {
16483
16786
  const failedSfu = this.credentials?.server.edge_name;
16484
16787
  if (failedSfu) {
@@ -16509,6 +16812,8 @@ class Call {
16509
16812
  }
16510
16813
  // exponential backoff with jitter, capped at 5 s
16511
16814
  await sleep(retryInterval(attempt));
16815
+ if (supersededByLeave())
16816
+ return;
16512
16817
  const wasMigrating = this.reconnectStrategy === WebsocketReconnectStrategy.MIGRATE;
16513
16818
  const mustPerformRejoin = (Date.now() - reconnectStartTime) / 1000 >
16514
16819
  this.fastReconnectDeadlineSeconds;
@@ -16527,7 +16832,8 @@ class Call {
16527
16832
  this.reconnectStrategy = nextStrategy;
16528
16833
  this.logger.info(`[Reconnect] ${currentStrategy} (${this.reconnectAttempts}) failed. Attempting with ${WebsocketReconnectStrategy[nextStrategy]}`, error);
16529
16834
  }
16530
- } while (this.state.callingState !== CallingState.JOINED &&
16835
+ } while (!supersededByLeave() &&
16836
+ this.state.callingState !== CallingState.JOINED &&
16531
16837
  this.state.callingState !== CallingState.RECONNECTING_FAILED &&
16532
16838
  this.state.callingState !== CallingState.LEFT);
16533
16839
  this.logger.info('[Reconnect] Reconnection flow finished');
@@ -16538,10 +16844,13 @@ class Call {
16538
16844
  * @internal
16539
16845
  */
16540
16846
  reconnectFast = async () => {
16847
+ const reconnectLeaveGeneration = this.leaveGeneration;
16541
16848
  const reconnectStartTime = Date.now();
16542
16849
  this.reconnectStrategy = WebsocketReconnectStrategy.FAST;
16543
16850
  this.state.setCallingState(CallingState.RECONNECTING);
16544
16851
  await this.doJoin(this.joinCallData);
16852
+ if (this.leaveGeneration !== reconnectLeaveGeneration)
16853
+ return;
16545
16854
  await this.get(); // fetch the latest call state, as it might have changed
16546
16855
  this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.FAST, (Date.now() - reconnectStartTime) / 1000);
16547
16856
  };
@@ -16550,14 +16859,19 @@ class Call {
16550
16859
  * @internal
16551
16860
  */
16552
16861
  reconnectRejoin = async () => {
16862
+ const reconnectLeaveGeneration = this.leaveGeneration;
16553
16863
  const reconnectStartTime = Date.now();
16554
16864
  this.reconnectStrategy = WebsocketReconnectStrategy.REJOIN;
16555
16865
  this.state.setCallingState(CallingState.RECONNECTING);
16556
16866
  const joinReason = this.reconnectReason === ReconnectReason.NETWORK_BACK_ONLINE
16557
16867
  ? 'network-available'
16558
16868
  : 'full-rejoin';
16559
- await this.clientEventReporter.withJoinLifecycle(this.cid, joinReason, () => this.doJoin(this.joinCallData));
16869
+ await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason }, () => this.doJoin(this.joinCallData));
16870
+ if (this.leaveGeneration !== reconnectLeaveGeneration)
16871
+ return;
16560
16872
  await this.restorePublishedTracks();
16873
+ if (this.leaveGeneration !== reconnectLeaveGeneration)
16874
+ return;
16561
16875
  this.restoreSubscribedTracks();
16562
16876
  this.sfuStatsReporter?.sendReconnectionTime(WebsocketReconnectStrategy.REJOIN, (Date.now() - reconnectStartTime) / 1000);
16563
16877
  };
@@ -16566,6 +16880,7 @@ class Call {
16566
16880
  * @internal
16567
16881
  */
16568
16882
  reconnectMigrate = async () => {
16883
+ const reconnectLeaveGeneration = this.leaveGeneration;
16569
16884
  const reconnectStartTime = Date.now();
16570
16885
  const currentSfuClient = this.sfuClient;
16571
16886
  if (!currentSfuClient) {
@@ -16579,26 +16894,32 @@ class Call {
16579
16894
  currentPublisher?.detachEventHandlers();
16580
16895
  const migrationTask = makeSafePromise(currentSfuClient.enterMigration());
16581
16896
  try {
16582
- const currentSfu = currentSfuClient.edgeName;
16583
- await this.clientEventReporter.withJoinLifecycle(this.cid, 'migration', () => this.doJoin({
16584
- ...this.joinCallData,
16585
- migrating_from: currentSfu,
16586
- migrating_from_list: [currentSfu],
16587
- }));
16588
- }
16589
- finally {
16590
- // cleanup the migration_from field after the migration is complete or failed
16591
- // as we don't want to keep dirty data in the join call data
16592
- delete this.joinCallData?.migrating_from;
16593
- delete this.joinCallData?.migrating_from_list;
16594
- }
16595
- await this.restorePublishedTracks();
16596
- this.restoreSubscribedTracks();
16597
- try {
16897
+ try {
16898
+ const currentSfu = currentSfuClient.edgeName;
16899
+ await this.clientEventReporter.withJoinLifecycle(this.cid, { joinReason: 'migration' }, () => this.doJoin({
16900
+ ...this.joinCallData,
16901
+ migrating_from: currentSfu,
16902
+ migrating_from_list: [currentSfu],
16903
+ }));
16904
+ }
16905
+ finally {
16906
+ // cleanup the migration_from field after the migration is complete or failed
16907
+ // as we don't want to keep dirty data in the join call data
16908
+ delete this.joinCallData?.migrating_from;
16909
+ delete this.joinCallData?.migrating_from_list;
16910
+ }
16911
+ if (this.leaveGeneration !== reconnectLeaveGeneration)
16912
+ return;
16913
+ await this.restorePublishedTracks();
16914
+ if (this.leaveGeneration !== reconnectLeaveGeneration)
16915
+ return;
16916
+ this.restoreSubscribedTracks();
16598
16917
  // Wait for the migration to complete, then close the previous SFU client
16599
16918
  // and the peer connection instances. In case of failure, the migration
16600
16919
  // task would throw an error and REJOIN would be attempted.
16601
16920
  await migrationTask();
16921
+ if (this.leaveGeneration !== reconnectLeaveGeneration)
16922
+ return;
16602
16923
  // in MIGRATE, we can consider the call as joined only after
16603
16924
  // `participantMigrationComplete` event is received, signaled by
16604
16925
  // the `migrationTask`
@@ -16883,10 +17204,11 @@ class Call {
16883
17204
  * Must be called before {@link join} so the RTCPeerConnection can be
16884
17205
  * configured for E2EE.
16885
17206
  *
16886
- * The manager is kept across {@link leave} so a rejoin of this same instance
16887
- * stays encrypted: do not dispose it while this call may be joined again.
16888
- * A disposed manager throws from `encrypt`/`decrypt` rather than silently
16889
- * publishing nothing, so re-attach a fresh one instead of reusing it.
17207
+ * The application owns the manager's lifetime - the SDK never disposes it, and
17208
+ * closing the peer connections does not release it. Discard the manager
17209
+ * together with this call instance once its flow has ended, and create a fresh
17210
+ * pair for a later flow: a disposed manager throws from `encrypt`/`decrypt`
17211
+ * rather than silently publishing nothing.
16890
17212
  *
16891
17213
  * @param e2ee - Any `E2EEManager`. Use `EncryptionManager.create()` for the
16892
17214
  * built-in AES-GCM scheme, or pass your own implementation.
@@ -16934,7 +17256,7 @@ class Call {
16934
17256
  });
16935
17257
  };
16936
17258
  /**
16937
- * Will enhance the reported stats with additional participant-specific information (`callStatsReport$` state [store variable](./StreamVideoClient.md/#readonlystatestore)).
17259
+ * Will enhance the reported stats with additional participant-specific information (the `callStatsReport$` state variable).
16938
17260
  * This is usually helpful when detailed stats for a specific participant are needed.
16939
17261
  *
16940
17262
  * @param sessionId the sessionId to start reporting for.
@@ -17353,40 +17675,36 @@ class Call {
17353
17675
  */
17354
17676
  scheduleAutoDrop = () => {
17355
17677
  this.cancelAutoDrop();
17356
- const settings = this.state.settings;
17357
- if (!settings)
17358
- return;
17359
- // ignore if the call is not ringing
17360
- if (this.state.callingState !== CallingState.RINGING)
17361
- return;
17362
- const timeoutInMs = this.isCreatedByMe
17363
- ? settings.ring.auto_cancel_timeout_ms
17364
- : settings.ring.incoming_call_timeout_ms;
17365
- // 0 means no auto-drop
17366
- if (timeoutInMs <= 0)
17367
- return;
17368
- this.dropTimeout = setTimeout(() => {
17369
- // the call might have stopped ringing by this point,
17370
- // e.g. it was already accepted and joined
17371
- if (this.state.callingState !== CallingState.RINGING)
17372
- return;
17373
- this.leave({
17374
- reject: true,
17375
- reason: 'timeout',
17376
- message: `ringing timeout - ${this.isCreatedByMe
17377
- ? 'no one accepted'
17378
- : `user didn't interact with incoming call screen`}`,
17379
- }).catch((err) => {
17380
- this.logger.error('Failed to drop call', err);
17381
- });
17382
- }, timeoutInMs);
17678
+ this.ringTimeout = new RingTimeout(this);
17679
+ this.ringTimeout.start();
17383
17680
  };
17384
17681
  /**
17385
17682
  * Cancels a scheduled auto-drop timeout.
17386
17683
  */
17387
17684
  cancelAutoDrop = () => {
17388
- clearTimeout(this.dropTimeout);
17389
- this.dropTimeout = undefined;
17685
+ this.ringTimeout?.stop();
17686
+ this.ringTimeout = undefined;
17687
+ };
17688
+ /**
17689
+ * Starts polling for the ring outcome. Applicable only to ringing calls the
17690
+ * current user created.
17691
+ */
17692
+ scheduleRingStatePolling = () => {
17693
+ this.cancelRingStatePolling();
17694
+ if (!this.isCreatedByMe)
17695
+ return;
17696
+ const options = this.streamClient.options.ringStatePolling;
17697
+ if (options === false)
17698
+ return;
17699
+ this.ringStatePoller = new RingStatePoller(this, options);
17700
+ this.ringStatePoller.start();
17701
+ };
17702
+ /**
17703
+ * Cancels the ring state polling.
17704
+ */
17705
+ cancelRingStatePolling = () => {
17706
+ this.ringStatePoller?.stop();
17707
+ this.ringStatePoller = undefined;
17390
17708
  };
17391
17709
  /**
17392
17710
  * Retrieves the list of recordings for the current call or call session.
@@ -19059,11 +19377,11 @@ class StreamClient {
19059
19377
  return await this.wsConnection.connect(this.defaultWSTimeout);
19060
19378
  };
19061
19379
  getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
19062
- "2.0.0-beta.0";
19380
+ "2.0.0-beta.1";
19063
19381
  getUserAgent = () => {
19064
19382
  if (!this.cachedUserAgent) {
19065
19383
  const { clientAppIdentifier = {} } = this.options;
19066
- const { sdkName = 'js', sdkVersion = "2.0.0-beta.0", ...extras } = clientAppIdentifier;
19384
+ const { sdkName = 'js', sdkVersion = "2.0.0-beta.1", ...extras } = clientAppIdentifier;
19067
19385
  this.cachedUserAgent = [
19068
19386
  `stream-video-${sdkName}-v${sdkVersion}`,
19069
19387
  ...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
@@ -19196,6 +19514,7 @@ class ClientEventReporter {
19196
19514
  callContexts = new Map();
19197
19515
  joinAttemptIds = new Map();
19198
19516
  joinReasons = new Map();
19517
+ joinSources = new Map();
19199
19518
  coordinatorPairs = new Map();
19200
19519
  wsPairs = new Map();
19201
19520
  peerConnectionPairs = new Map();
@@ -19310,6 +19629,7 @@ class ClientEventReporter {
19310
19629
  this.callContexts.delete(cid);
19311
19630
  this.joinAttemptIds.delete(cid);
19312
19631
  this.joinReasons.delete(cid);
19632
+ this.joinSources.delete(cid);
19313
19633
  this.coordinatorPairs.delete(cid);
19314
19634
  this.wsPairs.delete(cid);
19315
19635
  this.firstFrameReported.delete(`${cid}:FirstVideoFrame`);
@@ -19334,7 +19654,12 @@ class ClientEventReporter {
19334
19654
  this.logger.warn('Failed to start join correlation', err);
19335
19655
  }
19336
19656
  };
19337
- withJoinLifecycle = async (cid, joinReason, op) => {
19657
+ withJoinLifecycle = async (cid, options, op) => {
19658
+ const { joinReason, joinSource } = options;
19659
+ if (joinSource)
19660
+ this.joinSources.set(cid, joinSource);
19661
+ else
19662
+ this.joinSources.delete(cid);
19338
19663
  this.startCorrelation(cid, joinReason);
19339
19664
  try {
19340
19665
  return await op();
@@ -19343,6 +19668,9 @@ class ClientEventReporter {
19343
19668
  this.closeCallPairs(cid);
19344
19669
  throw err;
19345
19670
  }
19671
+ finally {
19672
+ this.joinSources.delete(cid);
19673
+ }
19346
19674
  };
19347
19675
  track = async (cid, stage, op) => {
19348
19676
  this.beginAttempt(cid, stage);
@@ -19466,6 +19794,7 @@ class ClientEventReporter {
19466
19794
  startedAt: Date.now(),
19467
19795
  joinAttemptIdSnapshot: this.joinAttemptIds.get(cid),
19468
19796
  joinReasonSnapshot: this.joinReasons.get(cid),
19797
+ joinSourceSnapshot: this.joinSources.get(cid),
19469
19798
  };
19470
19799
  this.coordinatorPairs.set(cid, pair);
19471
19800
  this.sendForCall(cid, {
@@ -19473,6 +19802,7 @@ class ClientEventReporter {
19473
19802
  ...(pair.joinReasonSnapshot && {
19474
19803
  join_reason: pair.joinReasonSnapshot,
19475
19804
  }),
19805
+ ...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
19476
19806
  event_type: 'initiated',
19477
19807
  });
19478
19808
  }
@@ -19487,6 +19817,7 @@ class ClientEventReporter {
19487
19817
  ...this.buildCommon(cid, 'CoordinatorJoin', pair),
19488
19818
  ...this.sessionIdField(cid),
19489
19819
  ...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
19820
+ ...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
19490
19821
  event_type: 'completed',
19491
19822
  outcome: 'success',
19492
19823
  retry_count_attempt: pair.attempts - 1,
@@ -19505,6 +19836,7 @@ class ClientEventReporter {
19505
19836
  ...this.buildCommon(cid, 'CoordinatorJoin', pair),
19506
19837
  ...this.sessionIdField(cid),
19507
19838
  ...(pair.joinReasonSnapshot && { join_reason: pair.joinReasonSnapshot }),
19839
+ ...(pair.joinSourceSnapshot && { source: pair.joinSourceSnapshot }),
19508
19840
  event_type: 'completed',
19509
19841
  outcome: 'failure',
19510
19842
  retry_count_attempt: pair.attempts - 1,
@@ -19805,16 +20137,11 @@ const mapWsJoinError = (err) => {
19805
20137
  */
19806
20138
  class StreamVideoClient {
19807
20139
  /**
19808
- * A reactive store that exposes all the state variables reactively.
20140
+ * The reactive state of this client.
19809
20141
  * You can subscribe to changes of the different state variables.
19810
- * Our library is built in a way that all state changes are exposed in this store,
19811
- * o all UI changes in your application should be handled by subscribing to these variables.
19812
- *
19813
- * @deprecated use the `client.state` getter.
19814
20142
  */
19815
- readOnlyStateStore;
20143
+ state = new ClientState();
19816
20144
  logger;
19817
- writeableStateStore;
19818
20145
  streamClient;
19819
20146
  clientEventReporter;
19820
20147
  effectsRegistered = false;
@@ -19839,8 +20166,6 @@ class StreamVideoClient {
19839
20166
  streamClient: this.streamClient,
19840
20167
  enabled: clientOptions?.clientEventsReportingEnabled ?? true,
19841
20168
  });
19842
- this.writeableStateStore = new StreamVideoWriteableStateStore();
19843
- this.readOnlyStateStore = new StreamVideoReadOnlyStateStore(this.writeableStateStore);
19844
20169
  if (typeof apiKeyOrArgs !== 'string' && apiKeyOrArgs.user) {
19845
20170
  const user = apiKeyOrArgs.user;
19846
20171
  if (user.type === 'anonymous')
@@ -19877,19 +20202,13 @@ class StreamVideoClient {
19877
20202
  }
19878
20203
  StreamVideoClient._instances.set(instanceKey, this);
19879
20204
  };
19880
- /**
19881
- * Return the reactive state store, use this if you want to be notified about changes to the client state
19882
- */
19883
- get state() {
19884
- return this.readOnlyStateStore;
19885
- }
19886
20205
  registerEffects = () => {
19887
20206
  if (this.effectsRegistered)
19888
20207
  return;
19889
20208
  this.eventHandlersToUnregister.push(this.on('call.created', (event) => this.initCallFromEvent(event)), this.on('call.ring', (event) => this.initCallFromEvent(event)), this.on('connection.changed', (event) => {
19890
20209
  if (!event.online)
19891
20210
  return;
19892
- const callsToReWatch = this.writeableStateStore.calls
20211
+ const callsToReWatch = this.state.calls
19893
20212
  .filter((call) => call.watching)
19894
20213
  .map((call) => call.cid);
19895
20214
  if (callsToReWatch.length <= 0)
@@ -19909,7 +20228,7 @@ class StreamVideoClient {
19909
20228
  const concurrencyTag = getCallInitConcurrencyTag(e.call_cid);
19910
20229
  await withoutConcurrency(concurrencyTag, async () => {
19911
20230
  const ringing = e.type === 'call.ring';
19912
- let call = this.writeableStateStore.findCall(e.call.type, e.call.id);
20231
+ let call = this.state.findCall(e.call.type, e.call.id);
19913
20232
  if (call) {
19914
20233
  if (ringing) {
19915
20234
  if (this.shouldRejectCall(call.cid)) {
@@ -19935,7 +20254,7 @@ class StreamVideoClient {
19935
20254
  type: e.call.type,
19936
20255
  id: e.call.id,
19937
20256
  members: e.members,
19938
- clientStore: this.writeableStateStore,
20257
+ clientState: this.state,
19939
20258
  ringing,
19940
20259
  });
19941
20260
  if (ringing) {
@@ -19951,7 +20270,7 @@ class StreamVideoClient {
19951
20270
  }
19952
20271
  else {
19953
20272
  call.state.updateFromCallResponse(e.call);
19954
- this.writeableStateStore.registerCall(call);
20273
+ this.state.registerCall(call);
19955
20274
  this.logger.info(`New call created and registered: ${call.cid}`);
19956
20275
  }
19957
20276
  });
@@ -19982,7 +20301,7 @@ class StreamVideoClient {
19982
20301
  filter_conditions: { cid: { $in: callsToReWatch } },
19983
20302
  });
19984
20303
  for (const c of response.calls) {
19985
- const call = this.writeableStateStore.findCall(c.call.type, c.call.id);
20304
+ const call = this.state.findCall(c.call.type, c.call.id);
19986
20305
  if (call) {
19987
20306
  call.updateFromCallStateResponse(c);
19988
20307
  }
@@ -20003,8 +20322,6 @@ class StreamVideoClient {
20003
20322
  };
20004
20323
  /**
20005
20324
  * Connects the given user to the client.
20006
- * Only one user can connect at a time, if you want to change users, call `disconnectUser` before connecting a new user.
20007
- * If the connection is successful, the connected user [state variable](#readonlystatestore) will be updated accordingly.
20008
20325
  *
20009
20326
  * @param user the user to connect.
20010
20327
  * @param tokenOrProvider a token or a function that returns a token.
@@ -20048,24 +20365,40 @@ class StreamVideoClient {
20048
20365
  });
20049
20366
  // connectUserResponse will be void if connectUser called twice for the same user
20050
20367
  if (connectUserResponse?.me) {
20051
- this.writeableStateStore.setConnectedUser(connectUserResponse.me);
20368
+ this.state.setConnectedUser(connectUserResponse.me);
20052
20369
  }
20053
20370
  this.registerEffects();
20054
20371
  return connectUserResponse;
20055
20372
  };
20373
+ /**
20374
+ * Leaves every call this client still tracks. Runs while the coordinator
20375
+ * connection is alive, so that the leave requests each call sends reach the
20376
+ * backend.
20377
+ */
20378
+ leaveAllCalls = async () => {
20379
+ for (const call of this.state.calls) {
20380
+ if (call.state.callingState === CallingState.LEFT)
20381
+ continue;
20382
+ this.logger.info(`User disconnected, leaving call: ${call.cid}`);
20383
+ await call
20384
+ .leave({ message: 'client.disconnectUser() called' })
20385
+ .catch((err) => {
20386
+ this.logger.error(`Error leaving call: ${call.cid}`, err);
20387
+ });
20388
+ }
20389
+ };
20056
20390
  /**
20057
20391
  * Disconnects the currently connected user from the client.
20058
- *
20059
- * If the connection is successfully disconnected, the connected user [state variable](#readonlystatestore) will be updated accordingly
20392
+ * Leaves every call the user is still in, and updates `client.state`accordingly.
20060
20393
  *
20061
20394
  * @param timeout Max number of ms, to wait for close event of websocket, before forcefully assuming successful disconnection.
20062
- * https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
20063
20395
  */
20064
20396
  disconnectUser = async (timeout) => {
20065
20397
  await withoutConcurrency(this.connectionConcurrencyTag, async () => {
20066
20398
  const { user, key } = this.streamClient;
20067
20399
  if (!user)
20068
20400
  return;
20401
+ await this.leaveAllCalls();
20069
20402
  await this.streamClient.disconnectUser(timeout);
20070
20403
  if (user.id) {
20071
20404
  StreamVideoClient._instances.delete(getInstanceKey(key, user));
@@ -20073,13 +20406,15 @@ class StreamVideoClient {
20073
20406
  this.eventHandlersToUnregister.forEach((unregister) => unregister());
20074
20407
  this.eventHandlersToUnregister = [];
20075
20408
  this.effectsRegistered = false;
20076
- this.writeableStateStore.setConnectedUser(undefined);
20409
+ this.state.setConnectedUser(undefined);
20077
20410
  });
20078
20411
  };
20079
20412
  /**
20080
20413
  * You can subscribe to WebSocket events provided by the API.
20081
20414
  * To remove a subscription, call the `off` method or, execute the returned unsubscribe function.
20082
- * 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).
20415
+ * Please note that subscribing to WebSocket events is an advanced use-case,
20416
+ * for most use-cases it should be enough to watch for changes in the reactive
20417
+ * `client.state`.
20083
20418
  *
20084
20419
  * @param eventName the event name or 'all'.
20085
20420
  * @param callback the callback which will be called when the event is emitted.
@@ -20106,7 +20441,7 @@ class StreamVideoClient {
20106
20441
  */
20107
20442
  call = (type, id, options = {}) => {
20108
20443
  const call = options.reuseInstance
20109
- ? this.writeableStateStore.findCall(type, id)
20444
+ ? this.state.findCall(type, id)
20110
20445
  : undefined;
20111
20446
  return (call ??
20112
20447
  new Call({
@@ -20114,7 +20449,7 @@ class StreamVideoClient {
20114
20449
  clientEventReporter: this.clientEventReporter,
20115
20450
  id: id,
20116
20451
  type: type,
20117
- clientStore: this.writeableStateStore,
20452
+ clientState: this.state,
20118
20453
  }));
20119
20454
  };
20120
20455
  /**
@@ -20144,7 +20479,7 @@ class StreamVideoClient {
20144
20479
  members: c.members,
20145
20480
  ownCapabilities: c.own_capabilities,
20146
20481
  watching: data.watch,
20147
- clientStore: this.writeableStateStore,
20482
+ clientState: this.state,
20148
20483
  });
20149
20484
  call.state.updateFromCallResponse(c.call);
20150
20485
  await call.applyDeviceConfig(c.call.settings, {
@@ -20153,7 +20488,7 @@ class StreamVideoClient {
20153
20488
  });
20154
20489
  if (data.watch) {
20155
20490
  await call.setup();
20156
- this.writeableStateStore.registerCall(call);
20491
+ this.state.registerCall(call);
20157
20492
  }
20158
20493
  calls.push(call);
20159
20494
  }
@@ -20252,7 +20587,7 @@ class StreamVideoClient {
20252
20587
  clientEventReporter: this.clientEventReporter,
20253
20588
  type: callType,
20254
20589
  id: callId,
20255
- clientStore: this.writeableStateStore,
20590
+ clientState: this.state,
20256
20591
  ringing: true,
20257
20592
  });
20258
20593
  await call.get();
@@ -20758,9 +21093,9 @@ class EncryptionManager extends TypedEventEmitter {
20758
21093
  }
20759
21094
  if ((this.piped ??= new WeakSet()).has(target))
20760
21095
  return;
20761
- this.piped.add(target);
20762
21096
  // @ts-expect-error createEncodedStreams is not in the standard typedefs
20763
21097
  const { readable, writable } = target.createEncodedStreams();
21098
+ this.piped.add(target);
20764
21099
  this.worker.postMessage({ type: 'cmd.setup_transform', ...options, readable, writable }, [readable, writable]);
20765
21100
  };
20766
21101
  /**
@@ -20816,5 +21151,5 @@ class EncryptionManager extends TypedEventEmitter {
20816
21151
  };
20817
21152
  }
20818
21153
 
20819
- export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, CreateDeviceRequestPushProviderEnum, DEFAULT_LOOPBACK_RECORDING_DURATION_MS, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, EncryptionManager, EncryptionSettingsRequestModeEnum, EncryptionSettingsResponseModeEnum, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, HLSSettingsRequestQualityTracksEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsRequestOutputTypesEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LOOPBACK_STREAMS_WAIT_TIMEOUT_MS, LayoutSettingsRequestNameEnum, LoopbackStreamsTimeoutError, MAX_LOOPBACK_RECORDING_DURATION_MS, MIN_LOOPBACK_RECORDING_DURATION_MS, MediaStreamRecorder, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, RequestPermissionRequestPermissionsEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SfuTimeoutError, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, StreamVideoReadOnlyStateStore, StreamVideoWriteableStateStore, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, TranslationSettingsLanguagesEnum, UpdateUserPermissionsRequestGrantPermissionsEnum, UpdateUserPermissionsRequestRevokePermissionsEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, WSAuthMessageProductsEnum, checkIfAudioOutputChangeSupported, clampLoopbackRecordingDuration, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, enableLoopbackAudio, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getCallMediaEngineProvider, getClientDetails, getDeviceState, getLoopbackStreams, getLoopbackTracks, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishing, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setCallMediaEngineProvider, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, waitForLoopbackStreams, withLoopbackAudioEnabled, withParticipantSource };
21154
+ export { AudioSettingsRequestDefaultDeviceEnum, AudioSettingsResponseDefaultDeviceEnum, browsers as Browsers, Call, CallRecordingFailedEventRecordingTypeEnum, CallRecordingReadyEventRecordingTypeEnum, CallRecordingStartedEventRecordingTypeEnum, CallRecordingStoppedEventRecordingTypeEnum, CallState, CallType, CallTypes, CallingState, CameraManager, CameraManagerState, ClientState, CreateDeviceRequestPushProviderEnum, DEFAULT_LOOPBACK_RECORDING_DURATION_MS, DebounceType, DeviceManager, DeviceManagerState, DynascaleManager, EncryptionManager, EncryptionSettingsRequestModeEnum, EncryptionSettingsResponseModeEnum, ErrorFromResponse, FrameRecordingSettingsRequestModeEnum, FrameRecordingSettingsRequestQualityEnum, FrameRecordingSettingsResponseModeEnum, HLSSettingsRequestQualityTracksEnum, IndividualRecordingSettingsRequestModeEnum, IndividualRecordingSettingsRequestOutputTypesEnum, IndividualRecordingSettingsResponseModeEnum, IngressAudioEncodingOptionsRequestChannelsEnum, IngressSourceRequestFpsEnum, IngressVideoLayerRequestCodecEnum, LOOPBACK_STREAMS_WAIT_TIMEOUT_MS, LayoutSettingsRequestNameEnum, LoopbackStreamsTimeoutError, MAX_LOOPBACK_RECORDING_DURATION_MS, MIN_LOOPBACK_RECORDING_DURATION_MS, MediaStreamRecorder, MicrophoneManager, MicrophoneManagerState, NoiseCancellationSettingsModeEnum, OwnCapability, RTMPBroadcastRequestQualityEnum, RTMPSettingsRequestQualityEnum, RawRecordingSettingsRequestModeEnum, RawRecordingSettingsResponseModeEnum, RecordSettingsRequestModeEnum, RecordSettingsRequestQualityEnum, RequestPermissionRequestPermissionsEnum, rxUtils as RxUtils, ScreenShareManager, ScreenShareState, events as SfuEvents, SfuJoinError, models as SfuModels, SfuTimeoutError, SpeakerManager, SpeakerState, StartClosedCaptionsRequestLanguageEnum, StartTranscriptionRequestLanguageEnum, StreamSfuClient, StreamVideoClient, TranscriptionSettingsRequestClosedCaptionModeEnum, TranscriptionSettingsRequestLanguageEnum, TranscriptionSettingsRequestModeEnum, TranscriptionSettingsResponseClosedCaptionModeEnum, TranscriptionSettingsResponseLanguageEnum, TranscriptionSettingsResponseModeEnum, TranslationSettingsLanguagesEnum, TypedEventEmitter, UpdateUserPermissionsRequestGrantPermissionsEnum, UpdateUserPermissionsRequestRevokePermissionsEnum, VideoSettingsRequestCameraFacingEnum, VideoSettingsResponseCameraFacingEnum, ViewportTracker, VisibilityState, WSAuthMessageProductsEnum, checkIfAudioOutputChangeSupported, clampLoopbackRecordingDuration, combineComparators, conditional, createSoundDetector, defaultSortPreset, descending, deviceIds$, disposeOfMediaStream, dominantSpeaker, enableLoopbackAudio, getAudioBrowserPermission, getAudioDevices, getAudioOutputDevices, getAudioStream, getCallMediaEngineProvider, getClientDetails, getDeviceState, getLoopbackStreams, getLoopbackTracks, getScreenShareStream, getSdkInfo, getVideoBrowserPermission, getVideoDevices, getVideoStream, getWebRTCInfo, hasAudio$1 as hasAudio, hasInterruptedTrack, hasPausedTrack, hasScreenShare, hasScreenShareAudio, hasVideo, humanize, isPinned, livestreamOrAudioRoomSortPreset, logToConsole, name, noopComparator, paginatedLayoutSortPreset, pinned, publishing, publishingAudio, publishingVideo, reactionType, resolveDeviceId, role, screenSharing, setCallMediaEngineProvider, setDeviceInfo, setOSInfo, setPowerState, setSdkInfo, setThermalState, setWebRTCInfo, speakerLayoutSortPreset, speaking, videoLoggerSystem, waitForLoopbackStreams, withLoopbackAudioEnabled, withParticipantSource };
20820
21155
  //# sourceMappingURL=index.browser.es.js.map