livekit-client 2.22.2 → 2.22.3

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 (68) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +486 -437
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +398 -100
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.fm.worker.js +1 -1
  8. package/dist/livekit-client.fm.worker.js.map +1 -1
  9. package/dist/livekit-client.fm.worker.mjs +8 -1
  10. package/dist/livekit-client.fm.worker.mjs.map +1 -1
  11. package/dist/livekit-client.umd.js +1 -1
  12. package/dist/livekit-client.umd.js.map +1 -1
  13. package/dist/src/api/WebSocketStream.d.ts.map +1 -1
  14. package/dist/src/api/utils.d.ts +1 -0
  15. package/dist/src/api/utils.d.ts.map +1 -1
  16. package/dist/src/e2ee/E2eeManager.d.ts +26 -0
  17. package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
  18. package/dist/src/e2ee/types.d.ts +15 -1
  19. package/dist/src/e2ee/types.d.ts.map +1 -1
  20. package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
  21. package/dist/src/e2ee/worker/ErrorRateLimiter.d.ts +21 -0
  22. package/dist/src/e2ee/worker/ErrorRateLimiter.d.ts.map +1 -0
  23. package/dist/src/e2ee/worker/FrameCryptor.d.ts +1 -18
  24. package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
  25. package/dist/src/logger.d.ts +4 -0
  26. package/dist/src/logger.d.ts.map +1 -1
  27. package/dist/src/room/RTCEngine.d.ts +1 -0
  28. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  29. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  30. package/dist/src/room/participant/publishUtils.d.ts +16 -0
  31. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  32. package/dist/src/room/track/LocalVideoTrack.d.ts +7 -0
  33. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  34. package/dist/src/room/track/options.d.ts +1 -1
  35. package/dist/src/room/utils.d.ts +34 -0
  36. package/dist/src/room/utils.d.ts.map +1 -1
  37. package/dist/ts4.2/api/utils.d.ts +1 -0
  38. package/dist/ts4.2/e2ee/E2eeManager.d.ts +26 -0
  39. package/dist/ts4.2/e2ee/types.d.ts +15 -1
  40. package/dist/ts4.2/e2ee/worker/ErrorRateLimiter.d.ts +21 -0
  41. package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +1 -18
  42. package/dist/ts4.2/logger.d.ts +4 -0
  43. package/dist/ts4.2/room/RTCEngine.d.ts +1 -0
  44. package/dist/ts4.2/room/participant/publishUtils.d.ts +16 -0
  45. package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +7 -0
  46. package/dist/ts4.2/room/track/options.d.ts +1 -1
  47. package/dist/ts4.2/room/utils.d.ts +34 -0
  48. package/package.json +1 -1
  49. package/src/api/WebSocketStream.ts +3 -8
  50. package/src/api/utils.ts +10 -0
  51. package/src/e2ee/E2eeManager.test.ts +196 -0
  52. package/src/e2ee/E2eeManager.ts +114 -19
  53. package/src/e2ee/types.ts +19 -1
  54. package/src/e2ee/worker/DataCryptor.ts +2 -1
  55. package/src/e2ee/worker/ErrorRateLimiter.test.ts +53 -0
  56. package/src/e2ee/worker/ErrorRateLimiter.ts +52 -0
  57. package/src/e2ee/worker/FrameCryptor.ts +20 -70
  58. package/src/e2ee/worker/e2ee.worker.ts +34 -11
  59. package/src/logger.ts +22 -0
  60. package/src/room/RTCEngine.ts +28 -7
  61. package/src/room/Room.ts +1 -1
  62. package/src/room/participant/LocalParticipant.ts +30 -14
  63. package/src/room/participant/publishUtils.test.ts +133 -0
  64. package/src/room/participant/publishUtils.ts +54 -19
  65. package/src/room/track/LocalVideoTrack.ts +15 -5
  66. package/src/room/track/options.ts +1 -1
  67. package/src/room/utils.test.ts +87 -0
  68. package/src/room/utils.ts +59 -0
