@stream-io/video-client 1.13.1 → 1.15.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 (99) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.browser.es.js +1704 -1762
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +1706 -1780
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +1704 -1762
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +61 -30
  9. package/dist/src/StreamSfuClient.d.ts +4 -5
  10. package/dist/src/devices/CameraManager.d.ts +5 -8
  11. package/dist/src/devices/InputMediaDeviceManager.d.ts +5 -5
  12. package/dist/src/devices/MicrophoneManager.d.ts +7 -2
  13. package/dist/src/devices/ScreenShareManager.d.ts +1 -2
  14. package/dist/src/gen/coordinator/index.d.ts +904 -515
  15. package/dist/src/gen/video/sfu/event/events.d.ts +38 -19
  16. package/dist/src/gen/video/sfu/models/models.d.ts +76 -9
  17. package/dist/src/helpers/array.d.ts +7 -0
  18. package/dist/src/permissions/PermissionsContext.d.ts +6 -0
  19. package/dist/src/rtc/BasePeerConnection.d.ts +90 -0
  20. package/dist/src/rtc/Dispatcher.d.ts +0 -1
  21. package/dist/src/rtc/IceTrickleBuffer.d.ts +3 -2
  22. package/dist/src/rtc/Publisher.d.ts +32 -86
  23. package/dist/src/rtc/Subscriber.d.ts +4 -56
  24. package/dist/src/rtc/TransceiverCache.d.ts +55 -0
  25. package/dist/src/rtc/codecs.d.ts +1 -15
  26. package/dist/src/rtc/helpers/sdp.d.ts +8 -0
  27. package/dist/src/rtc/helpers/tracks.d.ts +1 -0
  28. package/dist/src/rtc/index.d.ts +3 -0
  29. package/dist/src/rtc/videoLayers.d.ts +11 -25
  30. package/dist/src/stats/{stateStoreStatsReporter.d.ts → CallStateStatsReporter.d.ts} +5 -1
  31. package/dist/src/stats/SfuStatsReporter.d.ts +4 -2
  32. package/dist/src/stats/index.d.ts +1 -1
  33. package/dist/src/stats/types.d.ts +8 -0
  34. package/dist/src/store/CallState.d.ts +47 -5
  35. package/dist/src/store/rxUtils.d.ts +15 -1
  36. package/dist/src/types.d.ts +26 -22
  37. package/package.json +1 -1
  38. package/src/Call.ts +310 -271
  39. package/src/StreamSfuClient.ts +9 -14
  40. package/src/StreamVideoClient.ts +1 -1
  41. package/src/__tests__/Call.publishing.test.ts +306 -0
  42. package/src/devices/CameraManager.ts +33 -16
  43. package/src/devices/InputMediaDeviceManager.ts +36 -27
  44. package/src/devices/MicrophoneManager.ts +29 -8
  45. package/src/devices/ScreenShareManager.ts +6 -8
  46. package/src/devices/__tests__/CameraManager.test.ts +111 -14
  47. package/src/devices/__tests__/InputMediaDeviceManager.test.ts +4 -4
  48. package/src/devices/__tests__/MicrophoneManager.test.ts +59 -21
  49. package/src/devices/__tests__/ScreenShareManager.test.ts +5 -5
  50. package/src/devices/__tests__/mocks.ts +1 -0
  51. package/src/events/__tests__/internal.test.ts +132 -0
  52. package/src/events/__tests__/mutes.test.ts +0 -3
  53. package/src/events/__tests__/speaker.test.ts +92 -0
  54. package/src/events/participant.ts +3 -4
  55. package/src/gen/coordinator/index.ts +902 -514
  56. package/src/gen/video/sfu/event/events.ts +91 -30
  57. package/src/gen/video/sfu/models/models.ts +105 -13
  58. package/src/helpers/array.ts +14 -0
  59. package/src/permissions/PermissionsContext.ts +22 -0
  60. package/src/permissions/__tests__/PermissionsContext.test.ts +40 -0
  61. package/src/rpc/__tests__/createClient.test.ts +38 -0
  62. package/src/rpc/createClient.ts +11 -5
  63. package/src/rtc/BasePeerConnection.ts +240 -0
  64. package/src/rtc/Dispatcher.ts +0 -9
  65. package/src/rtc/IceTrickleBuffer.ts +24 -4
  66. package/src/rtc/Publisher.ts +210 -528
  67. package/src/rtc/Subscriber.ts +26 -200
  68. package/src/rtc/TransceiverCache.ts +120 -0
  69. package/src/rtc/__tests__/Publisher.test.ts +407 -210
  70. package/src/rtc/__tests__/Subscriber.test.ts +88 -36
  71. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +22 -2
  72. package/src/rtc/__tests__/videoLayers.test.ts +161 -54
  73. package/src/rtc/codecs.ts +1 -131
  74. package/src/rtc/helpers/__tests__/rtcConfiguration.test.ts +34 -0
  75. package/src/rtc/helpers/__tests__/sdp.test.ts +59 -0
  76. package/src/rtc/helpers/sdp.ts +30 -0
  77. package/src/rtc/helpers/tracks.ts +3 -0
  78. package/src/rtc/index.ts +4 -0
  79. package/src/rtc/videoLayers.ts +68 -76
  80. package/src/stats/{stateStoreStatsReporter.ts → CallStateStatsReporter.ts} +58 -27
  81. package/src/stats/SfuStatsReporter.ts +31 -3
  82. package/src/stats/index.ts +1 -1
  83. package/src/stats/types.ts +12 -0
  84. package/src/store/CallState.ts +115 -5
  85. package/src/store/__tests__/CallState.test.ts +101 -0
  86. package/src/store/rxUtils.ts +23 -1
  87. package/src/types.ts +27 -22
  88. package/dist/src/helpers/sdp-munging.d.ts +0 -24
  89. package/dist/src/rtc/bitrateLookup.d.ts +0 -2
  90. package/dist/src/rtc/helpers/iceCandidate.d.ts +0 -2
  91. package/src/helpers/__tests__/hq-audio-sdp.ts +0 -332
  92. package/src/helpers/__tests__/sdp-munging.test.ts +0 -283
  93. package/src/helpers/sdp-munging.ts +0 -265
  94. package/src/rtc/__tests__/bitrateLookup.test.ts +0 -12
  95. package/src/rtc/__tests__/codecs.test.ts +0 -145
  96. package/src/rtc/bitrateLookup.ts +0 -61
  97. package/src/rtc/helpers/iceCandidate.ts +0 -16
  98. /package/dist/src/{compatibility.d.ts → helpers/compatibility.d.ts} +0 -0
  99. /package/src/{compatibility.ts → helpers/compatibility.ts} +0 -0
