@stream-io/video-client 1.56.0 → 1.58.0

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 (112) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/e2ee-worker.browser.es.js +1342 -0
  3. package/dist/e2ee-worker.browser.es.js.map +1 -0
  4. package/dist/index.browser.es.js +867 -71
  5. package/dist/index.browser.es.js.map +1 -1
  6. package/dist/index.cjs.js +911 -70
  7. package/dist/index.cjs.js.map +1 -1
  8. package/dist/index.d.ts +3 -0
  9. package/dist/index.es.js +900 -71
  10. package/dist/index.es.js.map +1 -1
  11. package/dist/src/Call.d.ts +52 -6
  12. package/dist/src/coordinator/connection/connection.d.ts +1 -1
  13. package/dist/src/devices/CameraManager.d.ts +6 -0
  14. package/dist/src/devices/DeviceManager.d.ts +1 -0
  15. package/dist/src/devices/MicrophoneManager.d.ts +6 -0
  16. package/dist/src/devices/SpeakerManager.d.ts +2 -1
  17. package/dist/src/events/participant.d.ts +10 -3
  18. package/dist/src/gen/coordinator/index.d.ts +1363 -243
  19. package/dist/src/gen/video/sfu/models/models.d.ts +8 -0
  20. package/dist/src/helpers/MediaPlaybackWatchdog.d.ts +4 -0
  21. package/dist/src/helpers/TypedEventEmitter.d.ts +23 -0
  22. package/dist/src/helpers/ensureExhausted.d.ts +1 -1
  23. package/dist/src/permissions/PermissionsContext.d.ts +2 -2
  24. package/dist/src/rtc/BasePeerConnection.d.ts +3 -1
  25. package/dist/src/rtc/e2ee/E2EEManager.d.ts +12 -0
  26. package/dist/src/rtc/e2ee/EncryptionManager.d.ts +148 -0
  27. package/dist/src/rtc/e2ee/e2ee-worker.d.ts +25 -0
  28. package/dist/src/rtc/e2ee/events.d.ts +163 -0
  29. package/dist/src/rtc/e2ee/transformSupport.d.ts +21 -0
  30. package/dist/src/rtc/helpers/rtcConfiguration.d.ts +2 -2
  31. package/dist/src/rtc/index.d.ts +1 -0
  32. package/dist/src/rtc/mediaEngine.d.ts +23 -0
  33. package/dist/src/rtc/types.d.ts +2 -0
  34. package/dist/src/store/CallState.d.ts +21 -0
  35. package/dist/src/types.d.ts +15 -6
  36. package/index.ts +3 -0
  37. package/package.json +11 -11
  38. package/src/Call.ts +160 -26
  39. package/src/__tests__/Call.test.ts +15 -0
  40. package/src/coordinator/connection/connection.ts +6 -3
  41. package/src/devices/AudioDeviceManager.ts +6 -0
  42. package/src/devices/CameraManager.ts +51 -4
  43. package/src/devices/DeviceManager.ts +17 -0
  44. package/src/devices/MicrophoneManager.ts +55 -5
  45. package/src/devices/ScreenShareManager.ts +3 -0
  46. package/src/devices/SpeakerManager.ts +42 -12
  47. package/src/devices/__tests__/MicrophoneManagerRN.test.ts +2 -22
  48. package/src/devices/__tests__/SpeakerManager.test.ts +138 -42
  49. package/src/devices/__tests__/mocks.ts +36 -3
  50. package/src/devices/devicePersistence.ts +1 -2
  51. package/src/events/__tests__/participant.test.ts +134 -1
  52. package/src/events/callEventHandlers.ts +9 -3
  53. package/src/events/participant.ts +42 -7
  54. package/src/gen/coordinator/index.ts +1384 -244
  55. package/src/gen/video/sfu/models/models.ts +15 -0
  56. package/src/helpers/MediaPlaybackWatchdog.ts +35 -16
  57. package/src/helpers/TypedEventEmitter.ts +71 -0
  58. package/src/helpers/__tests__/MediaPlaybackWatchdog.test.ts +137 -8
  59. package/src/helpers/__tests__/TypedEventEmitter.test.ts +133 -0
  60. package/src/helpers/__tests__/ViewportTracker.test.ts +1 -2
  61. package/src/helpers/ensureExhausted.ts +4 -2
  62. package/src/permissions/PermissionsContext.ts +10 -5
  63. package/src/reporting/ClientEventReporter.ts +2 -8
  64. package/src/rtc/BasePeerConnection.ts +12 -1
  65. package/src/rtc/Publisher.ts +8 -0
  66. package/src/rtc/Subscriber.ts +5 -0
  67. package/src/rtc/__tests__/Publisher.test.ts +101 -0
  68. package/src/rtc/__tests__/Subscriber.test.ts +89 -0
  69. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +31 -0
  70. package/src/rtc/e2ee/E2EEManager.ts +12 -0
  71. package/src/rtc/e2ee/EncryptionManager.ts +336 -0
  72. package/src/rtc/e2ee/SPEC.md +586 -0
  73. package/src/rtc/e2ee/__tests__/E2EEManager.contract.test.ts +39 -0
  74. package/src/rtc/e2ee/__tests__/EncryptionManager.test.ts +490 -0
  75. package/src/rtc/e2ee/__tests__/codec.test.ts +247 -0
  76. package/src/rtc/e2ee/__tests__/conformance.test.ts +184 -0
  77. package/src/rtc/e2ee/__tests__/failureTracker.test.ts +45 -0
  78. package/src/rtc/e2ee/__tests__/frameCounter.test.ts +90 -0
  79. package/src/rtc/e2ee/__tests__/keyStore.test.ts +221 -0
  80. package/src/rtc/e2ee/__tests__/notifications.test.ts +166 -0
  81. package/src/rtc/e2ee/__tests__/perf.test.ts +139 -0
  82. package/src/rtc/e2ee/__tests__/queue.test.ts +61 -0
  83. package/src/rtc/e2ee/__tests__/replayWindow.test.ts +133 -0
  84. package/src/rtc/e2ee/__tests__/trailer.test.ts +143 -0
  85. package/src/rtc/e2ee/__tests__/transform-pipeline.test.ts +839 -0
  86. package/src/rtc/e2ee/__tests__/transformSupport.test.ts +65 -0
  87. package/src/rtc/e2ee/e2ee-worker/codec.ts +220 -0
  88. package/src/rtc/e2ee/e2ee-worker/constants.ts +47 -0
  89. package/src/rtc/e2ee/e2ee-worker/decode.ts +159 -0
  90. package/src/rtc/e2ee/e2ee-worker/e2ee-worker-impl.ts +137 -0
  91. package/src/rtc/e2ee/e2ee-worker/encode.ts +147 -0
  92. package/src/rtc/e2ee/e2ee-worker/failureTracker.ts +26 -0
  93. package/src/rtc/e2ee/e2ee-worker/frameCounter.ts +37 -0
  94. package/src/rtc/e2ee/e2ee-worker/keyStore.ts +211 -0
  95. package/src/rtc/e2ee/e2ee-worker/notifications.ts +190 -0
  96. package/src/rtc/e2ee/e2ee-worker/perf.ts +129 -0
  97. package/src/rtc/e2ee/e2ee-worker/queue.ts +12 -0
  98. package/src/rtc/e2ee/e2ee-worker/replayWindow.ts +131 -0
  99. package/src/rtc/e2ee/e2ee-worker/trailer.ts +155 -0
  100. package/src/rtc/e2ee/e2ee-worker/tsconfig.json +8 -0
  101. package/src/rtc/e2ee/e2ee-worker/types.ts +29 -0
  102. package/src/rtc/e2ee/e2ee-worker.ts +27 -0
  103. package/src/rtc/e2ee/events.ts +179 -0
  104. package/src/rtc/e2ee/transformSupport.ts +35 -0
  105. package/src/rtc/helpers/rtcConfiguration.ts +4 -2
  106. package/src/rtc/index.ts +1 -0
  107. package/src/rtc/mediaEngine.ts +44 -0
  108. package/src/rtc/types.ts +3 -2
  109. package/src/stats/rtc/types.ts +8 -2
  110. package/src/store/CallState.ts +34 -3
  111. package/src/store/__tests__/CallState.test.ts +82 -1
  112. package/src/types.ts +24 -9
