@stream-io/video-client 1.26.1 → 1.27.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 (55) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/index.browser.es.js +276 -73
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +275 -71
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +276 -73
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +14 -2
  9. package/dist/src/StreamSfuClient.d.ts +7 -3
  10. package/dist/src/devices/devices.d.ts +5 -5
  11. package/dist/src/events/internal.d.ts +7 -1
  12. package/dist/src/gen/video/sfu/event/events.d.ts +57 -1
  13. package/dist/src/gen/video/sfu/models/models.d.ts +21 -0
  14. package/dist/src/helpers/array.d.ts +7 -0
  15. package/dist/src/helpers/lazy.d.ts +1 -1
  16. package/dist/src/helpers/participantUtils.d.ts +8 -1
  17. package/dist/src/rtc/BasePeerConnection.d.ts +2 -2
  18. package/dist/src/rtc/Dispatcher.d.ts +1 -1
  19. package/dist/src/rtc/signal.d.ts +1 -1
  20. package/dist/src/stats/rtc/Tracer.d.ts +4 -1
  21. package/dist/src/stats/rtc/types.d.ts +1 -0
  22. package/dist/src/store/CallState.d.ts +2 -1
  23. package/dist/src/timers/index.d.ts +1 -1
  24. package/dist/src/types.d.ts +10 -1
  25. package/package.json +2 -2
  26. package/src/Call.ts +55 -9
  27. package/src/StreamSfuClient.ts +33 -14
  28. package/src/coordinator/connection/connection.ts +0 -2
  29. package/src/devices/CameraManager.ts +1 -1
  30. package/src/devices/InputMediaDeviceManager.ts +5 -3
  31. package/src/devices/MicrophoneManager.ts +2 -1
  32. package/src/devices/SpeakerManager.ts +1 -1
  33. package/src/devices/devices.ts +29 -11
  34. package/src/events/__tests__/internal.test.ts +78 -0
  35. package/src/events/__tests__/participant.test.ts +66 -0
  36. package/src/events/callEventHandlers.ts +2 -0
  37. package/src/events/internal.ts +28 -1
  38. package/src/events/participant.ts +4 -1
  39. package/src/gen/video/sfu/event/events.ts +104 -0
  40. package/src/gen/video/sfu/models/models.ts +21 -0
  41. package/src/helpers/__tests__/participantUtils.test.ts +167 -0
  42. package/src/helpers/array.ts +16 -0
  43. package/src/helpers/lazy.ts +3 -3
  44. package/src/helpers/participantUtils.ts +23 -1
  45. package/src/rtc/BasePeerConnection.ts +6 -5
  46. package/src/rtc/Dispatcher.ts +3 -2
  47. package/src/rtc/__tests__/Publisher.test.ts +3 -2
  48. package/src/rtc/__tests__/Subscriber.test.ts +3 -2
  49. package/src/rtc/__tests__/videoLayers.test.ts +4 -6
  50. package/src/rtc/signal.ts +3 -3
  51. package/src/rtc/videoLayers.ts +12 -6
  52. package/src/stats/rtc/Tracer.ts +19 -1
  53. package/src/stats/rtc/types.ts +1 -0
  54. package/src/store/CallState.ts +7 -4
  55. package/src/types.ts +11 -0
