@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
@@ -1,15 +1,18 @@
1
1
  import {
2
- BehaviorSubject,
3
2
  combineLatest,
4
3
  distinctUntilChanged,
5
4
  map,
6
- Observable,
7
5
  ReplaySubject,
8
- shareReplay,
9
6
  startWith,
10
7
  } from 'rxjs';
11
- import type { Patch } from './rxUtils';
12
- import * as RxUtils from './rxUtils';
8
+ import {
9
+ getCurrentValue,
10
+ isShallowArrayEqual,
11
+ type Patch,
12
+ setCurrentValue,
13
+ updateValue,
14
+ } from './rxUtils';
15
+ import { duc, shared, subject } from './subjects';
13
16
  import { CallingState } from './CallingState';
14
17
  import {
15
18
  type CallRecordingType,
@@ -37,6 +40,7 @@ import {
37
40
  CallSessionParticipantLeftEvent,
38
41
  CallSessionResponse,
39
42
  CallSettingsResponse,
43
+ GetCallRingStateResponse,
40
44
  ClosedCaptionEvent,
41
45
  EgressResponse,
42
46
  MemberResponse,
@@ -59,10 +63,9 @@ import { Comparator, defaultSortPreset } from '../sorting';
59
63
  import { ensureExhausted } from '../helpers/ensureExhausted';
60
64
  import { hasScreenShare } from '../helpers/participantUtils';
61
65
  import { videoLoggerSystem } from '../logger';
66
+ import type { AllEventHandlers, CallStateEventHandlers } from './types';
62
67
 
63
- /**
64
- * Returns the default egress object - when no egress data is available.
65
- */
68
+ /** Returns the default egress object - when no egress data is available. */
66
69
  const defaultEgress: EgressResponse = {
67
70
  broadcasting: false,
68
71
  hls: { playlist_url: '', status: '' },
@@ -82,359 +85,188 @@ type OrphanedTrack = {
82
85
  * @react You don't have to use this class directly, as we are exposing the state through Hooks.
83
86
  */
84
87
  export class CallState {
85
- private backstageSubject = new BehaviorSubject<boolean>(true);
86
- private blockedUserIdsSubject = new BehaviorSubject<string[]>([]);
87
- private createdAtSubject = new BehaviorSubject<Date>(new Date());
88
- private endedAtSubject = new BehaviorSubject<Date | undefined>(undefined);
89
- private startsAtSubject = new BehaviorSubject<Date | undefined>(undefined);
90
- private updatedAtSubject = new BehaviorSubject<Date>(new Date());
91
- private createdBySubject = new BehaviorSubject<UserResponse | undefined>(
92
- undefined,
93
- );
94
- private customSubject = new BehaviorSubject<Record<string, any>>({});
95
- private egressSubject = new BehaviorSubject<EgressResponse | undefined>(
96
- undefined,
97
- );
98
- private ingressSubject = new BehaviorSubject<CallIngressResponse | undefined>(
99
- undefined,
100
- );
101
- private recordingSubject = new BehaviorSubject<boolean>(false);
102
- private individualRecordingSubject = new BehaviorSubject<boolean>(false);
103
- private rawRecordingSubject = new BehaviorSubject<boolean>(false);
104
- private sessionSubject = new BehaviorSubject<CallSessionResponse | undefined>(
105
- undefined,
106
- );
107
- private settingsSubject = new BehaviorSubject<
108
- CallSettingsResponse | undefined
109
- >(undefined);
110
- private transcribingSubject = new BehaviorSubject<boolean>(false);
111
- private captioningSubject = new BehaviorSubject<boolean>(false);
112
- private e2eeEnabledSubject = new BehaviorSubject<boolean>(false);
113
- private endedBySubject = new BehaviorSubject<UserResponse | undefined>(
114
- undefined,
115
- );
116
- private thumbnailsSubject = new BehaviorSubject<
117
- ThumbnailResponse | undefined
118
- >(undefined);
119
- private membersSubject = new BehaviorSubject<MemberResponse[]>([]);
120
- private ownCapabilitiesSubject = new BehaviorSubject<OwnCapability[]>([]);
121
- private callingStateSubject = new BehaviorSubject<CallingState>(
122
- CallingState.UNKNOWN,
123
- );
124
- private startedAtSubject = new BehaviorSubject<Date | undefined>(undefined);
125
- private participantCountSubject = new BehaviorSubject<number>(0);
126
- private anonymousParticipantCountSubject = new BehaviorSubject<number>(0);
127
- private participantsSubject = new BehaviorSubject<StreamVideoParticipant[]>(
128
- [],
129
- );
130
- private callStatsReportSubject = new BehaviorSubject<
131
- CallStatsReport | undefined
132
- >(undefined);
133
- private closedCaptionsSubject = new BehaviorSubject<CallClosedCaption[]>([]);
134
-
135
- // These are tracks that were delivered to the Subscriber's onTrack event
136
- // that we couldn't associate with a participant yet.
137
- // This happens when the participantJoined event hasn't been received yet.
138
- // We keep these tracks around until we can associate them with a participant.
139
- private orphanedTracks: OrphanedTrack[] = [];
88
+ readonly logger = videoLoggerSystem.getLogger('CallState');
89
+ /** A list of comparators that are used to sort the participants. */
90
+ private sortParticipantsBy = defaultSortPreset;
140
91
 
92
+ private backstageSubject = subject(true);
93
+ private blockedUserIdsSubject = subject<string[]>([]);
94
+ private createdAtSubject = subject(new Date());
95
+ private endedAtSubject = subject<Date>();
96
+ private startsAtSubject = subject<Date>();
97
+ private updatedAtSubject = subject(new Date());
98
+ private createdBySubject = subject<UserResponse>();
99
+ private customSubject = subject<Record<string, any>>({});
100
+ private egressSubject = subject<EgressResponse>();
101
+ private ingressSubject = subject<CallIngressResponse>();
102
+ private recordingSubject = subject(false);
103
+ private individualRecordingSubject = subject(false);
104
+ private rawRecordingSubject = subject(false);
105
+ private sessionSubject = subject<CallSessionResponse>();
106
+ private settingsSubject = subject<CallSettingsResponse>();
107
+ private transcribingSubject = subject(false);
108
+ private captioningSubject = subject(false);
109
+ private e2eeEnabledSubject = subject(false);
110
+ private endedBySubject = subject<UserResponse>();
111
+ private thumbnailsSubject = subject<ThumbnailResponse>();
112
+ private membersSubject = subject<MemberResponse[]>([]);
113
+ private ownCapabilitiesSubject = subject<OwnCapability[]>([]);
114
+ private callingStateSubject = subject<CallingState>(CallingState.UNKNOWN);
115
+ private startedAtSubject = subject<Date>();
116
+ private participantCountSubject = subject(0);
117
+ private anonymousParticipantCountSubject = subject(0);
118
+ private participantsSubject = subject<StreamVideoParticipant[]>([]);
119
+ private callStatsReportSubject = subject<CallStatsReport>();
120
+ private closedCaptionsSubject = subject<CallClosedCaption[]>([]);
141
121
  private callGrantsSubject = new ReplaySubject<CallGrants>(1);
142
122
 
143
- // Derived state
144
-
145
- /**
146
- * The time the call session actually started.
147
- * Useful for displaying the call duration.
148
- */
149
- startedAt$: Observable<Date | undefined>;
150
-
151
- /**
152
- * The server-side counted number of participants connected to the current call.
153
- * This number includes the anonymous participants as well.
154
- */
155
- participantCount$: Observable<number>;
156
-
157
- /**
158
- * The server-side counted number of anonymous participants connected to the current call.
159
- * This number excludes the regular participants.
160
- */
161
- anonymousParticipantCount$: Observable<number>;
162
-
163
- /**
164
- * All participants of the current call (this includes the current user and other participants as well),
165
- * unsorted. This observable only updates when participants join or leave the call.
166
- */
167
- rawParticipants$: Observable<StreamVideoParticipant[]>;
168
-
169
- /**
170
- * All participants of the current call (this includes the current user and other participants as well),
171
- * sorted according to the current `sortByParticipantsBy` setting
172
- */
173
- participants$: Observable<StreamVideoParticipant[]>;
174
-
175
- /**
176
- * Remote participants of the current call (this includes every participant except the logged-in user).
177
- */
178
- remoteParticipants$: Observable<StreamVideoParticipant[]>;
179
-
180
- /**
181
- * The local participant of the current call (the logged-in user).
182
- */
183
- localParticipant$: Observable<StreamVideoParticipant | undefined>;
184
-
185
- /**
186
- * Pinned participants of the current call.
187
- */
188
- pinnedParticipants$: Observable<StreamVideoParticipant[]>;
123
+ /** The backstage state. */
124
+ backstage$ = duc(this.backstageSubject);
189
125
 
190
- /**
191
- * The currently elected dominant speaker in the current call.
192
- */
193
- dominantSpeaker$: Observable<StreamVideoParticipant | undefined>;
126
+ /** Will provide the list of blocked user IDs. */
127
+ blockedUserIds$ = duc(this.blockedUserIdsSubject, isShallowArrayEqual);
194
128
 
195
- /**
196
- * Emits true whenever there is an active screen sharing session within
197
- * the current call. Useful for displaying a "screen sharing" indicator and
198
- * switching the layout to a screen sharing layout.
199
- *
200
- * The actual screen sharing track isn't exposed here, but can be retrieved
201
- * from the list of call participants. We also don't want to be limiting
202
- * to the number of share screen tracks are displayed in a call.
203
- */
204
- hasOngoingScreenShare$: Observable<boolean>;
129
+ /** Will provide the time when this call has been created. */
130
+ createdAt$ = this.createdAtSubject.asObservable();
205
131
 
206
- /**
207
- * The latest stats report of the current call.
208
- * When stats gathering is enabled, this observable will emit a new value
209
- * at a regular (configurable) interval.
210
- *
211
- * Consumers of this observable can implement their own batching logic
212
- * in case they want to show historical stats data.
213
- */
214
- callStatsReport$: Observable<CallStatsReport | undefined>;
132
+ /** Will provide the time when this call has been ended. */
133
+ endedAt$ = this.endedAtSubject.asObservable();
215
134
 
216
- /**
217
- * The list of members in the current call.
218
- */
219
- members$: Observable<MemberResponse[]>;
135
+ /** Will provide the time when this call has been scheduled to start. */
136
+ startsAt$ = this.startsAtSubject.asObservable();
220
137
 
221
- /**
222
- * The list of capabilities of the current user.
223
- */
224
- ownCapabilities$: Observable<OwnCapability[]>;
138
+ /** Will provide the time when this call has been updated. */
139
+ updatedAt$ = this.updatedAtSubject.asObservable();
225
140
 
226
- /**
227
- * The calling state.
228
- */
229
- callingState$: Observable<CallingState>;
141
+ /** Will provide the user who created this call. */
142
+ createdBy$ = this.createdBySubject.asObservable();
230
143
 
231
- /**
232
- * The backstage state.
233
- */
234
- backstage$: Observable<boolean>;
144
+ /** Will provide the custom data of this call. */
145
+ custom$ = this.customSubject.asObservable();
235
146
 
236
- /**
237
- * Will provide the list of blocked user IDs.
238
- */
239
- blockedUserIds$: Observable<string[]>;
147
+ /** Will provide the egress data of this call. */
148
+ egress$ = this.egressSubject.asObservable();
240
149
 
241
- /**
242
- * Will provide the time when this call has been created.
243
- */
244
- createdAt$: Observable<Date>;
150
+ /** Will provide the ingress data of this call. */
151
+ ingress$ = this.ingressSubject.asObservable();
245
152
 
246
- /**
247
- * Will provide the time when this call has been ended.
248
- */
249
- endedAt$: Observable<Date | undefined>;
153
+ /** Will provide the recording state of this call. */
154
+ recording$ = duc(this.recordingSubject);
250
155
 
251
- /**
252
- * Will provide the time when this call has been scheduled to start.
253
- */
254
- startsAt$: Observable<Date | undefined>;
156
+ /** Will provide the recording state of this call. */
157
+ individualRecording$ = duc(this.individualRecordingSubject);
255
158
 
256
- /**
257
- * Will provide the time when this call has been updated.
258
- */
259
- updatedAt$: Observable<Date>;
159
+ /** Will provide the recording state of this call. */
160
+ rawRecording$ = duc(this.rawRecordingSubject);
260
161
 
261
- /**
262
- * Will provide the user who created this call.
263
- */
264
- createdBy$: Observable<UserResponse | undefined>;
162
+ /** Will provide the session data of this call. */
163
+ session$ = this.sessionSubject.asObservable();
265
164
 
266
- /**
267
- * Will provide the custom data of this call.
268
- */
269
- custom$: Observable<Record<string, any>>;
165
+ /** Will provide the settings of this call. */
166
+ settings$ = this.settingsSubject.asObservable();
270
167
 
271
- /**
272
- * Will provide the egress data of this call.
273
- */
274
- egress$: Observable<EgressResponse | undefined>;
168
+ /** Will provide the transcribing state of this call. */
169
+ transcribing$ = duc(this.transcribingSubject);
275
170
 
276
- /**
277
- * Will provide the ingress data of this call.
278
- */
279
- ingress$: Observable<CallIngressResponse | undefined>;
171
+ /** Will provide the closed captioning state of this call. */
172
+ captioning$ = duc(this.captioningSubject);
280
173
 
281
174
  /**
282
- * Will provide the recording state of this call.
175
+ * Whether end-to-end encryption is active for this call, as reported by the
176
+ * SFU in the join response.
283
177
  */
284
- recording$: Observable<boolean>;
178
+ e2eeEnabled$ = duc(this.e2eeEnabledSubject);
285
179
 
286
- /**
287
- * Will provide the recording state of this call.
288
- */
289
- individualRecording$: Observable<boolean>;
180
+ /** Will provide the user who ended this call. */
181
+ endedBy$ = this.endedBySubject.asObservable();
290
182
 
291
- /**
292
- * Will provide the recording state of this call.
293
- */
294
- rawRecording$: Observable<boolean>;
183
+ /** Will provide the thumbnails of this call. */
184
+ thumbnails$ = this.thumbnailsSubject.asObservable();
295
185
 
296
- /**
297
- * Will provide the session data of this call.
298
- */
299
- session$: Observable<CallSessionResponse | undefined>;
186
+ /** The list of members in the current call. */
187
+ members$ = this.membersSubject.asObservable();
300
188
 
301
- /**
302
- * Will provide the settings of this call.
303
- */
304
- settings$: Observable<CallSettingsResponse | undefined>;
189
+ /** The calling state. */
190
+ callingState$ = duc(this.callingStateSubject);
305
191
 
306
192
  /**
307
- * Will provide the transcribing state of this call.
193
+ * The time the call session actually started.
194
+ * Useful for displaying the call duration.
308
195
  */
309
- transcribing$: Observable<boolean>;
196
+ startedAt$ = this.startedAtSubject.asObservable();
310
197
 
311
198
  /**
312
- * Will provide the closed captioning state of this call.
199
+ * The server-side counted number of participants connected to the current call.
200
+ * This number includes the anonymous participants as well.
313
201
  */
314
- captioning$: Observable<boolean>;
202
+ participantCount$ = duc(this.participantCountSubject);
315
203
 
316
204
  /**
317
- * Whether end-to-end encryption is active for this call, as reported by the
318
- * SFU in the join response.
205
+ * The server-side counted number of anonymous participants connected to the current call.
206
+ * This number excludes the regular participants.
319
207
  */
320
- e2eeEnabled$: Observable<boolean>;
208
+ anonymousParticipantCount$ = duc(this.anonymousParticipantCountSubject);
321
209
 
322
210
  /**
323
- * Will provide the user who ended this call.
211
+ * All participants of the current call (this includes the current user and other participants as well),
212
+ * unsorted. This observable only updates when participants join or leave the call.
324
213
  */
325
- endedBy$: Observable<UserResponse | undefined>;
214
+ rawParticipants$ = shared(this.participantsSubject.asObservable());
326
215
 
327
- /**
328
- * Will provide the thumbnails of this call.
329
- */
330
- thumbnails$: Observable<ThumbnailResponse | undefined>;
216
+ /** All participants of the current call sorted according to the current `sortByParticipantsBy` setting. */
217
+ participants$ = shared(
218
+ this.participantsSubject.asObservable().pipe(
219
+ // maintain stable-sort by mutating the participants stored in the original subject
220
+ map((ps) => ps.sort(this.sortParticipantsBy)),
221
+ ),
222
+ );
331
223
 
332
- /**
333
- * The queue of closed captions.
334
- */
335
- closedCaptions$: Observable<CallClosedCaption[]>;
224
+ /** The local participant of the current call. */
225
+ localParticipant$ = shared(
226
+ this.participants$.pipe(map((ps) => ps.find((p) => p.isLocalParticipant))),
227
+ );
336
228
 
337
- readonly logger = videoLoggerSystem.getLogger('CallState');
229
+ /** Remote participants of the current call (all except the local participant). */
230
+ remoteParticipants$ = shared(
231
+ this.participants$.pipe(
232
+ map((ps) => ps.filter((p) => !p.isLocalParticipant)),
233
+ ),
234
+ );
338
235
 
339
- /**
340
- * A list of comparators that are used to sort the participants.
341
- */
342
- private sortParticipantsBy = defaultSortPreset;
236
+ /** Pinned participants of the current call. */
237
+ pinnedParticipants$ = shared(
238
+ this.participants$.pipe(map((ps) => ps.filter((p) => !!p.pin))),
239
+ );
343
240
 
344
- /**
345
- * The closed captions configuration.
346
- */
347
- private closedCaptionsSettings: ClosedCaptionsSettings | undefined;
348
- private closedCaptionsTasks = new Map<string, NodeJS.Timeout>();
241
+ /** The currently elected dominant speaker in the current call. */
242
+ dominantSpeaker$ = shared(
243
+ this.participants$.pipe(map((ps) => ps.find((p) => p.isDominantSpeaker))),
244
+ );
349
245
 
350
- private readonly eventHandlers: {
351
- [EventType in VideoEvent['type']]:
352
- ((event: Extract<VideoEvent, { type: EventType }>) => void) | undefined;
353
- };
246
+ /** Emits true whenever there is an active screen sharing session within the current call. */
247
+ hasOngoingScreenShare$ = shared(
248
+ this.participants$.pipe(
249
+ map((ps) => ps.some((p) => hasScreenShare(p))),
250
+ distinctUntilChanged(),
251
+ ),
252
+ );
354
253
 
355
254
  /**
356
- * Creates a new instance of the CallState class.
255
+ * The latest stats report of the current call.
256
+ * When stats gathering is enabled, this observable will emit a new value
257
+ * at a regular (configurable) interval.
357
258
  *
259
+ * Consumers of this observable can implement their own batching logic
260
+ * in case they want to show historical stats data.
358
261
  */
359
- constructor() {
360
- this.rawParticipants$ = this.participantsSubject
361
- .asObservable()
362
- .pipe(shareReplay({ bufferSize: 1, refCount: true }));
363
-
364
- this.participants$ = this.participantsSubject.asObservable().pipe(
365
- // maintain stable-sort by mutating the participants stored
366
- // in the original subject
367
- map((ps) => ps.sort(this.sortParticipantsBy)),
368
- shareReplay({ bufferSize: 1, refCount: true }),
369
- );
370
-
371
- this.localParticipant$ = this.participants$.pipe(
372
- map((participants) => participants.find((p) => p.isLocalParticipant)),
373
- shareReplay({ bufferSize: 1, refCount: true }),
374
- );
375
-
376
- this.remoteParticipants$ = this.participants$.pipe(
377
- map((participants) => participants.filter((p) => !p.isLocalParticipant)),
378
- shareReplay({ bufferSize: 1, refCount: true }),
379
- );
380
-
381
- this.pinnedParticipants$ = this.participants$.pipe(
382
- map((participants) => participants.filter((p) => !!p.pin)),
383
- shareReplay({ bufferSize: 1, refCount: true }),
384
- );
385
-
386
- this.dominantSpeaker$ = this.participants$.pipe(
387
- map((participants) => participants.find((p) => p.isDominantSpeaker)),
388
- shareReplay({ bufferSize: 1, refCount: true }),
389
- );
262
+ callStatsReport$ = this.callStatsReportSubject.asObservable();
390
263
 
391
- this.hasOngoingScreenShare$ = this.participants$.pipe(
392
- map((participants) => participants.some((p) => hasScreenShare(p))),
393
- distinctUntilChanged(),
394
- shareReplay({ bufferSize: 1, refCount: true }),
395
- );
264
+ /** The queue of closed captions. */
265
+ closedCaptions$ = this.closedCaptionsSubject.asObservable();
396
266
 
397
- // dates
398
- this.createdAt$ = this.createdAtSubject.asObservable();
399
- this.endedAt$ = this.endedAtSubject.asObservable();
400
- this.startsAt$ = this.startsAtSubject.asObservable();
401
- this.startedAt$ = this.startedAtSubject.asObservable();
402
- this.updatedAt$ = this.updatedAtSubject.asObservable();
403
-
404
- this.callStatsReport$ = this.callStatsReportSubject.asObservable();
405
- this.members$ = this.membersSubject.asObservable();
406
-
407
- // complex objects should work as streams of data
408
- this.createdBy$ = this.createdBySubject.asObservable();
409
- this.custom$ = this.customSubject.asObservable();
410
- this.egress$ = this.egressSubject.asObservable();
411
- this.ingress$ = this.ingressSubject.asObservable();
412
- this.session$ = this.sessionSubject.asObservable();
413
- this.settings$ = this.settingsSubject.asObservable();
414
- this.endedBy$ = this.endedBySubject.asObservable();
415
- this.thumbnails$ = this.thumbnailsSubject.asObservable();
416
- this.closedCaptions$ = this.closedCaptionsSubject.asObservable();
417
-
418
- /**
419
- * Creates an Observable from the given subject by piping to the
420
- * `distinctUntilChanged()` operator.
421
- */
422
- const duc = <T>(
423
- subject: BehaviorSubject<T>,
424
- comparator?: (a: T, b: T) => boolean,
425
- ): Observable<T> => subject.pipe(distinctUntilChanged(comparator));
426
-
427
- // primitive values should only emit once the value they hold changes
428
- this.anonymousParticipantCount$ = duc(
429
- this.anonymousParticipantCountSubject,
430
- );
431
- this.blockedUserIds$ = duc(
432
- this.blockedUserIdsSubject,
433
- RxUtils.isShallowArrayEqual,
434
- );
435
- this.backstage$ = duc(this.backstageSubject);
436
- this.callingState$ = duc(this.callingStateSubject);
437
- this.ownCapabilities$ = combineLatest([
267
+ /** The list of capabilities of the current user. */
268
+ ownCapabilities$ = shared(
269
+ combineLatest([
438
270
  this.ownCapabilitiesSubject,
439
271
  this.callGrantsSubject.pipe(startWith(undefined)),
440
272
  ]).pipe(
@@ -466,56 +298,42 @@ export class CallState {
466
298
 
467
299
  return nextCapabilities;
468
300
  }),
469
- distinctUntilChanged(RxUtils.isShallowArrayEqual),
470
- shareReplay({ bufferSize: 1, refCount: true }),
471
- );
472
- this.participantCount$ = duc(this.participantCountSubject);
473
- this.recording$ = duc(this.recordingSubject);
474
- this.individualRecording$ = duc(this.individualRecordingSubject);
475
- this.rawRecording$ = duc(this.rawRecordingSubject);
476
- this.transcribing$ = duc(this.transcribingSubject);
477
- this.captioning$ = duc(this.captioningSubject);
478
- this.e2eeEnabled$ = duc(this.e2eeEnabledSubject);
301
+ distinctUntilChanged(isShallowArrayEqual),
302
+ ),
303
+ );
304
+
305
+ // These are tracks that were delivered to the Subscriber's onTrack event
306
+ // that we couldn't associate with a participant yet.
307
+ // This happens when the participantJoined event hasn't been received yet.
308
+ // We keep these tracks around until we can associate them with a participant.
309
+ private orphanedTracks: OrphanedTrack[] = [];
310
+
311
+ /** The closed captions configuration. */
312
+ private closedCaptionsSettings: ClosedCaptionsSettings | undefined;
313
+ private closedCaptionsTasks = new Map<string, NodeJS.Timeout>();
479
314
 
315
+ private readonly eventHandlers: Partial<AllEventHandlers>;
316
+
317
+ /** Creates a new instance of the CallState class. */
318
+ constructor() {
480
319
  this.eventHandlers = {
481
- // these events are not updating the call state:
482
- 'call.frame_recording_ready': undefined,
483
- 'call.kicked_user': undefined,
484
- 'call.moderation_blur': undefined,
485
- 'call.moderation_warning': undefined,
486
- 'call.permission_request': undefined,
487
- 'call.recording_ready': undefined,
488
- 'call.rtmp_broadcast_failed': undefined,
489
- 'call.rtmp_broadcast_started': undefined,
490
- 'call.rtmp_broadcast_stopped': undefined,
491
- 'call.stats_report_ready': undefined,
492
- 'call.transcription_ready': undefined,
493
- 'call.user_feedback_submitted': undefined,
494
- 'call.user_muted': undefined,
495
- 'connection.error': undefined,
496
- 'connection.ok': undefined,
497
- 'health.check': undefined,
498
- 'user.updated': undefined,
499
- custom: undefined,
500
-
501
- // events that update call state:
502
320
  'call.accepted': (e) => this.updateFromCallResponse(e.call),
503
321
  'call.blocked_user': this.blockUser,
504
322
  'call.closed_caption': this.updateFromClosedCaptions,
505
323
  'call.closed_captions_failed': () => {
506
- this.setCurrentValue(this.captioningSubject, false);
324
+ setCurrentValue(this.captioningSubject, false);
507
325
  },
508
326
  'call.closed_captions_started': () => {
509
- this.setCurrentValue(this.captioningSubject, true);
327
+ setCurrentValue(this.captioningSubject, true);
510
328
  },
511
329
  'call.closed_captions_stopped': () => {
512
- this.setCurrentValue(this.captioningSubject, false);
330
+ setCurrentValue(this.captioningSubject, false);
513
331
  },
514
332
  'call.created': (e) => this.updateFromCallResponse(e.call),
515
333
  'call.deleted': (e) => this.updateFromCallResponse(e.call),
516
334
  'call.ended': (e) => {
517
335
  this.updateFromCallResponse(e.call);
518
- this.setCurrentValue(this.endedBySubject, e.user);
336
+ setCurrentValue(this.endedBySubject, e.user);
519
337
  },
520
338
  'call.frame_recording_failed': (e) => {
521
339
  this.updateFromCallResponse(e.call);
@@ -562,22 +380,20 @@ export class CallState {
562
380
  'call.session_participant_left': this.updateFromSessionParticipantLeft,
563
381
  'call.session_started': (e) => this.updateFromCallResponse(e.call),
564
382
  'call.transcription_started': () => {
565
- this.setCurrentValue(this.transcribingSubject, true);
383
+ setCurrentValue(this.transcribingSubject, true);
566
384
  },
567
385
  'call.transcription_stopped': () => {
568
- this.setCurrentValue(this.transcribingSubject, false);
386
+ setCurrentValue(this.transcribingSubject, false);
569
387
  },
570
388
  'call.transcription_failed': () => {
571
- this.setCurrentValue(this.transcribingSubject, false);
389
+ setCurrentValue(this.transcribingSubject, false);
572
390
  },
573
391
  'call.unblocked_user': this.unblockUser,
574
392
  'call.updated': (e) => this.updateFromCallResponse(e.call),
575
- };
393
+ } satisfies CallStateEventHandlers;
576
394
  }
577
395
 
578
- /**
579
- * Runs the cleanup tasks.
580
- */
396
+ /** Runs the cleanup tasks. */
581
397
  dispose = () => {
582
398
  for (const [ccKey, taskId] of this.closedCaptionsTasks.entries()) {
583
399
  clearTimeout(taskId);
@@ -595,43 +411,20 @@ export class CallState {
595
411
  setSortParticipantsBy = (comparator: Comparator<StreamVideoParticipant>) => {
596
412
  this.sortParticipantsBy = comparator;
597
413
  // trigger re-sorting of participants
598
- this.setCurrentValue(this.participantsSubject, (ps) => ps);
414
+ setCurrentValue(this.participantsSubject, (ps) => ps);
599
415
  };
600
416
 
601
- /**
602
- * Returns the comparator currently used to sort the participants.
603
- */
417
+ /** Returns the comparator currently used to sort the participants. */
604
418
  getSortParticipantsBy = (): Comparator<StreamVideoParticipant> => {
605
419
  return this.sortParticipantsBy;
606
420
  };
607
421
 
608
- /**
609
- * Gets the current value of an observable, or undefined if the observable has
610
- * not emitted a value yet.
611
- *
612
- * @param observable$ the observable to get the value from.
613
- */
614
- getCurrentValue = RxUtils.getCurrentValue;
615
-
616
- /**
617
- * Updates the value of the provided Subject.
618
- * An `update` can either be a new value or a function which takes
619
- * the current value and returns a new value.
620
- *
621
- * @internal
622
- *
623
- * @param subject the subject to update.
624
- * @param update the update to apply to the subject.
625
- * @return the updated value.
626
- */
627
- setCurrentValue = RxUtils.setCurrentValue;
628
-
629
422
  /**
630
423
  * The server-side counted number of participants connected to the current call.
631
424
  * This number includes the anonymous participants as well.
632
425
  */
633
426
  get participantCount() {
634
- return this.getCurrentValue(this.participantCount$);
427
+ return getCurrentValue(this.participantCountSubject);
635
428
  }
636
429
 
637
430
  /**
@@ -641,7 +434,7 @@ export class CallState {
641
434
  * @param count the number of participants.
642
435
  */
643
436
  setParticipantCount = (count: Patch<number>) => {
644
- return this.setCurrentValue(this.participantCountSubject, count);
437
+ return setCurrentValue(this.participantCountSubject, count);
645
438
  };
646
439
 
647
440
  /**
@@ -649,7 +442,7 @@ export class CallState {
649
442
  * Useful for displaying the call duration.
650
443
  */
651
444
  get startedAt() {
652
- return this.getCurrentValue(this.startedAt$);
445
+ return getCurrentValue(this.startedAtSubject);
653
446
  }
654
447
 
655
448
  /**
@@ -659,14 +452,12 @@ export class CallState {
659
452
  * @param startedAt the time the call session actually started.
660
453
  */
661
454
  setStartedAt = (startedAt: Patch<Date | undefined>) => {
662
- return this.setCurrentValue(this.startedAtSubject, startedAt);
455
+ return setCurrentValue(this.startedAtSubject, startedAt);
663
456
  };
664
457
 
665
- /**
666
- * Returns whether closed captions are enabled in the current call.
667
- */
458
+ /** Returns whether closed captions are enabled in the current call. */
668
459
  get captioning() {
669
- return this.getCurrentValue(this.captioning$);
460
+ return getCurrentValue(this.captioningSubject);
670
461
  }
671
462
 
672
463
  /**
@@ -676,7 +467,7 @@ export class CallState {
676
467
  * @param captioning the closed captioning state.
677
468
  */
678
469
  setCaptioning = (captioning: boolean) => {
679
- return RxUtils.updateValue(this.captioningSubject, captioning);
470
+ return updateValue(this.captioningSubject, captioning);
680
471
  };
681
472
 
682
473
  /**
@@ -684,7 +475,7 @@ export class CallState {
684
475
  * This number includes the anonymous participants as well.
685
476
  */
686
477
  get anonymousParticipantCount() {
687
- return this.getCurrentValue(this.anonymousParticipantCount$);
478
+ return getCurrentValue(this.anonymousParticipantCountSubject);
688
479
  }
689
480
 
690
481
  /**
@@ -694,21 +485,17 @@ export class CallState {
694
485
  * @param count the number of anonymous participants.
695
486
  */
696
487
  setAnonymousParticipantCount = (count: Patch<number>) => {
697
- return this.setCurrentValue(this.anonymousParticipantCountSubject, count);
488
+ return setCurrentValue(this.anonymousParticipantCountSubject, count);
698
489
  };
699
490
 
700
- /**
701
- * The list of participants in the current call.
702
- */
491
+ /** The list of participants in the current call. */
703
492
  get participants() {
704
- return this.getCurrentValue(this.participants$);
493
+ return getCurrentValue(this.participants$);
705
494
  }
706
495
 
707
- /**
708
- * The stable list of participants in the current call, unsorted.
709
- */
496
+ /** The stable list of participants in the current call, unsorted. */
710
497
  get rawParticipants() {
711
- return this.getCurrentValue(this.rawParticipants$);
498
+ return getCurrentValue(this.rawParticipants$);
712
499
  }
713
500
 
714
501
  /**
@@ -731,49 +518,37 @@ export class CallState {
731
518
  * @param participants the list of participants.
732
519
  */
733
520
  setParticipants = (participants: Patch<StreamVideoParticipant[]>) => {
734
- return this.setCurrentValue(this.participantsSubject, participants);
521
+ return setCurrentValue(this.participantsSubject, participants);
735
522
  };
736
523
 
737
- /**
738
- * The local participant in the current call.
739
- */
524
+ /** The local participant in the current call. */
740
525
  get localParticipant() {
741
- return this.getCurrentValue(this.localParticipant$);
526
+ return getCurrentValue(this.localParticipant$);
742
527
  }
743
528
 
744
- /**
745
- * The list of remote participants in the current call.
746
- */
529
+ /** The list of remote participants in the current call. */
747
530
  get remoteParticipants() {
748
- return this.getCurrentValue(this.remoteParticipants$);
531
+ return getCurrentValue(this.remoteParticipants$);
749
532
  }
750
533
 
751
- /**
752
- * The dominant speaker in the current call.
753
- */
534
+ /** The dominant speaker in the current call. */
754
535
  get dominantSpeaker() {
755
- return this.getCurrentValue(this.dominantSpeaker$);
536
+ return getCurrentValue(this.dominantSpeaker$);
756
537
  }
757
538
 
758
- /**
759
- * The list of pinned participants in the current call.
760
- */
539
+ /** The list of pinned participants in the current call. */
761
540
  get pinnedParticipants() {
762
- return this.getCurrentValue(this.pinnedParticipants$);
541
+ return getCurrentValue(this.pinnedParticipants$);
763
542
  }
764
543
 
765
- /**
766
- * Tell if there is an ongoing screen share in this call.
767
- */
544
+ /** Tell if there is an ongoing screen share in this call. */
768
545
  get hasOngoingScreenShare() {
769
- return this.getCurrentValue(this.hasOngoingScreenShare$);
546
+ return getCurrentValue(this.hasOngoingScreenShare$);
770
547
  }
771
548
 
772
- /**
773
- * The calling state.
774
- */
549
+ /** The calling state. */
775
550
  get callingState() {
776
- return this.getCurrentValue(this.callingState$);
551
+ return getCurrentValue(this.callingStateSubject);
777
552
  }
778
553
 
779
554
  /**
@@ -783,14 +558,12 @@ export class CallState {
783
558
  * @param state the new calling state.
784
559
  */
785
560
  setCallingState = (state: Patch<CallingState>) => {
786
- return this.setCurrentValue(this.callingStateSubject, state);
561
+ return setCurrentValue(this.callingStateSubject, state);
787
562
  };
788
563
 
789
- /**
790
- * The call stats report.
791
- */
564
+ /** The call stats report. */
792
565
  get callStatsReport() {
793
- return this.getCurrentValue(this.callStatsReport$);
566
+ return getCurrentValue(this.callStatsReportSubject);
794
567
  }
795
568
 
796
569
  /**
@@ -808,14 +581,12 @@ export class CallState {
808
581
  * @param report the report to set.
809
582
  */
810
583
  setCallStatsReport = (report: Patch<CallStatsReport | undefined>) => {
811
- return this.setCurrentValue(this.callStatsReportSubject, report);
584
+ return setCurrentValue(this.callStatsReportSubject, report);
812
585
  };
813
586
 
814
- /**
815
- * The members of the current call.
816
- */
587
+ /** The members of the current call. */
817
588
  get members() {
818
- return this.getCurrentValue(this.members$);
589
+ return getCurrentValue(this.membersSubject);
819
590
  }
820
591
 
821
592
  /**
@@ -825,14 +596,12 @@ export class CallState {
825
596
  * @param members the members to set.
826
597
  */
827
598
  setMembers = (members: Patch<MemberResponse[]>) => {
828
- this.setCurrentValue(this.membersSubject, members);
599
+ setCurrentValue(this.membersSubject, members);
829
600
  };
830
601
 
831
- /**
832
- * The capabilities of the current user for the current call.
833
- */
602
+ /** The capabilities of the current user for the current call. */
834
603
  get ownCapabilities() {
835
- return this.getCurrentValue(this.ownCapabilities$);
604
+ return getCurrentValue(this.ownCapabilities$);
836
605
  }
837
606
 
838
607
  /**
@@ -842,7 +611,7 @@ export class CallState {
842
611
  * @param capabilities the capabilities to set.
843
612
  */
844
613
  setOwnCapabilities = (capabilities: Patch<OwnCapability[]>) => {
845
- return this.setCurrentValue(this.ownCapabilitiesSubject, capabilities);
614
+ return setCurrentValue(this.ownCapabilitiesSubject, capabilities);
846
615
  };
847
616
 
848
617
  /**
@@ -852,14 +621,12 @@ export class CallState {
852
621
  * @param grants the grants to set.
853
622
  */
854
623
  setCallGrants = (grants: Patch<CallGrants>) => {
855
- return this.setCurrentValue(this.callGrantsSubject, grants);
624
+ return setCurrentValue(this.callGrantsSubject, grants);
856
625
  };
857
626
 
858
- /**
859
- * The backstage state.
860
- */
627
+ /** The backstage state. */
861
628
  get backstage() {
862
- return this.getCurrentValue(this.backstage$);
629
+ return getCurrentValue(this.backstageSubject);
863
630
  }
864
631
 
865
632
  /**
@@ -867,28 +634,22 @@ export class CallState {
867
634
  * @param backstage the backstage state.
868
635
  */
869
636
  setBackstage = (backstage: Patch<boolean>) => {
870
- return this.setCurrentValue(this.backstageSubject, backstage);
637
+ return setCurrentValue(this.backstageSubject, backstage);
871
638
  };
872
639
 
873
- /**
874
- * Will provide the list of blocked user IDs.
875
- */
640
+ /** Will provide the list of blocked user IDs. */
876
641
  get blockedUserIds() {
877
- return this.getCurrentValue(this.blockedUserIds$);
642
+ return getCurrentValue(this.blockedUserIdsSubject);
878
643
  }
879
644
 
880
- /**
881
- * Will provide the time when this call has been created.
882
- */
645
+ /** Will provide the time when this call has been created. */
883
646
  get createdAt() {
884
- return this.getCurrentValue(this.createdAt$);
647
+ return getCurrentValue(this.createdAtSubject);
885
648
  }
886
649
 
887
- /**
888
- * Will provide the time when this call has been ended.
889
- */
650
+ /** Will provide the time when this call has been ended. */
890
651
  get endedAt() {
891
- return this.getCurrentValue(this.endedAt$);
652
+ return getCurrentValue(this.endedAtSubject);
892
653
  }
893
654
 
894
655
  /**
@@ -896,119 +657,91 @@ export class CallState {
896
657
  * @param endedAt the time when this call has been ended.
897
658
  */
898
659
  setEndedAt = (endedAt: Patch<Date | undefined>) => {
899
- return this.setCurrentValue(this.endedAtSubject, endedAt);
660
+ return setCurrentValue(this.endedAtSubject, endedAt);
900
661
  };
901
662
 
902
- /**
903
- * Will provide the time when this call has been scheduled to start.
904
- */
663
+ /** Will provide the time when this call has been scheduled to start. */
905
664
  get startsAt() {
906
- return this.getCurrentValue(this.startsAt$);
665
+ return getCurrentValue(this.startsAtSubject);
907
666
  }
908
667
 
909
- /**
910
- * Will provide the time when this call has been updated.
911
- */
668
+ /** Will provide the time when this call has been updated. */
912
669
  get updatedAt() {
913
- return this.getCurrentValue(this.updatedAt$);
670
+ return getCurrentValue(this.updatedAtSubject);
914
671
  }
915
672
 
916
- /**
917
- * Will provide the user who created this call.
918
- */
673
+ /** Will provide the user who created this call. */
919
674
  get createdBy() {
920
- return this.getCurrentValue(this.createdBy$);
675
+ return getCurrentValue(this.createdBySubject);
921
676
  }
922
677
 
923
- /**
924
- * Will provide the custom data of this call.
925
- */
678
+ /** Will provide the custom data of this call. */
926
679
  get custom() {
927
- return this.getCurrentValue(this.custom$);
680
+ return getCurrentValue(this.customSubject);
928
681
  }
929
682
 
930
683
  /**
931
684
  * Will provide the egress data of this call.
932
685
  */
933
686
  get egress() {
934
- return this.getCurrentValue(this.egress$);
687
+ return getCurrentValue(this.egressSubject);
935
688
  }
936
689
 
937
- /**
938
- * Will provide the ingress data of this call.
939
- */
690
+ /** Will provide the ingress data of this call. */
940
691
  get ingress() {
941
- return this.getCurrentValue(this.ingress$);
692
+ return getCurrentValue(this.ingressSubject);
942
693
  }
943
694
 
944
- /**
945
- * Will provide the composite recording state of this call.
946
- */
695
+ /** Will provide the composite recording state of this call. */
947
696
  get recording() {
948
- return this.getCurrentValue(this.recording$);
697
+ return getCurrentValue(this.recordingSubject);
949
698
  }
950
699
 
951
- /**
952
- * Will provide the individual recording state of this call.
953
- */
700
+ /** Will provide the individual recording state of this call. */
954
701
  get individualRecording() {
955
- return this.getCurrentValue(this.individualRecording$);
702
+ return getCurrentValue(this.individualRecordingSubject);
956
703
  }
957
704
 
958
- /**
959
- * Will provide the raw recording state of this call.
960
- */
705
+ /** Will provide the raw recording state of this call. */
961
706
  get rawRecording() {
962
- return this.getCurrentValue(this.rawRecording$);
707
+ return getCurrentValue(this.rawRecordingSubject);
963
708
  }
964
709
 
965
- /**
966
- * Will provide the session data of this call.
967
- */
710
+ /** Will provide the session data of this call. */
968
711
  get session() {
969
- return this.getCurrentValue(this.session$);
712
+ return getCurrentValue(this.sessionSubject);
970
713
  }
971
714
 
972
- /**
973
- * Will provide the settings of this call.
974
- */
715
+ /** Will provide the settings of this call. */
975
716
  get settings() {
976
- return this.getCurrentValue(this.settings$);
717
+ return getCurrentValue(this.settingsSubject);
977
718
  }
978
719
 
979
- /**
980
- * Will provide the transcribing state of this call.
981
- */
720
+ /** Will provide the transcribing state of this call. */
982
721
  get transcribing() {
983
- return this.getCurrentValue(this.transcribing$);
722
+ return getCurrentValue(this.transcribingSubject);
984
723
  }
985
724
 
986
- /**
987
- * Whether end-to-end encryption is active for this call.
988
- */
725
+ /** Whether end-to-end encryption is active for this call. */
989
726
  get e2eeEnabled() {
990
- return this.getCurrentValue(this.e2eeEnabled$);
727
+ return getCurrentValue(this.e2eeEnabledSubject);
991
728
  }
992
729
 
993
- /**
994
- * Will provide the user who ended this call.
995
- */
730
+ /** Will provide the user who ended this call. */
996
731
  get endedBy() {
997
- return this.getCurrentValue(this.endedBy$);
732
+ return getCurrentValue(this.endedBySubject);
998
733
  }
999
734
 
1000
735
  /**
1001
736
  * Will provide the thumbnails of this call, if enabled in the call settings.
1002
737
  */
1003
738
  get thumbnails() {
1004
- return this.getCurrentValue(this.thumbnails$);
739
+ return getCurrentValue(this.thumbnailsSubject);
1005
740
  }
1006
741
 
1007
- /**
1008
- * Returns the current queue of closed captions.
1009
- */
742
+ /** Returns the current queue of closed captions. */
1010
743
  get closedCaptions() {
1011
- return this.getCurrentValue(this.closedCaptions$);
744
+ return getCurrentValue(this.closedCaptionsSubject);
1012
745
  }
1013
746
 
1014
747
  /**
@@ -1023,9 +756,7 @@ export class CallState {
1023
756
  return this.participants.find((p) => p.sessionId === sessionId);
1024
757
  };
1025
758
 
1026
- /**
1027
- * Returns a new lookup table of participants indexed by their session ID.
1028
- */
759
+ /** Returns a new lookup table of participants indexed by their session ID. */
1029
760
  getParticipantLookupBySessionId = () => {
1030
761
  return this.participants.reduce<{
1031
762
  [sessionId: string]: StreamVideoParticipant | undefined;
@@ -1317,39 +1048,63 @@ export class CallState {
1317
1048
  */
1318
1049
  updateFromCallResponse = (call: CallResponse) => {
1319
1050
  this.setBackstage(call.backstage);
1320
- this.setCurrentValue(this.blockedUserIdsSubject, call.blocked_user_ids);
1321
- this.setCurrentValue(this.createdAtSubject, new Date(call.created_at));
1322
- this.setCurrentValue(this.updatedAtSubject, new Date(call.updated_at));
1323
- this.setCurrentValue(
1051
+ setCurrentValue(this.blockedUserIdsSubject, call.blocked_user_ids);
1052
+ setCurrentValue(this.createdAtSubject, new Date(call.created_at));
1053
+ setCurrentValue(this.updatedAtSubject, new Date(call.updated_at));
1054
+ setCurrentValue(
1324
1055
  this.startsAtSubject,
1325
1056
  call.starts_at ? new Date(call.starts_at) : undefined,
1326
1057
  );
1327
1058
  this.setEndedAt(call.ended_at ? new Date(call.ended_at) : undefined);
1328
- this.setCurrentValue(this.createdBySubject, call.created_by);
1329
- this.setCurrentValue(this.customSubject, call.custom);
1330
- this.setCurrentValue(this.egressSubject, call.egress);
1331
- this.setCurrentValue(this.ingressSubject, call.ingress);
1059
+ setCurrentValue(this.createdBySubject, call.created_by);
1060
+ setCurrentValue(this.customSubject, call.custom);
1061
+ setCurrentValue(this.egressSubject, call.egress);
1062
+ setCurrentValue(this.ingressSubject, call.ingress);
1332
1063
  const { individual_recording, composite_recording, raw_recording } =
1333
1064
  call.egress;
1334
- this.setCurrentValue(
1065
+ setCurrentValue(
1335
1066
  this.recordingSubject,
1336
1067
  call.recording || composite_recording?.status === 'running',
1337
1068
  );
1338
- this.setCurrentValue(
1069
+ setCurrentValue(
1339
1070
  this.individualRecordingSubject,
1340
1071
  individual_recording?.status === 'running',
1341
1072
  );
1342
- this.setCurrentValue(
1073
+ setCurrentValue(
1343
1074
  this.rawRecordingSubject,
1344
1075
  raw_recording?.status === 'running',
1345
1076
  );
1346
1077
 
1347
- const s = this.setCurrentValue(this.sessionSubject, call.session);
1078
+ const s = setCurrentValue(this.sessionSubject, call.session);
1348
1079
  this.updateParticipantCountFromSession(s);
1349
- this.setCurrentValue(this.settingsSubject, call.settings);
1350
- this.setCurrentValue(this.transcribingSubject, call.transcribing);
1351
- this.setCurrentValue(this.captioningSubject, call.captioning);
1352
- this.setCurrentValue(this.thumbnailsSubject, call.thumbnails);
1080
+ setCurrentValue(this.settingsSubject, call.settings);
1081
+ setCurrentValue(this.transcribingSubject, call.transcribing);
1082
+ setCurrentValue(this.captioningSubject, call.captioning);
1083
+ setCurrentValue(this.thumbnailsSubject, call.thumbnails);
1084
+ };
1085
+
1086
+ /**
1087
+ * Merges a polled ring state into the current session. Only the ring fields
1088
+ * are touched: the endpoint does not return the session roster.
1089
+ *
1090
+ * @internal
1091
+ *
1092
+ * @param ringState the ring state as returned by the coordinator.
1093
+ */
1094
+ updateFromRingState = (ringState: GetCallRingStateResponse) => {
1095
+ setCurrentValue(this.sessionSubject, (session) => {
1096
+ if (!session || session.id !== ringState.session_id) return session;
1097
+ return {
1098
+ ...session,
1099
+ accepted_by: ringState.accepted_by,
1100
+ rejected_by: ringState.rejected_by,
1101
+ missed_by: ringState.missed_by,
1102
+ ended_at: ringState.session_ended_at ?? session.ended_at,
1103
+ };
1104
+ });
1105
+ if (ringState.call_ended_at) {
1106
+ this.setEndedAt(new Date(ringState.call_ended_at));
1107
+ }
1353
1108
  };
1354
1109
 
1355
1110
  /**
@@ -1396,26 +1151,26 @@ export class CallState {
1396
1151
  this.setAnonymousParticipantCount(participantCount?.anonymous || 0);
1397
1152
  this.setStartedAt(startedAt ? Timestamp.toDate(startedAt) : new Date());
1398
1153
  this.setServerSidePins(pins);
1399
- this.setCurrentValue(this.e2eeEnabledSubject, e2EeEnabled);
1154
+ setCurrentValue(this.e2eeEnabledSubject, e2EeEnabled);
1400
1155
  };
1401
1156
 
1402
1157
  private updateFromMemberRemoved = (event: CallMemberRemovedEvent) => {
1403
1158
  this.updateFromCallResponse(event.call);
1404
- this.setCurrentValue(this.membersSubject, (members) =>
1159
+ setCurrentValue(this.membersSubject, (members) =>
1405
1160
  members.filter((m) => event.members.indexOf(m.user_id) === -1),
1406
1161
  );
1407
1162
  };
1408
1163
 
1409
1164
  private updateFromMemberAdded = (event: CallMemberAddedEvent) => {
1410
1165
  this.updateFromCallResponse(event.call);
1411
- this.setCurrentValue(this.membersSubject, (members) => [
1166
+ setCurrentValue(this.membersSubject, (members) => [
1412
1167
  ...members,
1413
1168
  ...event.members,
1414
1169
  ]);
1415
1170
  };
1416
1171
 
1417
1172
  private updateFromHLSBroadcastStopped = () => {
1418
- this.setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
1173
+ setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
1419
1174
  ...egress,
1420
1175
  broadcasting: false,
1421
1176
  hls: {
@@ -1426,7 +1181,7 @@ export class CallState {
1426
1181
  };
1427
1182
 
1428
1183
  private updateFromHLSBroadcastingFailed = () => {
1429
- this.setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
1184
+ setCurrentValue(this.egressSubject, (egress = defaultEgress) => ({
1430
1185
  ...egress,
1431
1186
  broadcasting: false,
1432
1187
  hls: {
@@ -1442,11 +1197,11 @@ export class CallState {
1442
1197
  ) => {
1443
1198
  // handle the legacy format, where `type` is absent in the emitted events
1444
1199
  if (type === undefined || type === 'composite') {
1445
- this.setCurrentValue(this.recordingSubject, running);
1200
+ setCurrentValue(this.recordingSubject, running);
1446
1201
  } else if (type === 'individual') {
1447
- this.setCurrentValue(this.individualRecordingSubject, running);
1202
+ setCurrentValue(this.individualRecordingSubject, running);
1448
1203
  } else if (type === 'raw') {
1449
- this.setCurrentValue(this.rawRecordingSubject, running);
1204
+ setCurrentValue(this.rawRecordingSubject, running);
1450
1205
  } else {
1451
1206
  ensureExhausted(type, 'Unknown recording type');
1452
1207
  }
@@ -1469,7 +1224,7 @@ export class CallState {
1469
1224
  private updateFromSessionParticipantCountUpdate = (
1470
1225
  event: CallSessionParticipantCountsUpdatedEvent,
1471
1226
  ) => {
1472
- const s = this.setCurrentValue(this.sessionSubject, (session) => {
1227
+ const s = setCurrentValue(this.sessionSubject, (session) => {
1473
1228
  if (!session) return session;
1474
1229
  return {
1475
1230
  ...session,
@@ -1483,7 +1238,7 @@ export class CallState {
1483
1238
  private updateFromSessionParticipantLeft = (
1484
1239
  event: CallSessionParticipantLeftEvent,
1485
1240
  ) => {
1486
- const s = this.setCurrentValue(this.sessionSubject, (session) => {
1241
+ const s = setCurrentValue(this.sessionSubject, (session) => {
1487
1242
  if (!session) return session;
1488
1243
  const { participants, participants_count_by_role } = session;
1489
1244
  const { user, user_session_id } = event.participant;
@@ -1507,7 +1262,7 @@ export class CallState {
1507
1262
  private updateFromSessionParticipantJoined = (
1508
1263
  event: CallSessionParticipantJoinedEvent,
1509
1264
  ) => {
1510
- const s = this.setCurrentValue(this.sessionSubject, (session) => {
1265
+ const s = setCurrentValue(this.sessionSubject, (session) => {
1511
1266
  if (!session) return session;
1512
1267
  const { participants, participants_count_by_role } = session;
1513
1268
  const { user, user_session_id } = event.participant;
@@ -1547,7 +1302,7 @@ export class CallState {
1547
1302
  event: CallMemberUpdatedEvent | CallMemberUpdatedPermissionEvent,
1548
1303
  ) => {
1549
1304
  this.updateFromCallResponse(event.call);
1550
- this.setCurrentValue(this.membersSubject, (members) =>
1305
+ setCurrentValue(this.membersSubject, (members) =>
1551
1306
  members.map((member) => {
1552
1307
  const memberUpdate = event.members.find(
1553
1308
  (m) => m.user_id === member.user_id,
@@ -1577,14 +1332,14 @@ export class CallState {
1577
1332
  };
1578
1333
 
1579
1334
  private unblockUser = (event: UnblockedUserEvent) => {
1580
- this.setCurrentValue(this.blockedUserIdsSubject, (current) => {
1335
+ setCurrentValue(this.blockedUserIdsSubject, (current) => {
1581
1336
  if (!current) return current;
1582
1337
  return current.filter((id) => id !== event.user.id);
1583
1338
  });
1584
1339
  };
1585
1340
 
1586
1341
  private blockUser = (event: BlockedUserEvent) => {
1587
- this.setCurrentValue(this.blockedUserIdsSubject, (current) => [
1342
+ setCurrentValue(this.blockedUserIdsSubject, (current) => [
1588
1343
  ...(current || []),
1589
1344
  event.user.id,
1590
1345
  ]);
@@ -1597,7 +1352,7 @@ export class CallState {
1597
1352
  };
1598
1353
 
1599
1354
  private updateFromClosedCaptions = (event: ClosedCaptionEvent) => {
1600
- this.setCurrentValue(this.closedCaptionsSubject, (queue) => {
1355
+ setCurrentValue(this.closedCaptionsSubject, (queue) => {
1601
1356
  const { closed_caption } = event;
1602
1357
 
1603
1358
  const keyOf = (c: CallClosedCaption) => `${c.speaker_id}/${c.start_time}`;
@@ -1613,7 +1368,7 @@ export class CallState {
1613
1368
  // schedule the removal of the closed caption after the retention time
1614
1369
  if (visibilityDurationMs > 0) {
1615
1370
  const taskId = setTimeout(() => {
1616
- this.setCurrentValue(this.closedCaptionsSubject, (captions) =>
1371
+ setCurrentValue(this.closedCaptionsSubject, (captions) =>
1617
1372
  captions.filter((caption) => caption !== closed_caption),
1618
1373
  );
1619
1374
  this.closedCaptionsTasks.delete(currentKey);