@@ -20,19 +20,19 @@ import {
20
20
  SendAnswerRequest,
21
21
  SendStatsRequest,
22
22
  SetPublisherRequest,
23
+ TrackMuteState,
23
24
  TrackSubscriptionDetails,
24
- UpdateMuteStatesRequest,
25
25
  } from './gen/video/sfu/signal_rpc/signal';
26
- import { ICETrickle, TrackType } from './gen/video/sfu/models/models';
26
+ import { ICETrickle } from './gen/video/sfu/models/models';
27
27
  import { StreamClient } from './coordinator/connection/client';
28
28
  import { generateUUIDv4 } from './coordinator/connection/utils';
29
29
  import { Credentials } from './gen/coordinator';
30
30
  import { Logger } from './coordinator/connection/types';
31
31
  import { getLogger, getLogLevel } from './logger';
32
32
  import {
33
- promiseWithResolvers,
34
- PromiseWithResolvers,
35
33
  makeSafePromise,
34
+ PromiseWithResolvers,
35
+ promiseWithResolvers,
36
36
  SafePromise,
37
37
  } from './helpers/promise';
38
38
  import { getTimers } from './timers';
@@ -277,7 +277,7 @@ export class StreamSfuClient {
277
277
  this.dispose();
278
278
  };
279
279
 
