@stream-io/video-client 1.8.3 → 1.9.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 (38) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.browser.es.js +434 -449
  3. package/dist/index.browser.es.js.map +1 -1
  4. package/dist/index.cjs.js +434 -449
  5. package/dist/index.cjs.js.map +1 -1
  6. package/dist/index.es.js +434 -449
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/src/Call.d.ts +10 -12
  9. package/dist/src/compatibility.d.ts +7 -0
  10. package/dist/src/devices/CameraManager.d.ts +2 -22
  11. package/dist/src/events/internal.d.ts +0 -4
  12. package/dist/src/gen/video/sfu/event/events.d.ts +2 -71
  13. package/dist/src/helpers/sdp-munging.d.ts +8 -0
  14. package/dist/src/rtc/Publisher.d.ts +18 -23
  15. package/dist/src/rtc/bitrateLookup.d.ts +2 -0
  16. package/dist/src/rtc/codecs.d.ts +9 -2
  17. package/dist/src/rtc/videoLayers.d.ts +31 -4
  18. package/dist/src/types.d.ts +30 -2
  19. package/package.json +1 -1
  20. package/src/Call.ts +21 -38
  21. package/src/compatibility.ts +7 -0
  22. package/src/devices/CameraManager.ts +18 -47
  23. package/src/devices/ScreenShareManager.ts +1 -3
  24. package/src/devices/__tests__/CameraManager.test.ts +7 -15
  25. package/src/devices/__tests__/ScreenShareManager.test.ts +0 -14
  26. package/src/events/callEventHandlers.ts +0 -2
  27. package/src/events/internal.ts +0 -16
  28. package/src/gen/video/sfu/event/events.ts +8 -120
  29. package/src/helpers/sdp-munging.ts +38 -15
  30. package/src/rtc/Publisher.ts +211 -317
  31. package/src/rtc/__tests__/Publisher.test.ts +196 -7
  32. package/src/rtc/__tests__/bitrateLookup.test.ts +12 -0
  33. package/src/rtc/__tests__/mocks/webrtc.mocks.ts +2 -0
  34. package/src/rtc/__tests__/videoLayers.test.ts +51 -36
  35. package/src/rtc/bitrateLookup.ts +61 -0
  36. package/src/rtc/codecs.ts +56 -9
  37. package/src/rtc/videoLayers.ts +74 -23
  38. package/src/types.ts +30 -2
@@ -17,7 +17,8 @@ vi.mock('../../StreamSfuClient', () => {
17
17
  };
18
18
  });
19
19
 