@@ -1,5 +1,5 @@
1
1
  import '../../rtc/__tests__/mocks/webrtc.mocks';
2
- import { describe, expect, it } from 'vitest';
2
+ import { describe, expect, it, vi } from 'vitest';
3
3
  import { CallState } from '../../store';
4
4
  import { VisibilityState } from '../../types';
5
5
  import { TrackType } from '../../gen/video/sfu/models/models';
@@ -192,6 +192,139 @@ describe('Participant events', () => {
192
192
  expect(p?.screenShareStream).toBe(mediaStream);
193
193
  expect(state.takeOrphanedTracks('track-lookup-prefix')).toHaveLength(0);
194
194
  });
195
+
196
+ it('participantJoined should attach E2EE decryptor to orphaned track receiver', () => {
197
+ const state = new CallState();
198
+ const mediaStream = new MediaStream();
199
+ const receiver = {} as RTCRtpReceiver;
200
+ const e2ee = { decrypt: vi.fn() };
201
+ state.registerOrphanedTrack({
202
+ trackLookupPrefix: 'track-lookup-prefix',
203
+ trackType: TrackType.VIDEO,
204
+ track: mediaStream,
205
+ id: mediaStream.id,
206
+ receiver,
207
+ });
208
+ // @ts-expect-error partial mock
209
+ const onParticipantJoined = watchParticipantJoined(state, () => e2ee);
210
+ onParticipantJoined({
211
+ // @ts-expect-error incomplete data
212
+ participant: {
213
+ userId: 'user-id',
214
+ sessionId: 'session-id',
215
+ trackLookupPrefix: 'track-lookup-prefix',
216
+ },
217
+ });
218
+
219
+ expect(e2ee.decrypt).toHaveBeenCalledWith(receiver, 'user-id', 'VIDEO');
220
+ });
221
+
222
+ it('trackPublished should attach E2EE decryptor to orphaned track receiver', () => {
223
+ const state = new CallState();
224
+ const mediaStream = new MediaStream();
225
+ const receiver = {} as RTCRtpReceiver;
226
+ const e2ee = { decrypt: vi.fn() };
227
+ state.registerOrphanedTrack({
228
+ trackLookupPrefix: 'track-lookup-prefix',
229
+ trackType: TrackType.AUDIO,
230
+ track: mediaStream,
231
+ id: mediaStream.id,
232
+ receiver,
233
+ });
234
+ // @ts-expect-error partial mock
235
+ const onTrackPublished = watchTrackPublished(state, () => e2ee);
236
+ onTrackPublished({
237
+ // @ts-expect-error incomplete data
238
+ participant: {
239
+ userId: 'user-id',
240
+ sessionId: 'session-id',
241
+ trackLookupPrefix: 'track-lookup-prefix',
242
+ },
243
+ });
244
+
245
+ expect(e2ee.decrypt).toHaveBeenCalledWith(receiver, 'user-id', 'AUDIO');
246
+ });
247
+
248
+ it('trackUnpublished should attach E2EE decryptor to orphaned track receiver', () => {
249
+ const state = new CallState();
250
+ const mediaStream = new MediaStream();
251
+ const receiver = {} as RTCRtpReceiver;
252
+ const e2ee = { decrypt: vi.fn() };
253
+ state.registerOrphanedTrack({
254
+ trackLookupPrefix: 'track-lookup-prefix',
255
+ trackType: TrackType.SCREEN_SHARE,
256
+ track: mediaStream,
257
+ id: mediaStream.id,
258
+ receiver,
259
+ });
260
+ // @ts-expect-error partial mock
261
+ const onTrackUnPublished = watchTrackUnpublished(state, () => e2ee);
262
+ onTrackUnPublished({
263
+ // @ts-expect-error incomplete data
264
+ participant: {
265
+ userId: 'user-id',
266
+ sessionId: 'session-id',
267
+ trackLookupPrefix: 'track-lookup-prefix',
268
+ },
269
+ });
270
+
271
+ expect(e2ee.decrypt).toHaveBeenCalledWith(
272
+ receiver,
273
+ 'user-id',
274
+ 'SCREEN_SHARE',
275
+ );
276
+ });
277
+
278
+ it('should not call decrypt when no E2EE manager is provided', () => {
279
+ const state = new CallState();
280
+ const mediaStream = new MediaStream();
281
+ const receiver = {} as RTCRtpReceiver;
282
+ state.registerOrphanedTrack({
283
+ trackLookupPrefix: 'track-lookup-prefix',
284
+ trackType: TrackType.VIDEO,
285
+ track: mediaStream,
286
+ id: mediaStream.id,
287
+ receiver,
288
+ });
289
+ const onParticipantJoined = watchParticipantJoined(state);
290
+ onParticipantJoined({
291
+ // @ts-expect-error incomplete data
292
+ participant: {
293
+ userId: 'user-id',
294
+ sessionId: 'session-id',
295
+ trackLookupPrefix: 'track-lookup-prefix',
296
+ },
297
+ });
298
+
299
+ const p = state.findParticipantBySessionId('session-id');
300
+ expect(p?.videoStream).toBe(mediaStream);
301
+ });
302
+
303
+ it('should not call decrypt when orphaned track has no receiver', () => {
304
+ const state = new CallState();
305
+ const mediaStream = new MediaStream();
306
+ const e2ee = { decrypt: vi.fn() };
307
+ state.registerOrphanedTrack({
308
+ trackLookupPrefix: 'track-lookup-prefix',
309
+ trackType: TrackType.VIDEO,
310
+ track: mediaStream,
311
+ id: mediaStream.id,
312
+ });
313
+ // @ts-expect-error partial mock
314
+ const onParticipantJoined = watchParticipantJoined(state, () => e2ee);
315
+ onParticipantJoined({
316
+ // @ts-expect-error incomplete data
317
+ participant: {
318
+ userId: 'user-id',
319
+ sessionId: 'session-id',
320
+ trackLookupPrefix: 'track-lookup-prefix',
321
+ },
322
+ });
323
+
324
+ expect(e2ee.decrypt).not.toHaveBeenCalled();
325
+ const p = state.findParticipantBySessionId('session-id');
326
+ expect(p?.videoStream).toBe(mediaStream);
327
+ });
195
328
  });