@@ -51,6 +51,11 @@ export type StreamSfuClientConstructor = {
51
51
  */
52
52
  credentials: Credentials;
53
53
 
54
+ /**
55
+ * The `cid` (call ID) to use for the connection.
56
+ */
57
+ cid: string;
58
+
54
59
  /**
55
60
  * `sessionId` to use for the connection.
56
61
  */
@@ -59,7 +64,7 @@ export type StreamSfuClientConstructor = {
59
64
  /**
60
65
  * A log tag to use for logging. Useful for debugging multiple instances.
61
66
  */
62
- logTag: string;
67
+ tag: string;
63
68
 
64
69
  /**
65
70
  * The timeout in milliseconds for waiting for the `joinResponse`.
@@ -83,6 +88,14 @@ export type StreamSfuClientConstructor = {
83
88
  enableTracing: boolean;
84
89
  };
85
90
 
91
+ type SfuWebSocketParams = {
92
+ attempt: string; // the reconnect attempt, start with 0
93
+ user_id: string;
94
+ api_key: string;
95
+ user_session_id: string;
96
+ cid: string;
97
+ };
98
+
86
99
  /**
87
100
  * The client used for exchanging information with the SFU.
88
101
  */
@@ -140,7 +153,7 @@ export class StreamSfuClient {
140
153
  private readonly unsubscribeNetworkChanged: () => void;
141
154
  private readonly onSignalClose: ((reason: string) => void) | undefined;
142
155
  private readonly logger: Logger;
143
- private readonly logTag: string;
156
+ readonly tag: string;
144
157
  private readonly credentials: Credentials;
145
158
  private readonly dispatcher: Dispatcher;
146
159
  private readonly joinResponseTimeout: number;
@@ -191,7 +204,8 @@ export class StreamSfuClient {
191
204
  dispatcher,
192
205
  credentials,
193
206
  sessionId,
194
- logTag,
207
+ cid,
208
+ tag,
195
209
  joinResponseTimeout = 5000,
196
210
  onSignalClose,
197
211
  streamClient,
@@ -204,10 +218,10 @@ export class StreamSfuClient {
204
218
  const { server, token } = credentials;
205
219
  this.edgeName = server.edge_name;
206
220
  this.joinResponseTimeout = joinResponseTimeout;
207
- this.logTag = logTag;
208
- this.logger = getLogger(['SfuClient', logTag]);
221
+ this.tag = tag;
222
+ this.logger = getLogger(['SfuClient', tag]);
209
223
  this.tracer = enableTracing
210
- ? new Tracer(`${logTag}-${this.edgeName}`)
224
+ ? new Tracer(`${tag}-${this.edgeName}`)
211
225
  : undefined;
212
226
  this.rpc = createSignalClient({
213
227
  baseUrl: server.url,
@@ -238,10 +252,16 @@ export class StreamSfuClient {
238
252
  }
239
253
  });
240
254
 
241
- this.createWebSocket();
255
+ this.createWebSocket({
256
+ attempt: tag,
257
+ user_id: streamClient.user?.id || '',
258
+ api_key: streamClient.key,
259
+ user_session_id: this.sessionId,
260
+ cid,
261
+ });
242
262
  }
243
263
 
244
- private createWebSocket = () => {
264
+ private createWebSocket = (params: SfuWebSocketParams) => {
245
265
  const eventsToTrace: Partial<Record<SfuEventKinds, boolean>> = {
246
266
  callEnded: true,
247
267
  changePublishQuality: true,
@@ -249,10 +269,11 @@ export class StreamSfuClient {
249
269
  connectionQualityChanged: true,
250
270
  error: true,
251
271
  goAway: true,
272
+ inboundStateNotification: true,
252
273
  };
253
274
  this.signalWs = createWebSocketSignalChannel({
254
- logTag: this.logTag,
255
- endpoint: `${this.credentials.server.ws_endpoint}?tag=${this.logTag}`,
275
+ tag: this.tag,
276
+ endpoint: `${this.credentials.server.ws_endpoint}?${new URLSearchParams(params).toString()}`,
256
277
  onMessage: (message) => {
257
278
  this.lastMessageTimestamp = new Date();
258
279
  this.scheduleConnectionCheck();
@@ -260,7 +281,7 @@ export class StreamSfuClient {
260
281
  if (eventsToTrace[eventKind]) {
261
282
  this.tracer?.trace(eventKind, message);
262
283
  }
263
- this.dispatcher.dispatch(message, this.logTag);
284
+ this.dispatcher.dispatch(message, this.tag);
264
285
  },
265
286
  });
266
287
 
@@ -443,9 +464,7 @@ export class StreamSfuClient {
443
464
  this.migrateAwayTimeout = setTimeout(() => {
444
465
  unsubscribe();
445
466
  task.reject(
446
- new Error(
447
- `Migration (${this.logTag}) failed to complete in ${timeout}ms`,
448
- ),
467
+ new Error(`Migration (${this.tag}) failed to complete in ${timeout}ms`),
449
468
  );
450
469
  }, timeout);
451
470
 
@@ -471,7 +471,6 @@ export class StableWSConnection {
471
471
  onmessage = (wsID: number, event: MessageEvent) => {
472
472
  if (this.wsID !== wsID) return;
473
473
 
474
- this._log('onmessage() - onmessage callback', { event, wsID });
475
474
  const data =
476
475
  typeof event.data === 'string'
477
476
  ? (JSON.parse(event.data) as StreamVideoEvent)
@@ -581,7 +580,6 @@ export class StableWSConnection {
581
580
  this.totalFailures += 1;
582
581
  this._setHealth(false);
583
582
  this.isConnecting = false;
584
- this.rejectConnectionOpen?.(new Error(`WebSocket error: ${event}`));
585
583
  this._log(`onerror() - WS connection resulted into error`, { event });
586
584
 
587
585
  this._reconnect();
@@ -149,7 +149,7 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
149
149
  }
150
150
 
151
151
  protected getDevices(): Observable<MediaDeviceInfo[]> {
152
- return getVideoDevices();
152
+ return getVideoDevices(this.call.tracer);
153
153
  }
154
154
 
155
155
  protected getStream(
@@ -198,6 +198,10 @@ export abstract class InputMediaDeviceManager<
198
198
  entry.stop?.();
199
199
  this.filters = this.filters.filter((f) => f !== entry);
200
200
  await this.applySettingsToStream();
201
+ this.call.tracer.trace(
202
+ `unregisterFilter.${TrackType[this.trackType]}`,
203
+ null,
204
+ );
201
205
  }),
202
206
  };
203
207
  }
@@ -219,9 +223,7 @@ export abstract class InputMediaDeviceManager<
219
223
  */
220
224
  async select(deviceId: string | undefined) {
221
225
  if (isReactNative()) {
222
- throw new Error(
223
- 'This method is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for reference.',
224
- );
226
+ throw new Error('This method is not supported in React Native.');
225
227
  }
226
228
  const prevDeviceId = this.state.selectedDevice;
227
229
  if (deviceId === prevDeviceId) {
@@ -195,6 +195,7 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
195
195
  this.logger('warn', 'Failed to unregister noise cancellation', err);
196
196
  });
197
197
 
198
+ this.call.tracer.trace('noiseCancellation.disabled', true);
198
199
  await this.call.notifyNoiseCancellationStopped();
199
200
  }
200
201
 
@@ -245,7 +246,7 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
245
246
  }
246
247
 
247
248
  protected getDevices(): Observable<MediaDeviceInfo[]> {
248
- return getAudioDevices();
249
+ return getAudioDevices(this.call.tracer);
249
250
  }
250
251
 
251
252
  protected getStream(
@@ -56,7 +56,7 @@ export class SpeakerManager {
56
56
  'This feature is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for more details',
57
57
  );
58
58
  }
59
- return getAudioOutputDevices();
59
+ return getAudioOutputDevices(this.call.tracer);
60
60
  }
61
61
 
62
62
  /**
@@ -7,6 +7,7 @@ import {
7
7
  merge,
8
8
  shareReplay,
9
9
  startWith,
10
+ tap,
10
11
  } from 'rxjs';
11
12
  import { getLogger } from '../logger';
12
13
  import { BrowserPermission } from './BrowserPermission';
@@ -21,8 +22,13 @@ import { getCurrentValue } from '../store/rxUtils';
21
22
  *
22
23
  * @param permission a BrowserPermission instance.
23
24
  * @param kind the kind of devices to enumerate.
25
+ * @param tracer the tracer to use for tracing the device enumeration.
24
26
  */
25
- const getDevices = (permission: BrowserPermission, kind: MediaDeviceKind) => {
27
+ const getDevices = (
28
+ permission: BrowserPermission,
29
+ kind: MediaDeviceKind,
30
+ tracer: Tracer | undefined,
31
+ ) => {
26
32
  return from(
27
33
  (async () => {
28
34
  let devices = await navigator.mediaDevices.enumerateDevices();
@@ -34,6 +40,11 @@ const getDevices = (permission: BrowserPermission, kind: MediaDeviceKind) => {
34
40
  if (shouldPromptForBrowserPermission && (await permission.prompt())) {
35
41
  devices = await navigator.mediaDevices.enumerateDevices();
36
42
  }
43
+ tracer?.traceOnce(
44
+ 'device-enumeration',
45
+ 'navigator.mediaDevices.enumerateDevices',
46
+ devices,
47
+ );
37
48
  return devices.filter(
38
49
  (device) =>
39
50
  device.kind === kind &&
@@ -99,11 +110,12 @@ export const getVideoBrowserPermission = lazy(
99
110
  }),
100
111
  );
101
112
 
102
- const getDeviceChangeObserver = lazy(() => {
113
+ const getDeviceChangeObserver = lazy((tracer: Tracer | undefined) => {
103
114
  // 'addEventListener' is not available in React Native, returning
104
115
  // an observable that will never fire
105
116
  if (!navigator.mediaDevices.addEventListener) return from([]);
106
117
  return fromEvent(navigator.mediaDevices, 'devicechange').pipe(
118
+ tap(() => tracer?.resetTrace('device-enumeration')),
107
119
  map(() => undefined),
108
120
  debounceTime(500),
109
121
  );
@@ -115,13 +127,15 @@ const getDeviceChangeObserver = lazy(() => {
115
127
  * if devices are added/removed the list is updated, and if the permission is revoked,
116
128
  * the observable errors.
117
129
  */
118
- export const getAudioDevices = lazy(() => {
130
+ export const getAudioDevices = lazy((tracer?: Tracer) => {
119
131
  return merge(
120
- getDeviceChangeObserver(),
132
+ getDeviceChangeObserver(tracer),
121
133
  getAudioBrowserPermission().asObservable(),
122
134
  ).pipe(
123
135
  startWith(undefined),
124
- concatMap(() => getDevices(getAudioBrowserPermission(), 'audioinput')),
136
+ concatMap(() =>
137
+ getDevices(getAudioBrowserPermission(), 'audioinput', tracer),
138
+ ),
125
139
  shareReplay(1),
126
140
  );
127
141
  });
@@ -132,13 +146,15 @@ export const getAudioDevices = lazy(() => {
132
146
  * if devices are added/removed the list is updated, and if the permission is revoked,
133
147
  * the observable errors.
134
148
  */
135
- export const getVideoDevices = lazy(() => {
149
+ export const getVideoDevices = lazy((tracer?: Tracer) => {
136
150
  return merge(
137
- getDeviceChangeObserver(),
151
+ getDeviceChangeObserver(tracer),
138
152
  getVideoBrowserPermission().asObservable(),
139
153
  ).pipe(
140
154
  startWith(undefined),
141
- concatMap(() => getDevices(getVideoBrowserPermission(), 'videoinput')),
155
+ concatMap(() =>
156
+ getDevices(getVideoBrowserPermission(), 'videoinput', tracer),
157
+ ),
142
158
  shareReplay(1),
143
159
  );
144
160
  });
@@ -149,13 +165,15 @@ export const getVideoDevices = lazy(() => {
149
165
  * if devices are added/removed the list is updated, and if the permission is revoked,
150
166
  * the observable errors.
151
167
  */
152
- export const getAudioOutputDevices = lazy(() => {
168
+ export const getAudioOutputDevices = lazy((tracer?: Tracer) => {
153
169
  return merge(
154
- getDeviceChangeObserver(),
170
+ getDeviceChangeObserver(tracer),
155
171
  getAudioBrowserPermission().asObservable(),
156
172
  ).pipe(
157
173
  startWith(undefined),
158
- concatMap(() => getDevices(getAudioBrowserPermission(), 'audiooutput')),
174
+ concatMap(() =>
175
+ getDevices(getAudioBrowserPermission(), 'audiooutput', tracer),
176
+ ),
159
177
  shareReplay(1),
160
178
  );
161
179
  });
@@ -2,8 +2,10 @@ import { describe, expect, it, vi } from 'vitest';
2
2
  import { Call } from '../../Call';
3
3
  import { Dispatcher } from '../../rtc';
4
4
  import { CallState } from '../../store';
5
+ import { noopComparator } from '../../sorting';
5
6
  import {
6
7
  watchConnectionQualityChanged,
8
+ watchInboundStateNotification,
7
9
  watchLiveEnded,
8
10
  watchParticipantCountChanged,
9
11
  watchPinsUpdated,
@@ -11,6 +13,7 @@ import {
11
13
  import {
12
14
  ConnectionQuality,
13
15
  ErrorCode,
16
+ TrackType,
14
17
  } from '../../gen/video/sfu/models/models';
15
18
 
16
19
  describe('internal events', () => {
@@ -131,4 +134,79 @@ describe('internal events', () => {
131
134
  { userId: 'u2', sessionId: 'session-2', pin: undefined },
132
135
  ]);
133
136
  });
137
+
138
+ it('handles InboundStateNotification', () => {
139
+ const state = new CallState();
140
+ state.setSortParticipantsBy(noopComparator());
141
+ state.setParticipants([
142
+ // @ts-expect-error incomplete data
143
+ { sessionId: 'session-1' },
144
+ // @ts-expect-error incomplete data
145
+ { sessionId: 'session-2' },
146
+ ]);
147
+
148
+ const update = watchInboundStateNotification(state);
149
+ update({
150
+ inboundVideoStates: [
151
+ {
152
+ userId: '1',
153
+ sessionId: 'session-1',
154
+ trackType: TrackType.VIDEO,
155
+ paused: true,
156
+ },
157
+ {
158
+ userId: '2',
159
+ sessionId: 'session-2',
160
+ trackType: TrackType.VIDEO,
161
+ paused: false,
162
+ },
163
+ ],
164
+ });
165
+ expect(
166
+ state.findParticipantBySessionId('session-1')?.pausedTracks,
167
+ ).toContain(TrackType.VIDEO);
168
+ expect(
169
+ state.findParticipantBySessionId('session-2')?.pausedTracks,
170
+ ).not.toContain(TrackType.VIDEO);
171
+
172
+ update({
173
+ inboundVideoStates: [
174
+ {
175
+ userId: '2',
176
+ sessionId: 'session-2',
177
+ trackType: TrackType.VIDEO,
178
+ paused: true,
179
+ },
180
+ ],
181
+ });
182
+ expect(
183
+ state.findParticipantBySessionId('session-1')?.pausedTracks,
184
+ ).toContain(TrackType.VIDEO);
185
+ expect(
186
+ state.findParticipantBySessionId('session-2')?.pausedTracks,
187
+ ).toContain(TrackType.VIDEO);
188
+
189
+ update({
190
+ inboundVideoStates: [
191
+ {
192
+ userId: '1',
193
+ sessionId: 'session-1',
194
+ trackType: TrackType.VIDEO,
195
+ paused: false,
196
+ },
197
+ {
198
+ userId: '2',
199
+ sessionId: 'session-2',
200
+ trackType: TrackType.VIDEO,
201
+ paused: false,
202
+ },
203
+ ],
204
+ });
205
+ expect(
206
+ state.findParticipantBySessionId('session-1')?.pausedTracks,
207
+ ).not.toContain(TrackType.VIDEO);
208
+ expect(
209
+ state.findParticipantBySessionId('session-2')?.pausedTracks,
210
+ ).not.toContain(TrackType.VIDEO);
211
+ });
134
212
  });
@@ -247,6 +247,72 @@ describe('Participant events', () => {
247
247
  });
248
248
  });
249
249
 
250
+ it('resets the paused track list if the track is unpublished', () => {
251
+ const state = new CallState();
252
+ state.setParticipants([
253
+ // @ts-expect-error setup one participant
254
+ {
255
+ sessionId: 'session-id',
256
+ publishedTracks: [TrackType.VIDEO, TrackType.SCREEN_SHARE],
257
+ pausedTracks: [TrackType.VIDEO, TrackType.SCREEN_SHARE],
258
+ },
259
+ ]);
260
+
261
+ const trackUnpublish = watchTrackUnpublished(state);
262
+ // @ts-expect-error incomplete data
263
+ trackUnpublish({ sessionId: 'session-id', type: TrackType.VIDEO });
264
+ expect(state.findParticipantBySessionId('session-id')).toEqual({
265
+ sessionId: 'session-id',
266
+ publishedTracks: [TrackType.SCREEN_SHARE],
267
+ pausedTracks: [TrackType.SCREEN_SHARE],
268
+ });
269
+
270
+ // @ts-expect-error incomplete data
271
+ trackUnpublish({ sessionId: 'session-id', type: TrackType.SCREEN_SHARE });
272
+ expect(state.findParticipantBySessionId('session-id')).toEqual({
273
+ sessionId: 'session-id',
274
+ publishedTracks: [],
275
+ pausedTracks: [],
276
+ });
277
+ });
278
+
279
+ it('resets the paused track list if the track is unpublished on full participant update', () => {
280
+ const state = new CallState();
281
+ state.setParticipants([
282
+ // @ts-expect-error setup one participant
283
+ {
284
+ sessionId: 'session-id',
285
+ publishedTracks: [TrackType.VIDEO, TrackType.SCREEN_SHARE],
286
+ pausedTracks: [TrackType.VIDEO, TrackType.SCREEN_SHARE],
287
+ },
288
+ ]);
289
+
290
+ const trackUnpublished = watchTrackUnpublished(state);
291
+ trackUnpublished({
292
+ sessionId: 'session-id',
293
+ type: TrackType.VIDEO,
294
+ // @ts-expect-error incomplete data
295
+ participant: { publishedTracks: [TrackType.SCREEN_SHARE] },
296
+ });
297
+ expect(state.findParticipantBySessionId('session-id')).toEqual({
298
+ sessionId: 'session-id',
299
+ publishedTracks: [TrackType.SCREEN_SHARE],
300
+ pausedTracks: [TrackType.SCREEN_SHARE],
301
+ });
302
+
303
+ trackUnpublished({
304
+ sessionId: 'session-id',
305
+ type: TrackType.SCREEN_SHARE,
306
+ // @ts-expect-error incomplete data
307
+ participant: { publishedTracks: [] },
308
+ });
309
+ expect(state.findParticipantBySessionId('session-id')).toEqual({
310
+ sessionId: 'session-id',
311
+ publishedTracks: [],
312
+ pausedTracks: [],
313
+ });
314
+ });
315
+
250
316
  it('adds the participant to the list of participants if provided', () => {
251
317
  const state = new CallState();
252
318
  const handler = watchTrackUnpublished(state);
@@ -9,6 +9,7 @@ import {
9
9
  watchCallRejected,
10
10
  watchConnectionQualityChanged,
11
11
  watchDominantSpeakerChanged,
12
+ watchInboundStateNotification,
12
13
  watchLiveEnded,
13
14
  watchParticipantCountChanged,
14
15
  watchParticipantJoined,
@@ -60,6 +61,7 @@ export const registerEventHandlers = (call: Call, dispatcher: Dispatcher) => {
60
61
 
61
62
  call.on('callGrantsUpdated', watchCallGrantsUpdated(state)),
62
63
  call.on('pinsUpdated', watchPinsUpdated(state)),
64
+ call.on('inboundStateNotification', watchInboundStateNotification(state)),
63
65
 
64
66
  handleRemoteSoftMute(call),
65
67
  ];
@@ -3,7 +3,11 @@ import { Call } from '../Call';
3
3
  import { CallState } from '../store';
4
4
  import { StreamVideoParticipantPatches } from '../types';
5
5
  import { getLogger } from '../logger';
6
- import type { PinsChanged } from '../gen/video/sfu/event/events';
6
+ import { pushToIfMissing, removeFromIfPresent } from '../helpers/array';
7
+ import type {
8
+ InboundStateNotification,
9
+ PinsChanged,
10
+ } from '../gen/video/sfu/event/events';
7
11
  import {
8
12
  ErrorCode,
9
13
  WebsocketReconnectStrategy,
@@ -89,3 +93,26 @@ export const watchPinsUpdated = (state: CallState) => {
89
93
  state.setServerSidePins(pins);
90
94
  };
91
95
  };
96
+
97
+ /**
98
+ * Watches for inbound state notifications and updates the paused tracks
99
+ *
100
+ * @param state the call state to update.
101
+ */
102
+ export const watchInboundStateNotification = (state: CallState) => {
103
+ return function onInboundStateNotification(e: InboundStateNotification) {
104
+ const { inboundVideoStates } = e;
105
+ const current = state.getParticipantLookupBySessionId();
106
+ const patches: StreamVideoParticipantPatches = {};
107
+ for (const { sessionId, trackType, paused } of inboundVideoStates) {
108
+ const pausedTracks = [...(current[sessionId]?.pausedTracks ?? [])];
109
+ if (paused) {
110
+ pushToIfMissing(pausedTracks, trackType);
111
+ } else {
112
+ removeFromIfPresent(pausedTracks, trackType);
113
+ }
114
+ patches[sessionId] = { pausedTracks };
115
+ }
116
+ state.updateParticipants(patches);
117
+ };
118
+ };
@@ -106,10 +106,13 @@ export const watchTrackUnpublished = (state: CallState) => {
106
106
  if (e.participant) {
107
107
  const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
108
108
  const participant = Object.assign(e.participant, orphanedTracks);
109
- state.updateOrAddParticipant(sessionId, participant);
109
+ state.updateOrAddParticipant(sessionId, participant, (p) => ({
110
+ pausedTracks: p.pausedTracks?.filter((t) => t !== type),
111
+ }));
110
112
  } else {
111
113
  state.updateParticipant(sessionId, (p) => ({
112
114
  publishedTracks: p.publishedTracks.filter((t) => t !== type),
115
+ pausedTracks: p.pausedTracks?.filter((t) => t !== type),
113
116
  }));
114
117
  }
115
118
  };
@@ -6,6 +6,7 @@ import {
6
6
  CallEndedReason,
7
7
  CallGrants,
8
8
  CallState,
9
+ ClientCapability,
9
10
  ClientDetails,
10
11
  Codec,
11
12
  ConnectionQuality,
@@ -255,6 +256,15 @@ export interface SfuEvent {
255
256
  */
256
257
  changePublishOptions: ChangePublishOptions;
257
258
  }
259
+ | {
260
+ oneofKind: 'inboundStateNotification';
261
+ /**
262
+ * InboundStateNotification
263
+ *
264
+ * @generated from protobuf field: stream.video.sfu.event.InboundStateNotification inbound_state_notification = 28;
265
+ */
266
+ inboundStateNotification: InboundStateNotification;
267
+ }
258
268
  | {
259
269
  oneofKind: undefined;
260
270
  };
@@ -508,6 +518,10 @@ export interface JoinRequest {
508
518
  * @generated from protobuf field: repeated stream.video.sfu.models.SubscribeOption preferred_subscribe_options = 10;
509
519
  */
510
520
  preferredSubscribeOptions: SubscribeOption[];
521
+ /**
522
+ * @generated from protobuf field: repeated stream.video.sfu.models.ClientCapability capabilities = 11;
523
+ */
524
+ capabilities: ClientCapability[];
511
525
  }
512
526
  /**
513
527
  * @generated from protobuf message stream.video.sfu.event.ReconnectDetails
@@ -875,6 +889,36 @@ export interface CallEnded {
875
889
  */
876
890
  reason: CallEndedReason;
877
891
  }
892
+ /**
893
+ * @generated from protobuf message stream.video.sfu.event.InboundStateNotification
894
+ */
895
+ export interface InboundStateNotification {
896
+ /**
897
+ * @generated from protobuf field: repeated stream.video.sfu.event.InboundVideoState inbound_video_states = 1;
898
+ */
899
+ inboundVideoStates: InboundVideoState[];
900
+ }
901
+ /**
902
+ * @generated from protobuf message stream.video.sfu.event.InboundVideoState
903
+ */
904
+ export interface InboundVideoState {
905
+ /**
906
+ * @generated from protobuf field: string user_id = 1;
907
+ */
908
+ userId: string;
909
+ /**
910
+ * @generated from protobuf field: string session_id = 2;
911
+ */
912
+ sessionId: string;
913
+ /**
914
+ * @generated from protobuf field: stream.video.sfu.models.TrackType track_type = 3;
915
+ */
916
+ trackType: TrackType;
917
+ /**
918
+ * @generated from protobuf field: bool paused = 4;
919
+ */
920
+ paused: boolean;
921
+ }
878
922
  // @generated message type with reflection information, may provide speed optimized methods
879
923
  class SfuEvent$Type extends MessageType<SfuEvent> {
880
924
  constructor() {
@@ -1033,6 +1077,13 @@ class SfuEvent$Type extends MessageType<SfuEvent> {
1033
1077
  oneof: 'eventPayload',
1034
1078
  T: () => ChangePublishOptions,
1035
1079
  },
1080
+ {
1081
+ no: 28,
1082
+ name: 'inbound_state_notification',
1083
+ kind: 'message',
1084
+ oneof: 'eventPayload',
1085
+ T: () => InboundStateNotification,
1086
+ },
1036
1087
  ]);
1037
1088
  }
1038
1089
  }
@@ -1342,6 +1393,17 @@ class JoinRequest$Type extends MessageType<JoinRequest> {
1342
1393
  repeat: 2 /*RepeatType.UNPACKED*/,
1343
1394
  T: () => SubscribeOption,
1344
1395
  },
1396
+ {
1397
+ no: 11,
1398
+ name: 'capabilities',
1399
+ kind: 'enum',
1400
+ repeat: 1 /*RepeatType.PACKED*/,
1401
+ T: () => [
1402
+ 'stream.video.sfu.models.ClientCapability',
1403
+ ClientCapability,
1404
+ 'CLIENT_CAPABILITY_',
1405
+ ],
1406
+ },
1345
1407
  ]);
1346
1408
  }
1347
1409
  }
@@ -1787,3 +1849,45 @@ class CallEnded$Type extends MessageType<CallEnded> {
1787
1849
  * @generated MessageType for protobuf message stream.video.sfu.event.CallEnded
1788
1850
  */
1789
1851
  export const CallEnded = new CallEnded$Type();
1852
+ // @generated message type with reflection information, may provide speed optimized methods
1853
+ class InboundStateNotification$Type extends MessageType<InboundStateNotification> {
1854
+ constructor() {
1855
+ super('stream.video.sfu.event.InboundStateNotification', [
1856
+ {
1857
+ no: 1,
1858
+ name: 'inbound_video_states',
1859
+ kind: 'message',
1860
+ repeat: 2 /*RepeatType.UNPACKED*/,
1861
+ T: () => InboundVideoState,
1862
+ },
1863
+ ]);
1864
+ }
1865
+ }
1866
+ /**
1867
+ * @generated MessageType for protobuf message stream.video.sfu.event.InboundStateNotification
1868
+ */
1869
+ export const InboundStateNotification = new InboundStateNotification$Type();
1870
+ // @generated message type with reflection information, may provide speed optimized methods
1871
+ class InboundVideoState$Type extends MessageType<InboundVideoState> {
1872
+ constructor() {
1873
+ super('stream.video.sfu.event.InboundVideoState', [
1874
+ { no: 1, name: 'user_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
1875
+ { no: 2, name: 'session_id', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
1876
+ {
1877
+ no: 3,
1878
+ name: 'track_type',
1879
+ kind: 'enum',
1880
+ T: () => [
1881
+ 'stream.video.sfu.models.TrackType',
1882
+ TrackType,
1883
+ 'TRACK_TYPE_',
1884
+ ],
1885
+ },
1886
+ { no: 4, name: 'paused', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
1887
+ ]);
1888
+ }
1889
+ }
1890
+ /**
1891
+ * @generated MessageType for protobuf message stream.video.sfu.event.InboundVideoState
1892
+ */
1893
+ export const InboundVideoState = new InboundVideoState$Type();