20
- vi.mock('../codecs', () => {
20
+ vi.mock('../codecs', async () => {
21
+ const codecs = await vi.importActual('../codecs');
21
22
  return {
22
23
  getPreferredCodecs: vi.fn((): RTCRtpCodecCapability[] => [
23
24
  {
@@ -27,6 +28,8 @@ vi.mock('../codecs', () => {
27
28
  sdpFmtpLine: 'profile-level-id=42e01f',
28
29
  },
29
30
  ]),
31
+ getOptimalVideoCodec: codecs.getOptimalVideoCodec,
32
+ isSvcCodec: codecs.isSvcCodec,
30
33
  };
31
34
  });
32
35
 
@@ -136,11 +139,7 @@ describe('Publisher', () => {
136
139
  vi.spyOn(transceiver.sender, 'track', 'get').mockReturnValue(newTrack);
137
140
 
138
141
  expect(track.stop).toHaveBeenCalled();
139
- expect(track.removeEventListener).toHaveBeenCalledWith(
140
- 'ended',
141
- expect.any(Function),
142
- );
143
- expect(newTrack.addEventListener).toHaveBeenCalledWith(
142
+ expect(newTrack.addEventListener).not.toHaveBeenCalledWith(
144
143
  'ended',
145
144
  expect.any(Function),
146
145
  );
@@ -154,7 +153,7 @@ describe('Publisher', () => {
154
153
  );
155
154
  });
156
155
 
157
- it('can publish and un-pubish with just enabling and disabling tracks', async () => {
156
+ it('can publish and un-publish with just enabling and disabling tracks', async () => {
158
157
  const mediaStream = new MediaStream();
159
158
  const track = new MediaStreamTrack();
160
159
  mediaStream.addTrack(track);
@@ -277,4 +276,194 @@ describe('Publisher', () => {
277
276
  expect(publisher.restartIce).toHaveBeenCalled();
278
277
  });
279
278
  });
279
+
280
+ describe('changePublishQuality', () => {
281
+ it('can dynamically activate/deactivate simulcast layers', async () => {
282
+ const transceiver = new RTCRtpTransceiver();
283
+ const setParametersSpy = vi
284
+ .spyOn(transceiver.sender, 'setParameters')
285
+ .mockResolvedValue();
286
+ const getParametersSpy = vi
287
+ .spyOn(transceiver.sender, 'getParameters')
288
+ .mockReturnValue({
289
+ codecs: [
290
+ // @ts-expect-error incomplete data
291
+ { mimeType: 'video/VP8' },
292
+ // @ts-expect-error incomplete data
293
+ { mimeType: 'video/VP9' },
294
+ // @ts-expect-error incomplete data
295
+ { mimeType: 'video/H264' },
296
+ // @ts-expect-error incomplete data
297
+ { mimeType: 'video/AV1' },
298
+ ],
299
+ encodings: [
300
+ { rid: 'q', active: true },
301
+ { rid: 'h', active: true },
302
+ { rid: 'f', active: true },
303
+ ],
304
+ });
305
+
306
+ // inject the transceiver
307
+ publisher['transceiverCache'].set(TrackType.VIDEO, transceiver);
308
+
309
+ await publisher['changePublishQuality']([
310
+ {
311
+ name: 'q',
312
+ active: true,
313
+ maxBitrate: 100,
314
+ scaleResolutionDownBy: 4,
315
+ maxFramerate: 30,
316
+ scalabilityMode: '',
317
+ },
318
+ {
319
+ name: 'h',
320
+ active: false,
321
+ maxBitrate: 150,
322
+ scaleResolutionDownBy: 2,
323
+ maxFramerate: 30,
324
+ scalabilityMode: '',
325
+ },
326
+ {
327
+ name: 'f',
328
+ active: true,
329
+ maxBitrate: 200,
330
+ scaleResolutionDownBy: 1,
331
+ maxFramerate: 30,
332
+ scalabilityMode: '',
333
+ },
334
+ ]);
335
+
336
+ expect(getParametersSpy).toHaveBeenCalled();
337
+ expect(setParametersSpy).toHaveBeenCalled();
338
+ expect(setParametersSpy.mock.calls[0][0].encodings).toEqual([
339
+ {
340
+ rid: 'q',
341
+ active: true,
342
+ maxBitrate: 100,
343
+ scaleResolutionDownBy: 4,
344
+ maxFramerate: 30,
345
+ },
346
+ {
347
+ rid: 'h',
348
+ active: false,
349
+ maxBitrate: 150,
350
+ scaleResolutionDownBy: 2,
351
+ maxFramerate: 30,
352
+ },
353
+ {
354
+ rid: 'f',
355
+ active: true,
356
+ maxBitrate: 200,
357
+ scaleResolutionDownBy: 1,
358
+ maxFramerate: 30,
359
+ },
360
+ ]);
361
+ });
362
+
363
+ it('can dynamically update scalability mode in SVC', async () => {
364
+ const transceiver = new RTCRtpTransceiver();
365
+ const setParametersSpy = vi
366
+ .spyOn(transceiver.sender, 'setParameters')
367
+ .mockResolvedValue();
368
+ const getParametersSpy = vi
369
+ .spyOn(transceiver.sender, 'getParameters')
370
+ .mockReturnValue({
371
+ codecs: [
372
+ // @ts-expect-error incomplete data
373
+ { mimeType: 'video/VP9' },
374
+ // @ts-expect-error incomplete data
375
+ { mimeType: 'video/AV1' },
376
+ // @ts-expect-error incomplete data
377
+ { mimeType: 'video/VP8' },
378
+ // @ts-expect-error incomplete data
379
+ { mimeType: 'video/H264' },
380
+ ],
381
+ encodings: [
382
+ {
383
+ rid: 'q',
384
+ active: true,
385
+ maxBitrate: 100,
386
+ // @ts-expect-error not in the standard lib yet
387
+ scalabilityMode: 'L3T3_KEY',
388
+ },
389
+ ],
390
+ });
391
+
392
+ // inject the transceiver
393
+ publisher['transceiverCache'].set(TrackType.VIDEO, transceiver);
394
+
395
+ await publisher['changePublishQuality']([
396
+ {
397
+ name: 'q',
398
+ active: true,
399
+ maxBitrate: 50,
400
+ scaleResolutionDownBy: 1,
401
+ maxFramerate: 30,
402
+ scalabilityMode: 'L1T3',
403
+ },
404
+ ]);
405
+
406
+ expect(getParametersSpy).toHaveBeenCalled();
407
+ expect(setParametersSpy).toHaveBeenCalled();
408
+ expect(setParametersSpy.mock.calls[0][0].encodings).toEqual([
409
+ {
410
+ rid: 'q',
411
+ active: true,
412
+ maxBitrate: 50,
413
+ scaleResolutionDownBy: 1,
414
+ maxFramerate: 30,
415
+ scalabilityMode: 'L1T3',
416
+ },
417
+ ]);
418
+ });
419
+
420
+ it('supports empty rid in SVC', async () => {
421
+ const transceiver = new RTCRtpTransceiver();
422
+ const setParametersSpy = vi
423
+ .spyOn(transceiver.sender, 'setParameters')
424
+ .mockResolvedValue();
425
+ const getParametersSpy = vi
426
+ .spyOn(transceiver.sender, 'getParameters')
427
+ .mockReturnValue({
428
+ codecs: [
429
+ // @ts-expect-error incomplete data
430
+ { mimeType: 'video/VP9' },
431
+ ],
432
+ encodings: [
433
+ {
434
+ rid: undefined, // empty rid
435
+ active: true,
436
+ // @ts-expect-error not in the standard lib yet
437
+ scalabilityMode: 'L3T3_KEY',
438
+ },
439
+ ],
440
+ });
441
+
442
+ // inject the transceiver
443
+ publisher['transceiverCache'].set(TrackType.VIDEO, transceiver);
444
+
445
+ await publisher['changePublishQuality']([
446
+ {
447
+ name: 'q',
448
+ active: true,
449
+ maxBitrate: 50,
450
+ scaleResolutionDownBy: 1,
451
+ maxFramerate: 30,
452
+ scalabilityMode: 'L1T3',
453
+ },
454
+ ]);
455
+
456
+ expect(getParametersSpy).toHaveBeenCalled();
457
+ expect(setParametersSpy).toHaveBeenCalled();
458
+ expect(setParametersSpy.mock.calls[0][0].encodings).toEqual([
459
+ {
460
+ active: true,
461
+ maxBitrate: 50,
462
+ scaleResolutionDownBy: 1,
463
+ maxFramerate: 30,
464
+ scalabilityMode: 'L1T3',
465
+ },
466
+ ]);
467
+ });
468
+ });
280
469
  });
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getOptimalBitrate } from '../bitrateLookup';
3
+
4
+ describe('bitrateLookup', () => {
5
+ it('should return optimal bitrate', () => {
6
+ expect(getOptimalBitrate('vp9', 720)).toBe(1_250_000);
7
+ });
8
+
9
+ it('should return nearest bitrate for exotic dimensions', () => {
10
+ expect(getOptimalBitrate('vp9', 1000)).toBe(1_500_000);
11
+ });
12
+ });
@@ -45,6 +45,8 @@ const RTCRtpTransceiverMock = vi.fn((): Partial<RTCRtpTransceiver> => {
45
45
  sender: {
46
46
  track: null,
47
47
  replaceTrack: vi.fn(),
48
+ getParameters: vi.fn(),
49
+ setParameters: vi.fn(),
48
50
  },
49
51
  setCodecPreferences: vi.fn(),
50
52
  };
@@ -4,7 +4,11 @@ import {
4
4
  findOptimalScreenSharingLayers,
5
5
  findOptimalVideoLayers,
6
6
  getComputedMaxBitrate,
7
+ OptimalVideoLayer,
8
+ ridToVideoQuality,
9
+ toSvcEncodings,
7
10
  } from '../videoLayers';
11
+ import { VideoQuality } from '../../gen/video/sfu/models/models';
8
12
 
9
13
  describe('videoLayers', () => {
10
14
  it('should find optimal screen sharing layers', () => {
@@ -135,15 +139,51 @@ describe('videoLayers', () => {
135
139
  expect(layers[2].rid).toBe('f');
136
140
  });
137
141
 
142
+ it('should announce only one layer for SVC codecs', () => {
143
+ const track = new MediaStreamTrack();
144
+ vi.spyOn(track, 'getSettings').mockReturnValue({
145
+ width: 1280,
146
+ height: 720,
147
+ });
148
+ const layers = findOptimalVideoLayers(track, undefined, 'vp9', {
149
+ preferredCodec: 'vp9',
150
+ scalabilityMode: 'L3T3',
151
+ });
152
+ expect(layers.length).toBe(3);
153
+ expect(layers[0].scalabilityMode).toBe('L3T3');
154
+ expect(layers[0].rid).toBe('q');
155
+ expect(layers[1].rid).toBe('h');
156
+ expect(layers[2].rid).toBe('f');
157
+ });
158
+
159
+ it('should map rids to VideoQuality', () => {
160
+ expect(ridToVideoQuality('q')).toBe(VideoQuality.LOW_UNSPECIFIED);
161
+ expect(ridToVideoQuality('h')).toBe(VideoQuality.MID);
162
+ expect(ridToVideoQuality('f')).toBe(VideoQuality.HIGH);
163
+ expect(ridToVideoQuality('')).toBe(VideoQuality.HIGH);
164
+ });
165
+
166
+ it('should map OptimalVideoLayer to SVC encodings', () => {
167
+ const layers: Array<Partial<OptimalVideoLayer>> = [
168
+ { rid: 'f', width: 1920, height: 1080, maxBitrate: 3000000 },
169
+ { rid: 'h', width: 960, height: 540, maxBitrate: 750000 },
170
+ { rid: 'q', width: 480, height: 270, maxBitrate: 187500 },
171
+ ];
172
+
173
+ const svcLayers = toSvcEncodings(layers as OptimalVideoLayer[]);
174
+ expect(svcLayers.length).toBe(1);
175
+ expect(svcLayers[0]).toEqual({
176
+ rid: 'q',
177
+ width: 1920,
178
+ height: 1080,
179
+ maxBitrate: 3000000,
180
+ });
181
+ });
182
+
138
183
  describe('getComputedMaxBitrate', () => {
139
184
  it('should scale target bitrate down if resolution is smaller than target resolution', () => {
140
185
  const targetResolution = { width: 1920, height: 1080, bitrate: 3000000 };
141
- const scaledBitrate = getComputedMaxBitrate(
142
- targetResolution,
143
- 1280,
144
- 720,
145
- undefined,
146
- );
186
+ const scaledBitrate = getComputedMaxBitrate(targetResolution, 1280, 720);
147
187
  expect(scaledBitrate).toBe(1333333);
148
188
  });
149
189
 
@@ -153,12 +193,7 @@ describe('videoLayers', () => {
153
193
  const targetBitrates = ['f', 'h', 'q'].map((rid) => {
154
194
  const width = targetResolution.width / downscaleFactor;
155
195
  const height = targetResolution.height / downscaleFactor;
156
- const bitrate = getComputedMaxBitrate(
157
- targetResolution,
158
- width,
159
- height,
160
- undefined,
161
- );
196
+ const bitrate = getComputedMaxBitrate(targetResolution, width, height);
162
197
  downscaleFactor *= 2;
163
198
  return {
164
199
  rid,
@@ -176,45 +211,25 @@ describe('videoLayers', () => {
176
211
 
177
212
  it('should not scale target bitrate if resolution is larger than target resolution', () => {
178
213
  const targetResolution = { width: 1280, height: 720, bitrate: 1000000 };
179
- const scaledBitrate = getComputedMaxBitrate(
180
- targetResolution,
181
- 2560,
182
- 1440,
183
- undefined,
184
- );
214
+ const scaledBitrate = getComputedMaxBitrate(targetResolution, 2560, 1440);
185
215
  expect(scaledBitrate).toBe(1000000);
186
216
  });
187
217
 
188
218
  it('should not scale target bitrate if resolution is equal to target resolution', () => {
189
219
  const targetResolution = { width: 1280, height: 720, bitrate: 1000000 };
190
- const scaledBitrate = getComputedMaxBitrate(
191
- targetResolution,
192
- 1280,
193
- 720,
194
- undefined,
195
- );
220
+ const scaledBitrate = getComputedMaxBitrate(targetResolution, 1280, 720);
196
221
  expect(scaledBitrate).toBe(1000000);
197
222
  });
198
223
 
199
224
  it('should handle 0 width and height', () => {
200
225
  const targetResolution = { width: 1280, height: 720, bitrate: 1000000 };
201
- const scaledBitrate = getComputedMaxBitrate(
202
- targetResolution,
203
- 0,
204
- 0,
205
- undefined,
206
- );
226
+ const scaledBitrate = getComputedMaxBitrate(targetResolution, 0, 0);
207
227
  expect(scaledBitrate).toBe(0);
208
228
  });
209
229
 
210
230
  it('should handle 4k target resolution', () => {
211
231
  const targetResolution = { width: 3840, height: 2160, bitrate: 15000000 };
212
- const scaledBitrate = getComputedMaxBitrate(
213
- targetResolution,
214
- 1280,
215
- 720,
216
- undefined,
217
- );
232
+ const scaledBitrate = getComputedMaxBitrate(targetResolution, 1280, 720);
218
233
  expect(scaledBitrate).toBe(1666667);
219
234
  });
220
235
  });
@@ -0,0 +1,61 @@
1
+ import { PreferredCodec } from '../types';
2
+
3
+ const bitrateLookupTable: Record<
4
+ PreferredCodec,
5
+ Record<number | 'default', number | undefined> | undefined
6
+ > = {
7
+ h264: {
8
+ 2160: 5_000_000,
9
+ 1440: 3_500_000,
10
+ 1080: 2_750_000,
11
+ 720: 1_250_000,
12
+ 540: 750_000,
13
+ 360: 400_000,
14
+ default: 1_250_000,
15
+ },
16
+ vp8: {
17
+ 2160: 5_000_000,
18
+ 1440: 2_750_000,
19
+ 1080: 2_000_000,
20
+ 720: 1_250_000,
21
+ 540: 600_000,
22
+ 360: 350_000,
23
+ default: 1_250_000,
24
+ },
25
+ vp9: {
26
+ 2160: 3_000_000,
27
+ 1440: 2_000_000,
28
+ 1080: 1_500_000,
29
+ 720: 1_250_000,
30
+ 540: 500_000,
31
+ 360: 275_000,
32
+ default: 1_250_000,
33
+ },
34
+ av1: {
35
+ 2160: 2_000_000,
36
+ 1440: 1_550_000,
37
+ 1080: 1_000_000,
38
+ 720: 600_000,
39
+ 540: 350_000,
40
+ 360: 200_000,
41
+ default: 600_000,
42
+ },
43
+ };
44
+
45
+ export const getOptimalBitrate = (
46
+ codec: PreferredCodec,
47
+ frameHeight: number,
48
+ ): number => {
49
+ const codecLookup = bitrateLookupTable[codec];
50
+ if (!codecLookup) throw new Error(`Unknown codec: ${codec}`);
51
+
52
+ let bitrate = codecLookup[frameHeight];
53
+ if (!bitrate) {
54
+ const keys = Object.keys(codecLookup).map(Number);
55
+ const nearest = keys.reduce((a, b) =>
56
+ Math.abs(b - frameHeight) < Math.abs(a - frameHeight) ? b : a,
57
+ );
58
+ bitrate = codecLookup[nearest];
59
+ }
60
+ return bitrate ?? codecLookup.default!;
61
+ };
package/src/rtc/codecs.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import { getOSInfo } from '../client-details';
2
+ import { isReactNative } from '../helpers/platforms';
3
+ import { isFirefox, isSafari } from '../helpers/browsers';
4
+ import type { PreferredCodec } from '../types';
2
5
 
3
6
  /**
4
7
  * Returns back a list of sorted codecs, with the preferred codec first.
@@ -92,14 +95,58 @@ export const getGenericSdp = async (direction: RTCRtpTransceiverDirection) => {
92
95
  };
93
96
 
94
97
  /**
95
- * Returns the optimal codec for RN.
98
+ * Returns the optimal video codec for the device.
96
99
  */
97
- export const getRNOptimalCodec = () => {
98
- const osName = getOSInfo()?.name.toLowerCase();
99
- // in ipads it was noticed that if vp8 codec is used
100
- // then the bytes sent is 0 in the outbound-rtp
101
- // so we are forcing h264 codec for ipads
102
- if (osName === 'ipados') return 'h264';
103
- if (osName === 'android') return 'vp8';
104
- return undefined;
100
+ export const getOptimalVideoCodec = (
101
+ preferredCodec: PreferredCodec | undefined,
102
+ ): PreferredCodec => {
103
+ if (isReactNative()) {
104
+ const os = getOSInfo()?.name.toLowerCase();
105
+ if (os === 'android') return preferredOr(preferredCodec, 'vp8');
106
+ if (os === 'ios' || os === 'ipados') return 'h264';
107
+ return preferredOr(preferredCodec, 'h264');
108
+ }
109
+ if (isSafari()) return 'h264';
110
+ if (isFirefox()) return 'vp8';
111
+ return preferredOr(preferredCodec, 'vp8');
112
+ };
113
+
114
+ /**
115
+ * Determines if the platform supports the preferred codec.
116
+ * If not, it returns the fallback codec.
117
+ */
118
+ const preferredOr = (
119
+ codec: PreferredCodec | undefined,
120
+ fallback: PreferredCodec,
121
+ ): PreferredCodec => {
122
+ if (!codec) return fallback;
123
+ if (!('getCapabilities' in RTCRtpSender)) return fallback;
124
+ const capabilities = RTCRtpSender.getCapabilities('video');
125
+ if (!capabilities) return fallback;
126
+
127
+ // Safari and Firefox do not have a good support encoding to SVC codecs,
128
+ // so we disable it for them.
129
+ if (isSvcCodec(codec) && (isSafari() || isFirefox())) return fallback;
130
+
131
+ const { codecs } = capabilities;
132
+ const codecMimeType = `video/${codec}`.toLowerCase();
133
+ return codecs.some((c) => c.mimeType.toLowerCase() === codecMimeType)
134
+ ? codec
135
+ : fallback;
136
+ };
137
+
138
+ /**
139
+ * Returns whether the codec is an SVC codec.
140
+ *
141
+ * @param codecOrMimeType the codec to check.
142
+ */
143
+ export const isSvcCodec = (codecOrMimeType: string | undefined) => {
144
+ if (!codecOrMimeType) return false;
145
+ codecOrMimeType = codecOrMimeType.toLowerCase();
146
+ return (
147
+ codecOrMimeType === 'vp9' ||
148
+ codecOrMimeType === 'av1' ||
149
+ codecOrMimeType === 'video/vp9' ||
150
+ codecOrMimeType === 'video/av1'
151
+ );
105
152
  };
@@ -1,9 +1,14 @@
1
- import { PublishOptions } from '../types';
1
+ import { PreferredCodec, PublishOptions } from '../types';
2
2
  import { TargetResolutionResponse } from '../gen/shims';
3
+ import { isSvcCodec } from './codecs';
4
+ import { getOptimalBitrate } from './bitrateLookup';
5
+ import { VideoQuality } from '../gen/video/sfu/models/models';
3
6
 
4
7
  export type OptimalVideoLayer = RTCRtpEncodingParameters & {
5
8
  width: number;
6
9
  height: number;
10
+ // NOTE OL: should be part of RTCRtpEncodingParameters
11
+ scalabilityMode?: string;
7
12
  };
8
13
 
9
14
  const DEFAULT_BITRATE = 1250000;
@@ -19,48 +24,87 @@ const defaultBitratePerRid: Record<string, number> = {
19
24
  f: DEFAULT_BITRATE,
20
25
  };
21
26
 
27
+ /**
28
+ * In SVC, we need to send only one video encoding (layer).
29
+ * this layer will have the additional spatial and temporal layers
30
+ * defined via the scalabilityMode property.
31
+ *
32
+ * @param layers the layers to process.
33
+ */
34
+ export const toSvcEncodings = (layers: OptimalVideoLayer[] | undefined) => {
35
+ // we take the `f` layer, and we rename it to `q`.
36
+ return layers?.filter((l) => l.rid === 'f').map((l) => ({ ...l, rid: 'q' }));
37
+ };
38
+
39
+ /**
40
+ * Converts the rid to a video quality.
41
+ */
42
+ export const ridToVideoQuality = (rid: string): VideoQuality => {
43
+ return rid === 'q'
44
+ ? VideoQuality.LOW_UNSPECIFIED
45
+ : rid === 'h'
46
+ ? VideoQuality.MID
47
+ : VideoQuality.HIGH; // default to HIGH
48
+ };
49
+
22
50
  /**
23
51
  * Determines the most optimal video layers for simulcasting
24
52
  * for the given track.
25
53
  *
26
54
  * @param videoTrack the video track to find optimal layers for.
27
55
  * @param targetResolution the expected target resolution.
56
+ * @param codecInUse the codec in use.
28
57
  * @param publishOptions the publish options for the track.
29
58
  */
30
59
  export const findOptimalVideoLayers = (
31
60
  videoTrack: MediaStreamTrack,
32
61
  targetResolution: TargetResolutionResponse = defaultTargetResolution,
62
+ codecInUse?: PreferredCodec,
33
63
  publishOptions?: PublishOptions,
34
64
  ) => {
35
65
  const optimalVideoLayers: OptimalVideoLayer[] = [];
36
66
  const settings = videoTrack.getSettings();
37
- const { width: w = 0, height: h = 0 } = settings;
38
- const { preferredBitrate, bitrateDownscaleFactor = 2 } = publishOptions || {};
67
+ const { width = 0, height = 0 } = settings;
68
+ const { scalabilityMode, bitrateDownscaleFactor = 2 } = publishOptions || {};
39
69
  const maxBitrate = getComputedMaxBitrate(
40
70
  targetResolution,
41
- w,
42
- h,
43
- preferredBitrate,
71
+ width,
72
+ height,
73
+ codecInUse,
74
+ publishOptions,
44
75
  );
45
76
  let downscaleFactor = 1;
46
77
  let bitrateFactor = 1;
47
- ['f', 'h', 'q'].forEach((rid) => {
48
- // Reversing the order [f, h, q] to [q, h, f] as Chrome uses encoding index
49
- // when deciding which layer to disable when CPU or bandwidth is constrained.
50
- // Encodings should be ordered in increasing spatial resolution order.
51
- optimalVideoLayers.unshift({
78
+ const svcCodec = isSvcCodec(codecInUse);
79
+ for (const rid of ['f', 'h', 'q']) {
80
+ const layer: OptimalVideoLayer = {
52
81
  active: true,
53
82
  rid,
54
- width: Math.round(w / downscaleFactor),
55
- height: Math.round(h / downscaleFactor),
56
- maxBitrate:
57
- Math.round(maxBitrate / bitrateFactor) || defaultBitratePerRid[rid],
58
- scaleResolutionDownBy: downscaleFactor,
83
+ width,
84
+ height,
85
+ maxBitrate,
59
86
  maxFramerate: 30,
60
- });
61
- downscaleFactor *= 2;
62
- bitrateFactor *= bitrateDownscaleFactor;
63
- });
87
+ };
88
+ if (svcCodec) {
89
+ // for SVC codecs, we need to set the scalability mode, and the
90
+ // codec will handle the rest (layers, temporal layers, etc.)
91
+ layer.scalabilityMode = scalabilityMode || 'L3T2_KEY';
92
+ } else {
93
+ // for non-SVC codecs, we need to downscale proportionally (simulcast)
94
+ layer.width = Math.round(width / downscaleFactor);
95
+ layer.height = Math.round(height / downscaleFactor);
96
+ const bitrate = Math.round(maxBitrate / bitrateFactor);
97
+ layer.maxBitrate = bitrate || defaultBitratePerRid[rid];
98
+ layer.scaleResolutionDownBy = downscaleFactor;
99
+ downscaleFactor *= 2;
100
+ bitrateFactor *= bitrateDownscaleFactor;
101
+ }
102
+
103
+ // Reversing the order [f, h, q] to [q, h, f] as Chrome uses encoding index
104
+ // when deciding which layer to disable when CPU or bandwidth is constrained.
105
+ // Encodings should be ordered in increasing spatial resolution order.
106
+ optimalVideoLayers.unshift(layer);
107
+ }
64
108
 
65
109
  // for simplicity, we start with all layers enabled, then this function
66
110
  // will clear/reassign the layers that are not needed
@@ -77,13 +121,15 @@ export const findOptimalVideoLayers = (
77
121
  * @param targetResolution the target resolution.
78
122
  * @param currentWidth the current width of the track.
79
123
  * @param currentHeight the current height of the track.
80
- * @param preferredBitrate the preferred bitrate for the track.
124
+ * @param codecInUse the codec in use.
125
+ * @param publishOptions the publish options.
81
126
  */
82
127
  export const getComputedMaxBitrate = (
83
128
  targetResolution: TargetResolutionResponse,
84
129
  currentWidth: number,
85
130
  currentHeight: number,
86
- preferredBitrate: number | undefined,
131
+ codecInUse?: PreferredCodec,
132
+ publishOptions?: PublishOptions,
87
133
  ): number => {
88
134
  // if the current resolution is lower than the target resolution,
89
135
  // we want to proportionally reduce the target bitrate
@@ -92,7 +138,12 @@ export const getComputedMaxBitrate = (
92
138
  height: targetHeight,
93
139
  bitrate: targetBitrate,
94
140
  } = targetResolution;
95
- const bitrate = preferredBitrate || targetBitrate;
141
+ const { preferredBitrate } = publishOptions || {};
142
+ const frameHeight =
143
+ currentWidth > currentHeight ? currentHeight : currentWidth;
144
+ const bitrate =
145
+ preferredBitrate ||
146
+ (codecInUse ? getOptimalBitrate(codecInUse, frameHeight) : targetBitrate);
96
147
  if (currentWidth < targetWidth || currentHeight < targetHeight) {
97
148
  const currentPixels = currentWidth * currentHeight;
98
149
  const targetPixels = targetWidth * targetHeight;