@stream-io/video-client 2.0.0-beta.0 → 2.0.0-beta.1

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