280
- dispose = () => {
280
+ private dispose = () => {
281
281
  this.logger('debug', 'Disposing SFU client');
282
282
  this.unsubscribeIceTrickle();
283
283
  this.unsubscribeNetworkChanged();
@@ -286,6 +286,7 @@ export class StreamSfuClient {
286
286
  clearTimeout(this.migrateAwayTimeout);
287
287
  this.abortController.abort();
288
288
  this.migrationTask?.resolve();
289
+ this.iceTrickleBuffer.dispose();
289
290
  };
290
291
 
291
292
  leaveAndClose = async (reason: string) => {
@@ -340,17 +341,11 @@ export class StreamSfuClient {
340
341
  );
341
342
  };
342
343
 
343
- updateMuteState = async (trackType: TrackType, muted: boolean) => {
344
- await this.joinTask;
345
- return this.updateMuteStates({ muteStates: [{ trackType, muted }] });
346
- };
347
-
348
- updateMuteStates = async (
349
- data: Omit<UpdateMuteStatesRequest, 'sessionId'>,
350
- ) => {
344
+ updateMuteStates = async (muteStates: TrackMuteState[]) => {
351
345
  await this.joinTask;
352
346
  return retryable(
353
- () => this.rpc.updateMuteStates({ ...data, sessionId: this.sessionId }),
347
+ () =>
348
+ this.rpc.updateMuteStates({ muteStates, sessionId: this.sessionId }),
354
349
  this.abortController.signal,
355
350
  );
356
351
  };
@@ -415,7 +415,7 @@ export class StreamVideoClient {
415
415
  clientStore: this.writeableStateStore,
416
416
  });
417
417
  call.state.updateFromCallResponse(c.call);
418
- await call.applyDeviceConfig(false);
418
+ await call.applyDeviceConfig(c.call.settings, false);
419
419
  if (data.watch) {
420
420
  this.writeableStateStore.registerCall(call);
421
421
  }
@@ -0,0 +1,306 @@
1
+ import '../rtc/__tests__/mocks/webrtc.mocks';
2
+
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { Call } from '../Call';
5
+ import { Publisher } from '../rtc';
6
+ import { StreamClient } from '../coordinator/connection/client';
7
+ import { generateUUIDv4 } from '../coordinator/connection/utils';
8
+ import { PermissionsContext } from '../permissions';
9
+ import { OwnCapability } from '../gen/coordinator';
10
+ import { StreamVideoWriteableStateStore } from '../store';
11
+ import { TrackType } from '../gen/video/sfu/models/models';
12
+ import { StreamSfuClient } from '../StreamSfuClient';
13
+
14
+ describe('Publishing and Unpublishing tracks', () => {
15
+ let call: Call;
16
+
17
+ beforeEach(async () => {
18
+ call = new Call({
19
+ type: 'test',
20
+ id: generateUUIDv4(),
21
+ streamClient: new StreamClient('abc'),
22
+ clientStore: new StreamVideoWriteableStateStore(),
23
+ });
24
+
25
+ const ctx = new PermissionsContext();
26
+ ctx.setPermissions([
27
+ OwnCapability.SEND_AUDIO,
28
+ OwnCapability.SEND_VIDEO,
29
+ OwnCapability.SCREENSHARE,
30
+ ]);
31
+ // @ts-expect-error permissionsContext is private
32
+ call['permissionsContext'] = ctx;
33
+ });
34
+
35
+ describe('Validations', () => {
36
+ it('publishing is not allowed only when call is not joined', async () => {
37
+ const ms = new MediaStream();
38
+ const err = 'Call not joined yet.';
39
+ await expect(call.publish(ms, TrackType.VIDEO)).rejects.toThrowError(err);
40
+ await expect(call.publish(ms, TrackType.AUDIO)).rejects.toThrowError(err);
41
+ await expect(
42
+ call.publish(ms, TrackType.SCREEN_SHARE),
43
+ ).rejects.toThrowError(err);
44
+ });
45
+
46
+ it('publishing is not allowed when permissions are not set', async () => {
47
+ // @ts-expect-error sfuClient is private
48
+ call['sfuClient'] = {};
49
+
50
+ call['permissionsContext'].setPermissions([]);
51
+
52
+ const ms = new MediaStream();
53
+ await expect(call.publish(ms, TrackType.VIDEO)).rejects.toThrowError(
54
+ `No permission to publish VIDEO`,
55
+ );
56
+ await expect(call.publish(ms, TrackType.AUDIO)).rejects.toThrowError(
57
+ 'No permission to publish AUDIO',
58
+ );
59
+ await expect(
60
+ call.publish(ms, TrackType.SCREEN_SHARE),
61
+ ).rejects.toThrowError('No permission to publish SCREEN_SHARE');
62
+ });
63
+
64
+ it('publishing is not allowed when the publisher is not initialized', async () => {
65
+ // @ts-expect-error sfuClient is private
66
+ call['sfuClient'] = {};
67
+
68
+ const ms = new MediaStream();
69
+ await expect(call.publish(ms, TrackType.VIDEO)).rejects.toThrowError(
70
+ 'Publisher is not initialized',
71
+ );
72
+ });
73
+
74
+ it('publishing is not allowed when there are no tracks in the stream', async () => {
75
+ // @ts-expect-error sfuClient is private
76
+ call['sfuClient'] = {};
77
+ // @ts-expect-error publisher is private
78
+ call['publisher'] = {};
79
+
80
+ const ms = new MediaStream();
81
+ vi.spyOn(ms, 'getVideoTracks').mockReturnValue([]);
82
+ vi.spyOn(ms, 'getAudioTracks').mockReturnValue([]);
83
+
84
+ await expect(call.publish(ms, TrackType.VIDEO)).rejects.toThrowError(
85
+ 'There is no VIDEO track in the stream',
86
+ );
87
+ await expect(call.publish(ms, TrackType.AUDIO)).rejects.toThrowError(
88
+ 'There is no AUDIO track in the stream',
89
+ );
90
+ await expect(
91
+ call.publish(ms, TrackType.SCREEN_SHARE),
92
+ ).rejects.toThrowError('There is no SCREEN_SHARE track in the stream');
93
+ });
94
+
95
+ it('publishing is not allowed when the track ended', async () => {
96
+ // @ts-expect-error sfuClient is private
97
+ call['sfuClient'] = {};
98
+ // @ts-expect-error publisher is private
99
+ call['publisher'] = {};
100
+
101
+ const ms = new MediaStream();
102
+ const track = new MediaStreamTrack();
103
+ vi.spyOn(ms, 'getVideoTracks').mockReturnValue([track]);
104
+ vi.spyOn(track, 'readyState', 'get').mockReturnValue('ended');
105
+
106
+ await expect(call.publish(ms, TrackType.VIDEO)).rejects.toThrowError(
107
+ `Can't publish ended tracks.`,
108
+ );
109
+ });
110
+ });
111
+
112
+ describe('Publishing and Unpublishing', () => {
113
+ const sessionId = 'abc';
114
+ let publisher: Publisher;
115
+ let sfuClient: StreamSfuClient;
116
+
117
+ beforeEach(() => {
118
+ // @ts-expect-error partial data
119
+ call.state.updateOrAddParticipant(sessionId, {
120
+ sessionId,
121
+ publishedTracks: [],
122
+ });
123
+
124
+ sfuClient = vi.fn() as unknown as StreamSfuClient;
125
+ // @ts-expect-error sessionId is readonly
126
+ sfuClient['sessionId'] = sessionId;
127
+ sfuClient.updateMuteStates = vi.fn();
128
+
129
+ publisher = vi.fn() as unknown as Publisher;
130
+ publisher.publish = vi.fn();
131
+ publisher.stopTracks = vi.fn();
132
+
133
+ call['sfuClient'] = sfuClient;
134
+ call.publisher = publisher;
135
+ });
136
+
137
+ it('publish video stream', async () => {
138
+ const track = new MediaStreamTrack();
139
+ const mediaStream = new MediaStream();
140
+ vi.spyOn(mediaStream, 'getVideoTracks').mockReturnValue([track]);
141
+
142
+ await call.publish(mediaStream, TrackType.VIDEO);
143
+ expect(publisher.publish).toHaveBeenCalledWith(track, TrackType.VIDEO);
144
+ expect(call['trackPublishOrder']).toEqual([TrackType.VIDEO]);
145
+
146
+ expect(sfuClient.updateMuteStates).toHaveBeenCalledWith([
147
+ { trackType: TrackType.VIDEO, muted: false },
148
+ ]);
149
+
150
+ const participant = call.state.findParticipantBySessionId(sessionId);
151
+ expect(participant).toBeDefined();
152
+ expect(participant!.publishedTracks).toEqual([TrackType.VIDEO]);
153
+ expect(participant!.videoStream).toEqual(mediaStream);
154
+ });
155
+
156
+ it('publish audio stream', async () => {
157
+ const track = new MediaStreamTrack();
158
+ const mediaStream = new MediaStream();
159
+ vi.spyOn(mediaStream, 'getAudioTracks').mockReturnValue([track]);
160
+
161
+ await call.publish(mediaStream, TrackType.AUDIO);
162
+ expect(publisher.publish).toHaveBeenCalledWith(track, TrackType.AUDIO);
163
+ expect(call['trackPublishOrder']).toEqual([TrackType.AUDIO]);
164
+
165
+ expect(sfuClient.updateMuteStates).toHaveBeenCalledWith([
166
+ { trackType: TrackType.AUDIO, muted: false },
167
+ ]);
168
+
169
+ const participant = call.state.findParticipantBySessionId(sessionId);
170
+ expect(participant).toBeDefined();
171
+ expect(participant!.publishedTracks).toEqual([TrackType.AUDIO]);
172
+ expect(participant!.audioStream).toEqual(mediaStream);
173
+ });
174
+
175
+ it('publish screen share stream', async () => {
176
+ const track = new MediaStreamTrack();
177
+ const mediaStream = new MediaStream();
178
+ vi.spyOn(mediaStream, 'getVideoTracks').mockReturnValue([track]);
179
+
180
+ await call.publish(mediaStream, TrackType.SCREEN_SHARE);
181
+ expect(publisher.publish).toHaveBeenCalledWith(
182
+ track,
183
+ TrackType.SCREEN_SHARE,
184
+ );
185
+ expect(call['trackPublishOrder']).toEqual([TrackType.SCREEN_SHARE]);
186
+
187
+ expect(sfuClient.updateMuteStates).toHaveBeenCalledWith([
188
+ { trackType: TrackType.SCREEN_SHARE, muted: false },
189
+ ]);
190
+
191
+ const participant = call.state.findParticipantBySessionId(sessionId);
192
+ expect(participant).toBeDefined();
193
+ expect(participant!.publishedTracks).toEqual([TrackType.SCREEN_SHARE]);
194
+ expect(participant!.screenShareStream).toEqual(mediaStream);
195
+ });
196
+
197
+ it('publish screen share stream with audio', async () => {
198
+ const videoTrack = new MediaStreamTrack();
199
+ const audioTrack = new MediaStreamTrack();
200
+ const mediaStream = new MediaStream();
201
+ vi.spyOn(mediaStream, 'getVideoTracks').mockReturnValue([videoTrack]);
202
+ vi.spyOn(mediaStream, 'getAudioTracks').mockReturnValue([audioTrack]);
203
+
204
+ await call.publish(mediaStream, TrackType.SCREEN_SHARE);
205
+ expect(publisher.publish).toHaveBeenCalledWith(
206
+ videoTrack,
207
+ TrackType.SCREEN_SHARE,
208
+ );
209
+ expect(publisher.publish).toHaveBeenCalledWith(
210
+ audioTrack,
211
+ TrackType.SCREEN_SHARE_AUDIO,
212
+ );
213
+ expect(call['trackPublishOrder']).toEqual([
214
+ TrackType.SCREEN_SHARE,
215
+ TrackType.SCREEN_SHARE_AUDIO,
216
+ ]);
217
+
218
+ expect(sfuClient.updateMuteStates).toHaveBeenCalledWith([
219
+ { trackType: TrackType.SCREEN_SHARE, muted: false },
220
+ { trackType: TrackType.SCREEN_SHARE_AUDIO, muted: false },
221
+ ]);
222
+
223
+ const participant = call.state.findParticipantBySessionId(sessionId);
224
+ expect(participant).toBeDefined();
225
+ expect(participant!.publishedTracks).toEqual([
226
+ TrackType.SCREEN_SHARE,
227
+ TrackType.SCREEN_SHARE_AUDIO,
228
+ ]);
229
+ expect(participant!.screenShareStream).toEqual(mediaStream);
230
+ expect(participant!.screenShareAudioStream).toEqual(mediaStream);
231
+ });
232
+
233
+ it('unpublish video stream', async () => {
234
+ const mediaStream = new MediaStream();
235
+ call.state.updateParticipant(sessionId, {
236
+ publishedTracks: [TrackType.VIDEO, TrackType.AUDIO],
237
+ videoStream: mediaStream,
238
+ });
239
+ await call.stopPublish(TrackType.VIDEO);
240
+ expect(publisher.publish).not.toHaveBeenCalled();
241
+ expect(publisher.stopTracks).toHaveBeenCalledWith(TrackType.VIDEO);
242
+ const participant = call.state.findParticipantBySessionId(sessionId);
243
+ expect(participant!.publishedTracks).toEqual([TrackType.AUDIO]);
244
+ expect(participant!.videoStream).toBeUndefined();
245
+ });
246
+
247
+ it('unpublish audio stream', async () => {
248
+ const mediaStream = new MediaStream();
249
+ call.state.updateParticipant(sessionId, {
250
+ publishedTracks: [TrackType.VIDEO, TrackType.AUDIO],
251
+ audioStream: mediaStream,
252
+ });
253
+ await call.stopPublish(TrackType.AUDIO);
254
+ expect(publisher.publish).not.toHaveBeenCalled();
255
+ expect(publisher.stopTracks).toHaveBeenCalledWith(TrackType.AUDIO);
256
+ const participant = call.state.findParticipantBySessionId(sessionId);
257
+ expect(participant!.publishedTracks).toEqual([TrackType.VIDEO]);
258
+ expect(participant!.audioStream).toBeUndefined();
259
+ });
260
+
261
+ it('unpublish screen share stream', async () => {
262
+ const mediaStream = new MediaStream();
263
+ call.state.updateParticipant(sessionId, {
264
+ publishedTracks: [TrackType.SCREEN_SHARE, TrackType.SCREEN_SHARE_AUDIO],
265
+ screenShareStream: mediaStream,
266
+ screenShareAudioStream: mediaStream,
267
+ });
268
+ await call.stopPublish(
269
+ TrackType.SCREEN_SHARE,
270
+ TrackType.SCREEN_SHARE_AUDIO,
271
+ );
272
+ expect(publisher.publish).not.toHaveBeenCalled();
273
+ expect(publisher.stopTracks).toHaveBeenCalledWith(
274
+ TrackType.SCREEN_SHARE,
275
+ TrackType.SCREEN_SHARE_AUDIO,
276
+ );
277
+ const participant = call.state.findParticipantBySessionId(sessionId);
278
+ expect(participant!.publishedTracks).toEqual([]);
279
+ expect(participant!.screenShareStream).toBeUndefined();
280
+ expect(participant!.screenShareAudioStream).toBeUndefined();
281
+ });
282
+ });
283
+
284
+ describe('Deprecated methods', () => {
285
+ it('publishVideoStream', async () => {
286
+ const ms = new MediaStream();
287
+ call.publish = vi.fn();
288
+ await call.publishVideoStream(ms);
289
+ expect(call.publish).toHaveBeenCalledWith(ms, TrackType.VIDEO);
290
+ });
291
+
292
+ it('publishAudioStream', async () => {
293
+ const ms = new MediaStream();
294
+ call.publish = vi.fn();
295
+ await call.publishAudioStream(ms);
296
+ expect(call.publish).toHaveBeenCalledWith(ms, TrackType.AUDIO);
297
+ });
298
+
299
+ it('publishScreenShareStream', async () => {
300
+ const ms = new MediaStream();
301
+ call.publish = vi.fn();
302
+ await call.publishScreenShareStream(ms);
303
+ expect(call.publish).toHaveBeenCalledWith(ms, TrackType.SCREEN_SHARE);
304
+ });
305
+ });
306
+ });
@@ -3,9 +3,9 @@ import { Call } from '../Call';
3
3
  import { CameraDirection, CameraManagerState } from './CameraManagerState';
4
4
  import { InputMediaDeviceManager } from './InputMediaDeviceManager';
5
5
  import { getVideoDevices, getVideoStream } from './devices';
6
+ import { OwnCapability, VideoSettingsResponse } from '../gen/coordinator';
6
7
  import { TrackType } from '../gen/video/sfu/models/models';
7
- import { PreferredCodec } from '../types';
8
- import { isMobile } from '../compatibility';
8
+ import { isMobile } from '../helpers/compatibility';
9
9
  import { isReactNative } from '../helpers/platforms';
10
10
 
11
11
  export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
@@ -86,14 +86,39 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
86
86
  }
87
87
 
88
88
  /**
89
- * Sets the preferred codec for encoding the video.
89
+ * Applies the video settings to the camera.
90
90
  *
91
- * @internal internal use only, not part of the public API.
92
- * @deprecated use {@link call.updatePublishOptions} instead.
93
- * @param codec the codec to use for encoding the video.
91
+ * @param settings the video settings to apply.
92
+ * @param publish whether to publish the stream after applying the settings.
94
93
  */
95
- setPreferredCodec(codec: PreferredCodec | undefined) {
96
- this.call.updatePublishOptions({ preferredCodec: codec });
94
+ async apply(settings: VideoSettingsResponse, publish: boolean) {
95
+ const hasPublishedVideo = !!this.call.state.localParticipant?.videoStream;
96
+ const hasPermission = this.call.permissionsContext.hasPermission(
97
+ OwnCapability.SEND_AUDIO,
98
+ );
99
+ if (hasPublishedVideo || !hasPermission) return;
100
+
101
+ // Wait for any in progress camera operation
102
+ await this.statusChangeSettled();
103
+
104
+ const { target_resolution, camera_facing, camera_default_on } = settings;
105
+ await this.selectTargetResolution(target_resolution);
106
+
107
+ // Set camera direction if it's not yet set
108
+ if (!this.state.direction && !this.state.selectedDevice) {
109
+ this.state.setDirection(camera_facing === 'front' ? 'front' : 'back');
110
+ }
111
+
112
+ if (!publish) return;
113
+
114
+ const { mediaStream } = this.state;
115
+ if (this.enabled && mediaStream) {
116
+ // The camera is already enabled (e.g. lobby screen). Publish the stream
117
+ await this.publishStream(mediaStream);
118
+ } else if (this.state.status === undefined && camera_default_on) {
119
+ // Start camera if backend config specifies, and there is no local setting
120
+ await this.enable();
121
+ }
97
122
  }
98
123
 
99
124
  protected getDevices(): Observable<MediaDeviceInfo[]> {
@@ -118,12 +143,4 @@ export class CameraManager extends InputMediaDeviceManager<CameraManagerState> {
118
143
  }
119
144
  return getVideoStream(constraints);
120
145
  }
121
-
122
- protected publishStream(stream: MediaStream): Promise<void> {
123
- return this.call.publishVideoStream(stream);
124
- }
125
-
126
- protected stopPublishStream(stopTracks: boolean): Promise<void> {
127
- return this.call.stopPublish(TrackType.VIDEO, stopTracks);
128
- }
129
146
  }
@@ -241,43 +241,45 @@ export abstract class InputMediaDeviceManager<
241
241
 
242
242
  protected abstract getStream(constraints: C): Promise<MediaStream>;
243
243
 
244
- protected abstract publishStream(stream: MediaStream): Promise<void>;
244
+ protected publishStream(stream: MediaStream): Promise<void> {
245
+ return this.call.publish(stream, this.trackType);
246
+ }
245
247
 
246
- protected abstract stopPublishStream(stopTracks: boolean): Promise<void>;
248
+ protected stopPublishStream(): Promise<void> {
249
+ return this.call.stopPublish(this.trackType);
250
+ }
247
251
 
248
252
  protected getTracks(): MediaStreamTrack[] {
249
253
  return this.state.mediaStream?.getTracks() ?? [];
250
254
  }
251
255
 
252
256
  protected async muteStream(stopTracks: boolean = true) {
253
- if (!this.state.mediaStream) return;
257
+ const mediaStream = this.state.mediaStream;
258
+ if (!mediaStream) return;
254
259
  this.logger('debug', `${stopTracks ? 'Stopping' : 'Disabling'} stream`);
255
260
  if (this.call.state.callingState === CallingState.JOINED) {
256
- await this.stopPublishStream(stopTracks);
261
+ await this.stopPublishStream();
257
262
  }
258
263
  this.muteLocalStream(stopTracks);
259
264
  const allEnded = this.getTracks().every((t) => t.readyState === 'ended');
260
265
  if (allEnded) {
261
- if (
262
- this.state.mediaStream &&
263
- // @ts-expect-error release() is present in react-native-webrtc
264
- typeof this.state.mediaStream.release === 'function'
265
- ) {
266
+ // @ts-expect-error release() is present in react-native-webrtc
267
+ if (typeof mediaStream.release === 'function') {
266
268
  // @ts-expect-error called to dispose the stream in RN
267
- this.state.mediaStream.release();
269
+ mediaStream.release();
268
270
  }
269
271
  this.state.setMediaStream(undefined, undefined);
270
272
  this.filters.forEach((entry) => entry.stop?.());
271
273
  }
272
274
  }
273
275
 
274
- private muteTracks() {
276
+ private disableTracks() {
275
277
  this.getTracks().forEach((track) => {
276
278
  if (track.enabled) track.enabled = false;
277
279
  });
278
280
  }
279
281
 
280
- private unmuteTracks() {
282
+ private enableTracks() {
281
283
  this.getTracks().forEach((track) => {
282
284
  if (!track.enabled) track.enabled = true;
283
285
  });
@@ -296,7 +298,7 @@ export abstract class InputMediaDeviceManager<
296
298
  if (stopTracks) {
297
299
  this.stopTracks();
298
300
  } else {
299
- this.muteTracks();
301
+ this.disableTracks();
300
302
  }
301
303
  }
302
304
 
@@ -309,7 +311,7 @@ export abstract class InputMediaDeviceManager<
309
311
  this.getTracks().every((t) => t.readyState === 'live')
310
312
  ) {
311
313
  stream = this.state.mediaStream;
312
- this.unmuteTracks();
314
+ this.enableTracks();
313
315
  } else {
314
316
  const defaultConstraints = this.state.defaultConstraints;
315
317
  const constraints: MediaTrackConstraints = {
@@ -414,11 +416,22 @@ export abstract class InputMediaDeviceManager<
414
416
  await this.disable();
415
417
  }
416
418
  };
417
- this.getTracks().forEach((track) => {
419
+ const createTrackMuteHandler = (muted: boolean) => () => {
420
+ this.call.notifyTrackMuteState(muted, this.trackType).catch((err) => {
421
+ this.logger('warn', 'Error while notifying track mute state', err);
422
+ });
423
+ };
424
+ stream.getTracks().forEach((track) => {
425
+ const muteHandler = createTrackMuteHandler(true);
426
+ const unmuteHandler = createTrackMuteHandler(false);
427
+ track.addEventListener('mute', muteHandler);
428
+ track.addEventListener('unmute', unmuteHandler);
418
429
  track.addEventListener('ended', handleTrackEnded);
419
- this.subscriptions.push(() =>
420
- track.removeEventListener('ended', handleTrackEnded),
421
- );
430
+ this.subscriptions.push(() => {
431
+ track.removeEventListener('mute', muteHandler);
432
+ track.removeEventListener('unmute', unmuteHandler);
433
+ track.removeEventListener('ended', handleTrackEnded);
434
+ });
422
435
  });
423
436
  }
424
437
  }
@@ -447,11 +460,8 @@ export abstract class InputMediaDeviceManager<
447
460
 
448
461
  let isDeviceDisconnected = false;
449
462
  let isDeviceReplaced = false;
450
- const currentDevice = this.findDeviceInList(
451
- currentDevices,
452
- deviceId,
453
- );
454
- const prevDevice = this.findDeviceInList(prevDevices, deviceId);
463
+ const currentDevice = this.findDevice(currentDevices, deviceId);
464
+ const prevDevice = this.findDevice(prevDevices, deviceId);
455
465
  if (!currentDevice && prevDevice) {
456
466
  isDeviceDisconnected = true;
457
467
  } else if (
@@ -490,9 +500,8 @@ export abstract class InputMediaDeviceManager<
490
500
  );
491
501
  }
492
502
 
493
- private findDeviceInList(devices: MediaDeviceInfo[], deviceId: string) {
494
- return devices.find(
495
- (d) => d.deviceId === deviceId && d.kind === this.mediaDeviceKind,
496
- );
503
+ private findDevice(devices: MediaDeviceInfo[], deviceId: string) {
504
+ const kind = this.mediaDeviceKind;
505
+ return devices.find((d) => d.deviceId === deviceId && d.kind === kind);
497
506
  }
498
507
  }
@@ -9,6 +9,7 @@ import { TrackType } from '../gen/video/sfu/models/models';
9
9
  import { createSoundDetector } from '../helpers/sound-detector';
10
10
  import { isReactNative } from '../helpers/platforms';
11
11
  import {
12
+ AudioSettingsResponse,
12
13
  NoiseCancellationSettingsModeEnum,
13
14
  OwnCapability,
14
15
  } from '../gen/coordinator';
@@ -201,6 +202,34 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
201
202
  await this.stopSpeakingWhileMutedDetection();
202
203
  }
203
204
 
205
+ /**
206
+ * Applies the audio settings to the microphone.
207
+ * @param settings the audio settings to apply.
208
+ * @param publish whether to publish the stream after applying the settings.
209
+ */
210
+ async apply(settings: AudioSettingsResponse, publish: boolean) {
211
+ if (!publish) return;
212
+
213
+ const hasPublishedAudio = !!this.call.state.localParticipant?.audioStream;
214
+ const hasPermission = this.call.permissionsContext.hasPermission(
215
+ OwnCapability.SEND_AUDIO,
216
+ );
217
+ if (hasPublishedAudio || !hasPermission) return;
218
+
219
+ // Wait for any in progress mic operation
220
+ await this.statusChangeSettled();
221
+
222
+ // Publish media stream that was set before we joined
223
+ const { mediaStream } = this.state;
224
+ if (this.enabled && mediaStream) {
225
+ // The mic is already enabled (e.g. lobby screen). Publish the stream
226
+ await this.publishStream(mediaStream);
227
+ } else if (this.state.status === undefined && settings.mic_default_on) {
228
+ // Start mic if backend config specifies, and there is no local setting
229
+ await this.enable();
230
+ }
231
+ }
232
+
204
233
  protected getDevices(): Observable<MediaDeviceInfo[]> {
205
234
  return getAudioDevices();
206
235
  }
@@ -211,14 +240,6 @@ export class MicrophoneManager extends InputMediaDeviceManager<MicrophoneManager
211
240
  return getAudioStream(constraints);
212
241
  }