196
329
 
197
330
  describe('trackPublished', () => {
@@ -40,6 +40,12 @@ type RingCallEvents = Extract<
40
40
  */
41
41
  export const registerEventHandlers = (call: Call, dispatcher: Dispatcher) => {
42
42
  const state = call.state;
43
+ // Read lazily on each event: setE2EEManager can run after setup() (an app that
44
+ // inspects call settings via get()/getOrCreate() before deciding to encrypt has
45
+ // already triggered it), so a value captured here would be a stale undefined
46
+ // for the whole call and orphaned tracks would never get a decryptor.
47
+ const e2ee = () => call.e2eeManager;
48
+
43
49
  const eventHandlers = [
44
50
  call.on('call.ended', watchCallEnded(call)),
45
51
  watchSfuCallEnded(call),
@@ -49,12 +55,12 @@ export const registerEventHandlers = (call: Call, dispatcher: Dispatcher) => {
49
55
  watchConnectionQualityChanged(dispatcher, state),
50
56
  watchParticipantCountChanged(dispatcher, state),
51
57
 
52
- call.on('participantJoined', watchParticipantJoined(state)),
58
+ call.on('participantJoined', watchParticipantJoined(state, e2ee)),
53
59
  call.on('participantLeft', watchParticipantLeft(state)),
54
60
  call.on('participantUpdated', watchParticipantUpdated(state)),
55
61
 
56
- call.on('trackPublished', watchTrackPublished(state)),
57
- call.on('trackUnpublished', watchTrackUnpublished(state)),
62
+ call.on('trackPublished', watchTrackPublished(state, e2ee)),
63
+ call.on('trackUnpublished', watchTrackUnpublished(state, e2ee)),
58
64
 
59
65
  watchAudioLevelChanged(dispatcher, state),
60
66
  watchDominantSpeakerChanged(dispatcher, state),
@@ -5,7 +5,7 @@ import type {
5
5
  TrackPublished,
6
6
  TrackUnpublished,
7
7
  } from '../gen/video/sfu/event/events';
8
- import type { Participant } from '../gen/video/sfu/models/models';
8
+ import { type Participant, TrackType } from '../gen/video/sfu/models/models';
9
9
  import {
10
10
  StreamVideoParticipant,
11
11
  StreamVideoParticipantPatch,
@@ -14,11 +14,21 @@ import {
14
14
  import { CallState } from '../store';
15
15
  import { trackTypeToParticipantStreamKey } from '../rtc';
16
16
  import { pushToIfMissing } from '../helpers/array';
17
+ import type { E2EEManager } from '../rtc/e2ee/E2EEManager';
18
+
19
+ /**
20
+ * Reads the call's current encryption manager. Deliberately not the manager
21
+ * itself: `Call.setE2EEManager` may be called after these handlers are wired up.
22
+ */
23
+ type GetE2EEManager = () => E2EEManager | undefined;
17
24
 
18
25
  /**
19
26
  * An event responder which handles the `participantJoined` event.
20
27
  */
21
- export const watchParticipantJoined = (state: CallState) => {
28
+ export const watchParticipantJoined = (
29
+ state: CallState,
30
+ e2ee: GetE2EEManager | undefined = undefined,
31
+ ) => {
22
32
  return function onParticipantJoined(e: ParticipantJoined) {
23
33
  const { participant } = e;
24
34
  if (!participant) return;
@@ -30,7 +40,7 @@ export const watchParticipantJoined = (state: CallState) => {
30
40
  // The SFU would send participant info as part of the `join`
31
41
  // response and then follow up with a `participantJoined` event for
32
42
  // already announced participants.
33
- const orphanedTracks = reconcileOrphanedTracks(state, participant);
43
+ const orphanedTracks = reconcileOrphanedTracks(state, participant, e2ee);
34
44
  state.updateOrAddParticipant(
35
45
  participant.sessionId,
36
46
  Object.assign<
@@ -77,7 +87,10 @@ export const watchParticipantUpdated = (state: CallState) => {
77
87
  * An event responder which handles the `trackPublished` event.
78
88
  * The SFU will send this event when a participant publishes a track.
79
89
  */
80
- export const watchTrackPublished = (state: CallState) => {
90
+ export const watchTrackPublished = (
91
+ state: CallState,
92
+ e2ee: GetE2EEManager | undefined = undefined,
93
+ ) => {
81
94
  return function onTrackPublished(e: TrackPublished) {
82
95
  const { type, sessionId } = e;
83
96
  // An optimization for large calls.
@@ -85,7 +98,11 @@ export const watchTrackPublished = (state: CallState) => {
85
98
  // events, and instead, it would only provide the participant's information
86
99
  // once they start publishing a track.
87
100
  if (e.participant) {
88
- const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
101
+ const orphanedTracks = reconcileOrphanedTracks(
102
+ state,
103
+ e.participant,
104
+ e2ee,
105
+ );
89
106
  const participant = Object.assign(e.participant, orphanedTracks);
90
107
  state.updateOrAddParticipant(sessionId, participant);
91
108
  } else {
@@ -100,12 +117,19 @@ export const watchTrackPublished = (state: CallState) => {
100
117
  * An event responder which handles the `trackUnpublished` event.
101
118
  * The SFU will send this event when a participant unpublishes a track.
102
119
  */
103
- export const watchTrackUnpublished = (state: CallState) => {
120
+ export const watchTrackUnpublished = (
121
+ state: CallState,
122
+ e2ee: GetE2EEManager | undefined = undefined,
123
+ ) => {
104
124
  return function onTrackUnpublished(e: TrackUnpublished) {
105
125
  const { type, sessionId } = e;
106
126
  // An optimization for large calls. See `watchTrackPublished`.
107
127
  if (e.participant) {
108
- const orphanedTracks = reconcileOrphanedTracks(state, e.participant);
128
+ const orphanedTracks = reconcileOrphanedTracks(
129
+ state,
130
+ e.participant,
131
+ e2ee,
132
+ );
109
133
  const participant = Object.assign(e.participant, orphanedTracks);
110
134
  state.updateOrAddParticipant(sessionId, participant, (p) => ({
111
135
  pausedTracks: p.pausedTracks?.filter((t) => t !== type),
@@ -124,18 +148,29 @@ export const watchTrackUnpublished = (state: CallState) => {
124
148
  *
125
149
  * @param state the call state.
126
150
  * @param participant the participant.
151
+ * @param e2ee accessor for the call's encryption manager, if any.
127
152
  */
128
153
  const reconcileOrphanedTracks = (
129
154
  state: CallState,
130
155
  participant: Participant,
156
+ e2ee: GetE2EEManager | undefined,
131
157
  ): StreamVideoParticipantPatch | undefined => {
132
158
  const orphanTracks = state.takeOrphanedTracks(participant.trackLookupPrefix);
133
159
  if (!orphanTracks.length) return;
160
+ const manager = e2ee?.();
134
161
  const reconciledTracks: StreamVideoParticipantPatch = {};
135
162
  for (const orphan of orphanTracks) {
136
163
  const key = trackTypeToParticipantStreamKey(orphan.trackType);
137
164
  if (!key) continue;
138
165
  reconciledTracks[key] = orphan.track;
166
+
167
+ if (manager && orphan.receiver) {
168
+ manager.decrypt(
169
+ orphan.receiver,
170
+ participant.userId,
171
+ TrackType[orphan.trackType],
172
+ );
173
+ }
139
174
  }
140
175
  return reconciledTracks;
141
176
  };