@@ -13,12 +13,12 @@ import type {
13
13
  import { ScreenSharePresets, VideoPreset, VideoPresets, VideoPresets43 } from '../track/options';
14
14
  import type { LoggerOptions } from '../types';
15
15
  import {
16
- compareVersions,
17
16
  getReactNativeOs,
18
17
  isReactNative,
19
18
  isSVCCodec,
20
- isSafariBased,
19
+ isSVCSimulcast,
21
20
  isSafariSvcApi,
21
+ usesLegacySVCEncodings,
22
22
  } from '../utils';
23
23
 
24
24
  /** @internal */
@@ -111,6 +111,8 @@ export function computeVideoEncodings(
111
111
  const useSimulcast = options?.simulcast;
112
112
  const scalabilityMode = options?.scalabilityMode;
113
113
  const videoCodec = options?.videoCodec;
114
+ // VP9/AV1 published as rid based simulcast rather than SVC, see isSVCSimulcast
115
+ const useSVCSimulcast = isSVCSimulcast(videoCodec, options);
114
116
 
115
117
  if ((!videoEncoding && !useSimulcast && !scalabilityMode) || !width || !height) {
116
118
  // when we aren't simulcasting or svc, will need to return a single encoding without
@@ -134,7 +136,7 @@ export function computeVideoEncodings(
134
136
  videoEncoding.priority,
135
137
  );
136
138
 
137
- if (scalabilityMode && isSVCCodec(videoCodec)) {
139
+ if (scalabilityMode && isSVCCodec(videoCodec) && !useSVCSimulcast) {
138
140
  const sm = new ScalabilityMode(scalabilityMode);
139
141
 
140
142
  const encodings: RTCRtpEncodingParameters[] = [];
@@ -142,20 +144,10 @@ export function computeVideoEncodings(
142
144
  if (sm.spatial > 3) {
143
145
  throw new Error(`unsupported scalabilityMode: ${scalabilityMode}`);
144
146
  }
145
- // Before M113 in Chrome, defining multiple encodings with an SVC codec indicated
146
- // that SVC mode should be used. Safari still works this way.
147
- // This is a bit confusing but is due to how libwebrtc interpreted the encodings field
148
- // before M113.
149
- // Announced here: https://groups.google.com/g/discuss-webrtc/c/-QQ3pxrl-fw?pli=1
147
+ // Browsers that read multiple encodings on an SVC codec as SVC rather than as
148
+ // simulcast, see usesLegacySVCEncodings
150
149
  const browser = getBrowser();
151
- if (
152
- isSafariBased() ||
153
- // Even tho RN runs M114, it does not produce SVC layers when a single encoding
154
- // is provided. So we'll use the legacy SVC specification for now.
155
- // TODO: when we upstream libwebrtc, this will need additional verification
156
- isReactNative() ||
157
- (browser?.name === 'Chrome' && compareVersions(browser?.version, '113') < 0)
158
- ) {
150
+ if (usesLegacySVCEncodings()) {
159
151
  const bitratesRatio = sm.suffix == 'h' ? 2 : 3;
160
152
  // safari 18.4 uses a different svc API that requires scaleResolutionDownBy to be set.
161
153
  const requireScale = isSafariSvcApi(browser);
@@ -193,6 +185,19 @@ export function computeVideoEncodings(
193
185
  return [videoEncoding];
194
186
  }
195
187
 
188
+ // Chrome M113+ only treats multiple encodings on an SVC capable codec as real
189
+ // simulcast when every encoding carries an explicit scalabilityMode next to its
190
+ // scaleResolutionDownBy. Without it the encodings are interpreted as legacy SVC.
191
+ const applySVCSimulcastMode = (encodings: RTCRtpEncodingParameters[]) => {
192
+ if (useSVCSimulcast) {
193
+ encodings.forEach((encoding) => {
194
+ /* @ts-ignore */
195
+ encoding.scalabilityMode = scalabilityMode;
196
+ });
197
+ }
198
+ return encodings;
199
+ };
200
+
196
201
  let presets: Array<VideoPreset>;
197
202
  if (isScreenShare) {
198
203
  presets =
@@ -220,13 +225,43 @@ export function computeVideoEncodings(
220
225
  // based on other conditions.
221
226
  const size = Math.max(width, height);
222
227
  if (size >= 960 && midPreset) {
223
- return encodingsFromPresets(width, height, [lowPreset, midPreset, original], sourceFramerate);
228
+ return applySVCSimulcastMode(
229
+ encodingsFromPresets(width, height, [lowPreset, midPreset, original], sourceFramerate),
230
+ );
224
231
  }
225
232
  if (size >= 480) {
226
- return encodingsFromPresets(width, height, [lowPreset, original], sourceFramerate);
233
+ return applySVCSimulcastMode(
234
+ encodingsFromPresets(width, height, [lowPreset, original], sourceFramerate),
235
+ );
227
236
  }
228
237
  }
229
- return encodingsFromPresets(width, height, [original]);
238
+ return applySVCSimulcastMode(encodingsFromPresets(width, height, [original]));
239
+ }
240
+
241
+ /**
242
+ * Bitrate to hint to the bandwidth estimator through `x-google-start-bitrate`, so that
243
+ * a publish does not spend its first seconds ramping up from a very low rate.
244
+ *
245
+ * It has to be the total the encoder will put on the wire, which means picking the
246
+ * encoding that carries the inclusive bitrate:
247
+ * - SVC publishes a single stream with the layers built in. `encodings[0]` holds the
248
+ * full bitrate — the legacy SVC shape orders its encodings `f`..`q`, so that holds
249
+ * for both SVC shapes.
250
+ * - Simulcast publishes independent streams ordered `q`..`f`, so the total is the sum.
251
+ * This includes VP9/AV1 published as rid based simulcast, where `encodings[0]` is
252
+ * the *smallest* layer even though the codec is SVC capable.
253
+ *
254
+ * @internal
255
+ */
256
+ export function computeStartTargetBitrate(
257
+ codec: string,
258
+ options: TrackPublishOptions | undefined,
259
+ encodings: RTCRtpEncodingParameters[],
260
+ ): number {
261
+ if (isSVCCodec(codec) && !isSVCSimulcast(codec, options)) {
262
+ return encodings[0]?.maxBitrate ?? 0;
263
+ }
264
+ return encodings.reduce((sum, enc) => sum + (enc.maxBitrate ?? 0), 0);
230
265
  }
231
266
 
232
267
  export function computeTrackBackupEncodings(
@@ -16,7 +16,7 @@ import {
16
16
  import type { VideoSenderStats } from '../stats';
17
17
  import { computeBitrate, monitorFrequency } from '../stats';
18
18
  import type { LoggerOptions } from '../types';
19
- import { isFireFox, isMobile, isSVCCodec, isWeb } from '../utils';
19
+ import { isFireFox, isMobile, isSVCCodec, isSVCSimulcast, isWeb } from '../utils';
20
20
  import LocalTrack from './LocalTrack';
21
21
  import { Track, VideoQuality } from './Track';
22
22
  import type { TrackPublishOptions, VideoCaptureOptions, VideoCodec } from './options';
@@ -243,6 +243,16 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
243
243
  return items;
244
244
  }
245
245
 
246
+ /**
247
+ * Whether `codec` is being published as SVC (a single stream carrying all spatial
248
+ * layers) as opposed to rid based simulcast. VP9/AV1 are SVC unless the publisher
249
+ * opted into simulcast, in which case each rid is an independent stream and the
250
+ * layers can be enabled/disabled individually.
251
+ */
252
+ private isSvcPublish(codec?: string): boolean {
253
+ return isSVCCodec(codec) && !isSVCSimulcast(codec, this.publishOptions);
254
+ }
255
+
246
256
  setPublishingQuality(maxQuality: VideoQuality) {
247
257
  const qualities: SubscribedQuality[] = [];
248
258
  for (let q = VideoQuality.LOW; q <= VideoQuality.HIGH; q += 1) {
@@ -254,7 +264,7 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
254
264
  );
255
265
  }
256
266
  this.log.debug(`setting publishing quality. max quality ${maxQuality}`, this.logContext);
257
- this.setPublishingLayers(isSVCCodec(this.codec), qualities);
267
+ this.setPublishingLayers(this.isSvcPublish(this.codec), qualities);
258
268
  }
259
269
 
260
270
  async restartTrack(options?: VideoCaptureOptions) {
@@ -491,7 +501,7 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
491
501
  });
492
502
  // only enable simulcast codec for preference codec setted
493
503
  if (!this.codec && codecs.length > 0) {
494
- await this.setPublishingLayers(isSVCCodec(codecs[0].codec), codecs[0].qualities);
504
+ await this.setPublishingLayers(this.isSvcPublish(codecs[0].codec), codecs[0].qualities);
495
505
 
496
506
  return [];
497
507
  }
@@ -501,7 +511,7 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
501
511
  const newCodecs: VideoCodec[] = [];
502
512
  for await (const codec of codecs) {
503
513
  if (!this.codec || this.codec === codec.codec) {
504
- await this.setPublishingLayers(isSVCCodec(codec.codec), codec.qualities);
514
+ await this.setPublishingLayers(this.isSvcPublish(codec.codec), codec.qualities);
505
515
  } else {
506
516
  const simulcastCodecInfo = this.simulcastCodecs.get(codec.codec as VideoCodec);
507
517
  this.log.debug(`try setPublishingCodec for ${codec.codec}`, {
@@ -522,7 +532,7 @@ export default class LocalVideoTrack extends LocalTrack<Track.Kind.Video> {
522
532
  simulcastCodecInfo.encodings!,
523
533
  codec.qualities,
524
534
  this.senderLock,
525
- isSVCCodec(codec.codec),
535
+ this.isSvcPublish(codec.codec),
526
536
  this.log,
527
537
  this.logContext,
528
538
  );
@@ -79,7 +79,7 @@ export interface TrackPublishDefaults {
79
79
 
80
80
  /**
81
81
  * scalability mode for svc codecs, defaults to 'L3T3_KEY'.
82
- * for svc codecs, simulcast is disabled.
82
+ * for svc codecs, simulcast is disabled if more than one spatial layer is used ('L2Tx' or 'L3Tx').
83
83
  */
84
84
  scalabilityMode?: ScalabilityMode;
85
85
 
@@ -4,10 +4,13 @@ import {
4
4
  ddExtensionURI,
5
5
  extractMaxAgeFromRequestHeaders,
6
6
  getClientInfo,
7
+ isSVCSimulcast,
8
+ isSVCSimulcastSupportedByServer,
7
9
  negotiateDependencyDescriptor,
8
10
  splitUtf8,
9
11
  supportsAdaptiveStream,
10
12
  toWebsocketUrl,
13
+ usesLegacySVCEncodings,
11
14
  } from './utils';
12
15
 
13
16
  describe('toWebsocketUrl', () => {
@@ -280,3 +283,87 @@ describe('supportsAdaptiveStream', () => {
280
283
  expect(supportsAdaptiveStream()).toBe(false);
281
284
  });
282
285
  });
286
+
287
+ describe('isSVCSimulcast', () => {
288
+ it('requires an svc capable codec, simulcast and a single spatial layer mode', () => {
289
+ expect(isSVCSimulcast('vp9', { simulcast: true, scalabilityMode: 'L1T2' })).toBe(true);
290
+ expect(isSVCSimulcast('av1', { simulcast: true, scalabilityMode: 'L1T3' })).toBe(true);
291
+ });
292
+
293
+ it('stays on svc without the opt in', () => {
294
+ expect(isSVCSimulcast('vp9', { simulcast: false, scalabilityMode: 'L1T2' })).toBe(false);
295
+ expect(isSVCSimulcast('vp9', { simulcast: true })).toBe(false);
296
+ // a multi spatial layer mode is svc by definition
297
+ expect(isSVCSimulcast('vp9', { simulcast: true, scalabilityMode: 'L3T3_KEY' })).toBe(false);
298
+ expect(isSVCSimulcast('vp9', undefined)).toBe(false);
299
+ });
300
+
301
+ it('does not apply to non svc codecs', () => {
302
+ expect(isSVCSimulcast('vp8', { simulcast: true, scalabilityMode: 'L1T2' })).toBe(false);
303
+ expect(isSVCSimulcast('h264', { simulcast: true, scalabilityMode: 'L1T2' })).toBe(false);
304
+ expect(isSVCSimulcast(undefined, { simulcast: true, scalabilityMode: 'L1T2' })).toBe(false);
305
+ });
306
+ });
307
+
308
+ describe('isSVCSimulcastSupportedByServer', () => {
309
+ it('requires a server newer than 1.13.6', () => {
310
+ expect(isSVCSimulcastSupportedByServer('1.13.7')).toBe(true);
311
+ expect(isSVCSimulcastSupportedByServer('1.14.0')).toBe(true);
312
+ expect(isSVCSimulcastSupportedByServer('2.0.0')).toBe(true);
313
+ });
314
+
315
+ it('rejects 1.13.6 and older', () => {
316
+ expect(isSVCSimulcastSupportedByServer('1.13.6')).toBe(false);
317
+ expect(isSVCSimulcastSupportedByServer('1.13.5')).toBe(false);
318
+ expect(isSVCSimulcastSupportedByServer('1.9.0')).toBe(false);
319
+ expect(isSVCSimulcastSupportedByServer('0.15.1')).toBe(false);
320
+ });
321
+
322
+ it('treats an unknown version as unsupported', () => {
323
+ expect(isSVCSimulcastSupportedByServer(undefined)).toBe(false);
324
+ expect(isSVCSimulcastSupportedByServer('')).toBe(false);
325
+ });
326
+ });
327
+
328
+ describe('usesLegacySVCEncodings', () => {
329
+ const stubUserAgent = (userAgent: string) =>
330
+ vi.stubGlobal('navigator', { userAgent, product: 'Gecko' });
331
+
332
+ afterEach(() => {
333
+ vi.unstubAllGlobals();
334
+ });
335
+
336
+ const chrome = (v: string) =>
337
+ `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${v} Safari/537.36`;
338
+
339
+ it('is legacy on chrome before M113', () => {
340
+ stubUserAgent(chrome('112.0.0.0'));
341
+ expect(usesLegacySVCEncodings()).toBe(true);
342
+ });
343
+
344
+ it('is not legacy from chrome M113 onwards', () => {
345
+ stubUserAgent(chrome('113.0.0.0'));
346
+ expect(usesLegacySVCEncodings()).toBe(false);
347
+ stubUserAgent(chrome('120.0.0.0'));
348
+ expect(usesLegacySVCEncodings()).toBe(false);
349
+ });
350
+
351
+ it('is legacy on safari, which has no rid based svc simulcast', () => {
352
+ stubUserAgent(
353
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
354
+ );
355
+ expect(usesLegacySVCEncodings()).toBe(true);
356
+ });
357
+
358
+ it('is legacy on iOS, where every browser is webkit', () => {
359
+ stubUserAgent(
360
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/120.0.0.0 Mobile/15E148 Safari/604.1',
361
+ );
362
+ expect(usesLegacySVCEncodings()).toBe(true);
363
+ });
364
+
365
+ it('is legacy on react native regardless of chrome version', () => {
366
+ vi.stubGlobal('navigator', { userAgent: chrome('120.0.0.0'), product: 'ReactNative' });
367
+ expect(usesLegacySVCEncodings()).toBe(true);
368
+ });
369
+ });
package/src/room/utils.ts CHANGED
@@ -185,6 +185,65 @@ export function negotiateDependencyDescriptor(transceiver: RTCRtpTransceiver): b
185
185
  }
186
186
  }
187
187
 
188
+ /**
189
+ * VP9 and AV1 are published as SVC (a single RTP stream carrying every spatial layer)
190
+ * by default. They can instead be published as real, rid based simulcast — one
191
+ * independent stream per rid, each carrying a single spatial layer — when the caller
192
+ * opts in with `simulcast: true` and a single spatial layer scalability mode (`L1Tx`).
193
+ *
194
+ * The SFU has to be told about this: without an explicit
195
+ * `SimulcastCodec.videoLayerMode` it assumes `MULTIPLE_SPATIAL_LAYERS_PER_STREAM` for
196
+ * any SVC capable codec.
197
+ */
198
+ export function isSVCSimulcast(
199
+ codec?: string,
200
+ options?: { simulcast?: boolean; scalabilityMode?: string },
201
+ ): boolean {
202
+ return isSVCCodec(codec) && !!options?.simulcast && !!options.scalabilityMode?.startsWith('L1T');
203
+ }
204
+
205
+ /**
206
+ * Whether the browser reads multiple encodings on an SVC capable codec as *legacy SVC*
207
+ * rather than as real simulcast.
208
+ *
209
+ * Before Chrome M113, supplying more than one encoding for VP9/AV1 selected SVC mode;
210
+ * only from M113 does libwebrtc treat such encodings as simulcast, and only when each
211
+ * one carries its own scalabilityMode. Safari (and anything WebKit based, i. e. every
212
+ * browser on iOS) still uses the old interpretation, as does React Native's libwebrtc.
213
+ * Announced at https://groups.google.com/g/discuss-webrtc/c/-QQ3pxrl-fw
214
+ *
215
+ * Where this is true the rids would not exist on the wire, so VP9/AV1 must be published
216
+ * as SVC no matter what the caller asked for.
217
+ */
218
+ export function usesLegacySVCEncodings(): boolean {
219
+ const browser = getBrowser();
220
+ return (
221
+ isSafariBased() ||
222
+ // Even tho RN runs M114, it does not produce SVC layers when a single encoding
223
+ // is provided. So we'll use the legacy SVC specification for now.
224
+ // TODO: when we upstream libwebrtc, this will need additional verification
225
+ isReactNative() ||
226
+ (browser?.name === 'Chrome' && compareVersions(browser.version, '113') < 0)
227
+ );
228
+ }
229
+
230
+ /**
231
+ * Last server version that doesn't support vp9/av1 simulcast.
232
+ */
233
+ const svcSimulcastMinServerVersion = '1.13.6';
234
+
235
+ /**
236
+ * Whether the connected server honours `SimulcastCodec.videoLayerMode`, i. e. whether
237
+ * VP9/AV1 can be published as rid based simulcast. An unknown version is treated as
238
+ * unsupported so the publish falls back to SVC.
239
+ */
240
+ export function isSVCSimulcastSupportedByServer(serverVersion?: string): boolean {
241
+ if (!serverVersion) {
242
+ return false;
243
+ }
244
+ return compareVersions(serverVersion, svcSimulcastMinServerVersion) > 0;
245
+ }
246
+
188
247
  export function supportsSetSinkId(elm?: HTMLMediaElement): boolean {
189
248
  if (!document || isSafariBased()) {
190
249
  return false;