213
242
 
214
- protected publishStream(stream: MediaStream): Promise<void> {
215
- return this.call.publishAudioStream(stream);
216
- }
217
-
218
- protected stopPublishStream(stopTracks: boolean): Promise<void> {
219
- return this.call.stopPublish(TrackType.AUDIO, stopTracks);
220
- }
221
-
222
243
  private async startSpeakingWhileMutedDetection(deviceId?: string) {
223
244
  await withoutConcurrency(this.soundDetectorConcurrencyTag, async () => {
224
245
  await this.stopSpeakingWhileMutedDetection();
@@ -46,7 +46,7 @@ export class ScreenShareManager extends InputMediaDeviceManager<
46
46
  async disableScreenShareAudio(): Promise<void> {
47
47
  this.state.setAudioEnabled(false);
48
48
  if (this.call.publisher?.isPublishing(TrackType.SCREEN_SHARE_AUDIO)) {
49
- await this.call.stopPublish(TrackType.SCREEN_SHARE_AUDIO, true);
49
+ await this.call.stopPublish(TrackType.SCREEN_SHARE_AUDIO);
50
50
  }
51
51
  }
52
52
 
@@ -79,13 +79,11 @@ export class ScreenShareManager extends InputMediaDeviceManager<
79
79
  return getScreenShareStream(constraints);
80
80
  }
81
81
 
82
- protected publishStream(stream: MediaStream): Promise<void> {
83
- return this.call.publishScreenShareStream(stream);
84
- }
85
-
86
- protected async stopPublishStream(stopTracks: boolean): Promise<void> {
87
- await this.call.stopPublish(TrackType.SCREEN_SHARE, stopTracks);
88
- await this.call.stopPublish(TrackType.SCREEN_SHARE_AUDIO, stopTracks);
82
+ protected async stopPublishStream(): Promise<void> {
83
+ return this.call.stopPublish(
84
+ TrackType.SCREEN_SHARE,
85
+ TrackType.SCREEN_SHARE_AUDIO,
86
+ );
89
87
  }
90
88
 
91
89
  /**