livekit-client 1.9.7 → 1.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. package/dist/livekit-client.esm.mjs +2972 -2583
  2. package/dist/livekit-client.esm.mjs.map +1 -1
  3. package/dist/livekit-client.umd.js +1 -1
  4. package/dist/livekit-client.umd.js.map +1 -1
  5. package/dist/src/api/SignalClient.d.ts +2 -1
  6. package/dist/src/api/SignalClient.d.ts.map +1 -1
  7. package/dist/src/connectionHelper/ConnectionCheck.d.ts +2 -3
  8. package/dist/src/connectionHelper/ConnectionCheck.d.ts.map +1 -1
  9. package/dist/src/connectionHelper/checks/Checker.d.ts +2 -3
  10. package/dist/src/connectionHelper/checks/Checker.d.ts.map +1 -1
  11. package/dist/src/index.d.ts +1 -0
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/proto/livekit_models.d.ts +108 -10
  14. package/dist/src/proto/livekit_models.d.ts.map +1 -1
  15. package/dist/src/proto/livekit_rtc.d.ts +513 -194
  16. package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
  17. package/dist/src/room/PCTransport.d.ts +1 -1
  18. package/dist/src/room/PCTransport.d.ts.map +1 -1
  19. package/dist/src/room/RTCEngine.d.ts +2 -4
  20. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  21. package/dist/src/room/Room.d.ts +6 -6
  22. package/dist/src/room/Room.d.ts.map +1 -1
  23. package/dist/src/room/events.d.ts +5 -1
  24. package/dist/src/room/events.d.ts.map +1 -1
  25. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  26. package/dist/src/room/participant/Participant.d.ts +4 -6
  27. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  28. package/dist/src/room/participant/RemoteParticipant.d.ts +2 -1
  29. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  30. package/dist/src/room/participant/publishUtils.d.ts +8 -0
  31. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  32. package/dist/src/room/track/LocalTrack.d.ts +33 -0
  33. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  34. package/dist/src/room/track/LocalVideoTrack.d.ts +1 -1
  35. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  36. package/dist/src/room/track/RemoteTrackPublication.d.ts +4 -1
  37. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  38. package/dist/src/room/track/RemoteVideoTrack.d.ts +1 -0
  39. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  40. package/dist/src/room/track/Track.d.ts +2 -4
  41. package/dist/src/room/track/Track.d.ts.map +1 -1
  42. package/dist/src/room/track/TrackPublication.d.ts +4 -5
  43. package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
  44. package/dist/src/room/track/options.d.ts +14 -5
  45. package/dist/src/room/track/options.d.ts.map +1 -1
  46. package/dist/src/room/track/processor/types.d.ts +19 -0
  47. package/dist/src/room/track/processor/types.d.ts.map +1 -0
  48. package/dist/src/room/track/types.d.ts +2 -1
  49. package/dist/src/room/track/types.d.ts.map +1 -1
  50. package/dist/src/room/utils.d.ts.map +1 -1
  51. package/dist/ts4.2/src/api/SignalClient.d.ts +2 -1
  52. package/dist/ts4.2/src/connectionHelper/ConnectionCheck.d.ts +2 -3
  53. package/dist/ts4.2/src/connectionHelper/checks/Checker.d.ts +2 -3
  54. package/dist/ts4.2/src/index.d.ts +1 -0
  55. package/dist/ts4.2/src/proto/livekit_models.d.ts +126 -12
  56. package/dist/ts4.2/src/proto/livekit_rtc.d.ts +617 -254
  57. package/dist/ts4.2/src/room/PCTransport.d.ts +1 -1
  58. package/dist/ts4.2/src/room/RTCEngine.d.ts +2 -4
  59. package/dist/ts4.2/src/room/Room.d.ts +6 -6
  60. package/dist/ts4.2/src/room/events.d.ts +5 -1
  61. package/dist/ts4.2/src/room/participant/Participant.d.ts +4 -6
  62. package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +2 -1
  63. package/dist/ts4.2/src/room/participant/publishUtils.d.ts +8 -0
  64. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +33 -0
  65. package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +1 -1
  66. package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +4 -1
  67. package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +1 -0
  68. package/dist/ts4.2/src/room/track/Track.d.ts +2 -4
  69. package/dist/ts4.2/src/room/track/TrackPublication.d.ts +4 -5
  70. package/dist/ts4.2/src/room/track/options.d.ts +14 -5
  71. package/dist/ts4.2/src/room/track/processor/types.d.ts +19 -0
  72. package/dist/ts4.2/src/room/track/types.d.ts +2 -1
  73. package/package.json +14 -15
  74. package/src/api/SignalClient.ts +8 -1
  75. package/src/connectionHelper/ConnectionCheck.ts +2 -3
  76. package/src/connectionHelper/checks/Checker.ts +2 -3
  77. package/src/index.ts +1 -0
  78. package/src/logger.ts +4 -4
  79. package/src/proto/google/protobuf/timestamp.ts +3 -3
  80. package/src/proto/livekit_models.ts +254 -161
  81. package/src/proto/livekit_rtc.ts +334 -180
  82. package/src/room/PCTransport.ts +1 -1
  83. package/src/room/RTCEngine.ts +4 -4
  84. package/src/room/Room.ts +67 -12
  85. package/src/room/events.ts +4 -0
  86. package/src/room/participant/LocalParticipant.ts +33 -5
  87. package/src/room/participant/Participant.ts +4 -5
  88. package/src/room/participant/RemoteParticipant.ts +8 -4
  89. package/src/room/participant/publishUtils.ts +47 -20
  90. package/src/room/track/LocalTrack.ts +180 -57
  91. package/src/room/track/LocalVideoTrack.ts +98 -33
  92. package/src/room/track/RemoteTrackPublication.ts +8 -1
  93. package/src/room/track/RemoteVideoTrack.ts +23 -6
  94. package/src/room/track/Track.ts +5 -7
  95. package/src/room/track/TrackPublication.ts +4 -5
  96. package/src/room/track/options.ts +14 -5
  97. package/src/room/track/processor/types.ts +20 -0
  98. package/src/room/track/types.ts +2 -1
  99. package/src/room/utils.ts +6 -3
@@ -1,16 +1,12 @@
1
1
  import log from '../../logger';
2
+ import { getBrowser } from '../../utils/browserParser';
2
3
  import DeviceManager from '../DeviceManager';
3
- import { TrackInvalidError } from '../errors';
4
+ import { DeviceUnsupportedError, TrackInvalidError } from '../errors';
4
5
  import { TrackEvent } from '../events';
5
- import {
6
- Mutex,
7
- getEmptyAudioStreamTrack,
8
- getEmptyVideoStreamTrack,
9
- isMobile,
10
- sleep,
11
- } from '../utils';
6
+ import { Mutex, compareVersions, isMobile, sleep } from '../utils';
12
7
  import { Track, attachToElement, detachTrack } from './Track';
13
8
  import type { VideoCodec } from './options';
9
+ import type { TrackProcessor } from './processor/types';
14
10
 
15
11
  const defaultDimensionsTimeout = 1000;
16
12
 
@@ -31,6 +27,12 @@ export default abstract class LocalTrack extends Track {
31
27
 
32
28
  protected pauseUpstreamLock: Mutex;
33
29
 
30
+ protected processorElement?: HTMLMediaElement;
31
+
32
+ protected processor?: TrackProcessor<typeof this.kind>;
33
+
34
+ protected isSettingUpProcessor: boolean = false;
35
+
34
36
  /**
35
37
  *
36
38
  * @param mediaTrack
@@ -45,12 +47,16 @@ export default abstract class LocalTrack extends Track {
45
47
  userProvidedTrack = false,
46
48
  ) {
47
49
  super(mediaTrack, kind);
48
- this._mediaStreamTrack.addEventListener('ended', this.handleEnded);
49
- this.constraints = constraints ?? mediaTrack.getConstraints();
50
50
  this.reacquireTrack = false;
51
51
  this.providedByUser = userProvidedTrack;
52
52
  this.muteLock = new Mutex();
53
53
  this.pauseUpstreamLock = new Mutex();
54
+ // added to satisfy TS compiler, constraints are synced with MediaStreamTrack
55
+ this.constraints = mediaTrack.getConstraints();
56
+ this.setMediaStreamTrack(mediaTrack);
57
+ if (constraints) {
58
+ this.constraints = constraints;
59
+ }
54
60
  }
55
61
 
56
62
  get id(): string {
@@ -82,6 +88,57 @@ export default abstract class LocalTrack extends Track {
82
88
  return this.providedByUser;
83
89
  }
84
90
 
91
+ get mediaStreamTrack() {
92
+ return this.processor?.processedTrack ?? this._mediaStreamTrack;
93
+ }
94
+
95
+ private async setMediaStreamTrack(newTrack: MediaStreamTrack) {
96
+ if (newTrack === this._mediaStreamTrack) {
97
+ return;
98
+ }
99
+ if (this._mediaStreamTrack) {
100
+ // detach
101
+ this.attachedElements.forEach((el) => {
102
+ detachTrack(this._mediaStreamTrack, el);
103
+ });
104
+ this._mediaStreamTrack.removeEventListener('ended', this.handleEnded);
105
+ this._mediaStreamTrack.removeEventListener('mute', this.pauseUpstream);
106
+ this._mediaStreamTrack.removeEventListener('unmute', this.resumeUpstream);
107
+ if (!this.providedByUser) {
108
+ this._mediaStreamTrack.stop();
109
+ }
110
+ }
111
+
112
+ this.mediaStream = new MediaStream([newTrack]);
113
+ if (newTrack) {
114
+ newTrack.addEventListener('ended', this.handleEnded);
115
+ // when underlying track emits mute, it indicates that the device is unable
116
+ // to produce media. In this case we'll need to signal with remote that
117
+ // the track is "muted"
118
+ // note this is different from LocalTrack.mute because we do not want to
119
+ // touch MediaStreamTrack.enabled
120
+ newTrack.addEventListener('mute', () => {
121
+ log.info('pausing upstream due to device mute');
122
+ this.pauseUpstream();
123
+ });
124
+ newTrack.addEventListener('unmute', this.resumeUpstream);
125
+ this.constraints = newTrack.getConstraints();
126
+ }
127
+ if (this.sender) {
128
+ await this.sender.replaceTrack(newTrack);
129
+ }
130
+ this._mediaStreamTrack = newTrack;
131
+ if (newTrack) {
132
+ // sync muted state with the enabled state of the newly provided track
133
+ this._mediaStreamTrack.enabled = !this.isMuted;
134
+ // when a valid track is replace, we'd want to start producing
135
+ await this.resumeUpstream();
136
+ this.attachedElements.forEach((el) => {
137
+ attachToElement(newTrack, el);
138
+ });
139
+ }
140
+ }
141
+
85
142
  async waitForDimensions(timeout = defaultDimensionsTimeout): Promise<Track.Dimensions> {
86
143
  if (this.kind === Track.Kind.Audio) {
87
144
  throw new Error('cannot get dimensions for audio tracks');
@@ -127,37 +184,15 @@ export default abstract class LocalTrack extends Track {
127
184
  throw new TrackInvalidError('unable to replace an unpublished track');
128
185
  }
129
186
 
130
- // detach
131
- this.attachedElements.forEach((el) => {
132
- detachTrack(this._mediaStreamTrack, el);
133
- });
134
- this._mediaStreamTrack.removeEventListener('ended', this.handleEnded);
135
- // on Safari, the old audio track must be stopped before attempting to acquire
136
- // the new track, otherwise the new track will stop with
137
- // 'A MediaStreamTrack ended due to a capture failure`
138
- if (!this.providedByUser) {
139
- this._mediaStreamTrack.stop();
140
- }
141
-
142
- track.addEventListener('ended', this.handleEnded);
143
187
  log.debug('replace MediaStreamTrack');
188
+ this.setMediaStreamTrack(track);
189
+ // this must be synced *after* setting mediaStreamTrack above, since it relies
190
+ // on the previous state in order to cleanup
191
+ this.providedByUser = userProvidedTrack;
144
192
 
145
- if (this.sender) {
146
- await this.sender.replaceTrack(track);
193
+ if (this.processor) {
194
+ await this.stopProcessor();
147
195
  }
148
- this._mediaStreamTrack = track;
149
-
150
- // sync muted state with the enabled state of the newly provided track
151
- this._mediaStreamTrack.enabled = !this.isMuted;
152
-
153
- await this.resumeUpstream();
154
-
155
- this.attachedElements.forEach((el) => {
156
- attachToElement(track, el);
157
- });
158
-
159
- this.mediaStream = new MediaStream([track]);
160
- this.providedByUser = userProvidedTrack;
161
196
  return this;
162
197
  }
163
198
 
@@ -178,9 +213,10 @@ export default abstract class LocalTrack extends Track {
178
213
  streamConstraints.audio = constraints;
179
214
  }
180
215
 
181
- // detach
216
+ // these steps are duplicated from setMediaStreamTrack because we must stop
217
+ // the previous tracks before new tracks can be acquired
182
218
  this.attachedElements.forEach((el) => {
183
- detachTrack(this._mediaStreamTrack, el);
219
+ detachTrack(this.mediaStreamTrack, el);
184
220
  });
185
221
  this._mediaStreamTrack.removeEventListener('ended', this.handleEnded);
186
222
  // on Safari, the old audio track must be stopped before attempting to acquire
@@ -194,21 +230,16 @@ export default abstract class LocalTrack extends Track {
194
230
  newTrack.addEventListener('ended', this.handleEnded);
195
231
  log.debug('re-acquired MediaStreamTrack');
196
232
 
197
- if (this.sender) {
198
- // Track can be restarted after it's unpublished
199
- await this.sender.replaceTrack(newTrack);
200
- }
201
-
202
- this._mediaStreamTrack = newTrack;
203
-
204
- await this.resumeUpstream();
205
-
206
- this.attachedElements.forEach((el) => {
207
- attachToElement(newTrack, el);
208
- });
209
-
210
- this.mediaStream = mediaStream;
233
+ this.setMediaStreamTrack(newTrack);
211
234
  this.constraints = constraints;
235
+ if (this.processor) {
236
+ const processor = this.processor;
237
+ await this.setProcessor(processor);
238
+ } else {
239
+ this.attachedElements.forEach((el) => {
240
+ attachToElement(this._mediaStreamTrack, el);
241
+ });
242
+ }
212
243
  this.emit(TrackEvent.Restarted, this);
213
244
  return this;
214
245
  }
@@ -253,6 +284,18 @@ export default abstract class LocalTrack extends Track {
253
284
  this.emit(TrackEvent.Ended, this);
254
285
  };
255
286
 
287
+ stop() {
288
+ super.stop();
289
+ this.processor?.destroy();
290
+ this.processor = undefined;
291
+ }
292
+
293
+ /**
294
+ * pauses publishing to the server without disabling the local MediaStreamTrack
295
+ * this is used to display a user's own video locally while pausing publishing to
296
+ * the server.
297
+ * this API is unsupported on Safari < 12 due to a bug
298
+ **/
256
299
  async pauseUpstream() {
257
300
  const unlock = await this.pauseUpstreamLock.lock();
258
301
  try {
@@ -266,9 +309,12 @@ export default abstract class LocalTrack extends Track {
266
309
 
267
310
  this._isUpstreamPaused = true;
268
311
  this.emit(TrackEvent.UpstreamPaused, this);
269
- const emptyTrack =
270
- this.kind === Track.Kind.Audio ? getEmptyAudioStreamTrack() : getEmptyVideoStreamTrack();
271
- await this.sender.replaceTrack(emptyTrack);
312
+ const browser = getBrowser();
313
+ if (browser?.name === 'Safari' && compareVersions(browser.version, '12.0') < 0) {
314
+ // https://bugs.webkit.org/show_bug.cgi?id=184911
315
+ throw new DeviceUnsupportedError('pauseUpstream is not supported on Safari < 12.');
316
+ }
317
+ await this.sender.replaceTrack(null);
272
318
  } finally {
273
319
  unlock();
274
320
  }
@@ -287,11 +333,88 @@ export default abstract class LocalTrack extends Track {
287
333
  this._isUpstreamPaused = false;
288
334
  this.emit(TrackEvent.UpstreamResumed, this);
289
335
 
336
+ // this operation is noop if mediastreamtrack is already being sent
290
337
  await this.sender.replaceTrack(this._mediaStreamTrack);
291
338
  } finally {
292
339
  unlock();
293
340
  }
294
341
  }
295
342
 
343
+ /**
344
+ * Sets a processor on this track.
345
+ * See https://github.com/livekit/track-processors-js for example usage
346
+ *
347
+ * @experimental
348
+ *
349
+ * @param processor
350
+ * @param showProcessedStreamLocally
351
+ * @returns
352
+ */
353
+ async setProcessor(
354
+ processor: TrackProcessor<typeof this.kind>,
355
+ showProcessedStreamLocally = true,
356
+ ) {
357
+ if (this.isSettingUpProcessor) {
358
+ log.warn('already trying to set up a processor');
359
+ return;
360
+ }
361
+ log.debug('setting up processor');
362
+ this.isSettingUpProcessor = true;
363
+ if (this.processor) {
364
+ await this.stopProcessor();
365
+ }
366
+ if (this.kind === 'unknown') {
367
+ throw TypeError('cannot set processor on track of unknown kind');
368
+ }
369
+ this.processorElement = this.processorElement ?? document.createElement(this.kind);
370
+ this.processorElement.muted = true;
371
+
372
+ attachToElement(this._mediaStreamTrack, this.processorElement);
373
+ this.processorElement.play().catch((e) => log.error(e));
374
+
375
+ const processorOptions = {
376
+ kind: this.kind,
377
+ track: this._mediaStreamTrack,
378
+ element: this.processorElement,
379
+ };
380
+
381
+ await processor.init(processorOptions);
382
+ this.processor = processor;
383
+ if (this.processor.processedTrack) {
384
+ for (const el of this.attachedElements) {
385
+ if (el !== this.processorElement && showProcessedStreamLocally) {
386
+ detachTrack(this._mediaStreamTrack, el);
387
+ attachToElement(this.processor.processedTrack, el);
388
+ }
389
+ }
390
+ await this.sender?.replaceTrack(this.processor.processedTrack);
391
+ }
392
+ this.isSettingUpProcessor = false;
393
+ }
394
+
395
+ getProcessor() {
396
+ return this.processor;
397
+ }
398
+
399
+ /**
400
+ * Stops the track processor
401
+ * See https://github.com/livekit/track-processors-js for example usage
402
+ *
403
+ * @experimental
404
+ * @returns
405
+ */
406
+ async stopProcessor() {
407
+ if (!this.processor) return;
408
+
409
+ log.debug('stopping processor');
410
+ this.processor.processedTrack?.stop();
411
+ await this.processor.destroy();
412
+ this.processor = undefined;
413
+ this.processorElement?.remove();
414
+ this.processorElement = undefined;
415
+
416
+ await this.restart();
417
+ }
418
+
296
419
  protected abstract monitorSender(): void;
297
420
  }
@@ -2,6 +2,7 @@ import type { SignalClient } from '../../api/SignalClient';
2
2
  import log from '../../logger';
3
3
  import { VideoLayer, VideoQuality } from '../../proto/livekit_models';
4
4
  import type { SubscribedCodec, SubscribedQuality } from '../../proto/livekit_rtc';
5
+ import { ScalabilityMode } from '../participant/publishUtils';
5
6
  import { computeBitrate, monitorFrequency } from '../stats';
6
7
  import type { VideoSenderStats } from '../stats';
7
8
  import { Mutex, isFireFox, isMobile, isWeb } from '../utils';
@@ -349,45 +350,90 @@ async function setPublishingLayersForSender(
349
350
  }
350
351
 
351
352
  let hasChanged = false;
352
- encodings.forEach((encoding, idx) => {
353
- let rid = encoding.rid ?? '';
354
- if (rid === '') {
355
- rid = 'q';
356
- }
357
- const quality = videoQualityForRid(rid);
358
- const subscribedQuality = qualities.find((q) => q.quality === quality);
359
- if (!subscribedQuality) {
360
- return;
361
- }
362
- if (encoding.active !== subscribedQuality.enabled) {
353
+
354
+ /* disable closable spatial layer as it has video blur / frozen issue with current server / client
355
+ 1. chrome 113: when switching to up layer with scalability Mode change, it will generate a
356
+ low resolution frame and recover very quickly, but noticable
357
+ 2. livekit sfu: additional pli request cause video frozen for a few frames, also noticable */
358
+ const closableSpatial = false;
359
+ /* @ts-ignore */
360
+ if (closableSpatial && encodings[0].scalabilityMode) {
361
+ // svc dynacast encodings
362
+ const encoding = encodings[0];
363
+ /* @ts-ignore */
364
+ // const mode = new ScalabilityMode(encoding.scalabilityMode);
365
+ let maxQuality = VideoQuality.OFF;
366
+ qualities.forEach((q) => {
367
+ if (q.enabled && (maxQuality === VideoQuality.OFF || q.quality > maxQuality)) {
368
+ maxQuality = q.quality;
369
+ }
370
+ });
371
+
372
+ if (maxQuality === VideoQuality.OFF) {
373
+ if (encoding.active) {
374
+ encoding.active = false;
375
+ hasChanged = true;
376
+ }
377
+ } else if (!encoding.active /* || mode.spatial !== maxQuality + 1*/) {
363
378
  hasChanged = true;
364
- encoding.active = subscribedQuality.enabled;
365
- log.debug(
366
- `setting layer ${subscribedQuality.quality} to ${
367
- encoding.active ? 'enabled' : 'disabled'
368
- }`,
369
- );
370
-
371
- // FireFox does not support setting encoding.active to false, so we
372
- // have a workaround of lowering its bitrate and resolution to the min.
373
- if (isFireFox()) {
374
- if (subscribedQuality.enabled) {
375
- encoding.scaleResolutionDownBy = senderEncodings[idx].scaleResolutionDownBy;
376
- encoding.maxBitrate = senderEncodings[idx].maxBitrate;
377
- /* @ts-ignore */
378
- encoding.maxFrameRate = senderEncodings[idx].maxFrameRate;
379
- } else {
380
- encoding.scaleResolutionDownBy = 4;
381
- encoding.maxBitrate = 10;
382
- /* @ts-ignore */
383
- encoding.maxFrameRate = 2;
384
- }
379
+ encoding.active = true;
380
+ /*
381
+ @ts-ignore
382
+ const originalMode = new ScalabilityMode(senderEncodings[0].scalabilityMode)
383
+ mode.spatial = maxQuality + 1;
384
+ mode.suffix = originalMode.suffix;
385
+ if (mode.spatial === 1) {
386
+ // no suffix for L1Tx
387
+ mode.suffix = undefined;
385
388
  }
389
+ @ts-ignore
390
+ encoding.scalabilityMode = mode.toString();
391
+ encoding.scaleResolutionDownBy = 2 ** (2 - maxQuality);
392
+ */
386
393
  }
387
- });
394
+ } else {
395
+ // simulcast dynacast encodings
396
+ encodings.forEach((encoding, idx) => {
397
+ let rid = encoding.rid ?? '';
398
+ if (rid === '') {
399
+ rid = 'q';
400
+ }
401
+ const quality = videoQualityForRid(rid);
402
+ const subscribedQuality = qualities.find((q) => q.quality === quality);
403
+ if (!subscribedQuality) {
404
+ return;
405
+ }
406
+ if (encoding.active !== subscribedQuality.enabled) {
407
+ hasChanged = true;
408
+ encoding.active = subscribedQuality.enabled;
409
+ log.debug(
410
+ `setting layer ${subscribedQuality.quality} to ${
411
+ encoding.active ? 'enabled' : 'disabled'
412
+ }`,
413
+ );
414
+
415
+ // FireFox does not support setting encoding.active to false, so we
416
+ // have a workaround of lowering its bitrate and resolution to the min.
417
+ if (isFireFox()) {
418
+ if (subscribedQuality.enabled) {
419
+ encoding.scaleResolutionDownBy = senderEncodings[idx].scaleResolutionDownBy;
420
+ encoding.maxBitrate = senderEncodings[idx].maxBitrate;
421
+ /* @ts-ignore */
422
+ encoding.maxFrameRate = senderEncodings[idx].maxFrameRate;
423
+ } else {
424
+ encoding.scaleResolutionDownBy = 4;
425
+ encoding.maxBitrate = 10;
426
+ /* @ts-ignore */
427
+ encoding.maxFrameRate = 2;
428
+ }
429
+ }
430
+ }
431
+ });
432
+ }
388
433
 
389
434
  if (hasChanged) {
390
435
  params.encodings = encodings;
436
+ log.debug(`setting encodings`, params.encodings);
391
437
  await sender.setParameters(params);
392
438
  }
393
439
  } finally {
@@ -412,6 +458,7 @@ export function videoLayersFromEncodings(
412
458
  width: number,
413
459
  height: number,
414
460
  encodings?: RTCRtpEncodingParameters[],
461
+ svc?: boolean,
415
462
  ): VideoLayer[] {
416
463
  // default to a single layer, HQ
417
464
  if (!encodings) {
@@ -425,6 +472,24 @@ export function videoLayersFromEncodings(
425
472
  },
426
473
  ];
427
474
  }
475
+
476
+ if (svc) {
477
+ // svc layers
478
+ /* @ts-ignore */
479
+ const sm = new ScalabilityMode(encodings[0].scalabilityMode);
480
+ const layers = [];
481
+ for (let i = 0; i < sm.spatial; i += 1) {
482
+ layers.push({
483
+ quality: VideoQuality.HIGH - i,
484
+ width: width / 2 ** i,
485
+ height: height / 2 ** i,
486
+ bitrate: encodings[0].maxBitrate ? encodings[0].maxBitrate / 3 ** i : 0,
487
+ ssrc: 0,
488
+ });
489
+ }
490
+ return layers;
491
+ }
492
+
428
493
  return encodings.map((encoding) => {
429
494
  const scale = encoding.scaleResolutionDownBy ?? 1;
430
495
  let quality = videoQualityForRid(encoding.rid ?? '');
@@ -1,5 +1,5 @@
1
1
  import log from '../../logger';
2
- import { TrackInfo, VideoQuality } from '../../proto/livekit_models';
2
+ import { SubscriptionError, TrackInfo, VideoQuality } from '../../proto/livekit_models';
3
3
  import { UpdateSubscription, UpdateTrackSettings } from '../../proto/livekit_rtc';
4
4
  import { TrackEvent } from '../events';
5
5
  import type RemoteTrack from './RemoteTrack';
@@ -24,6 +24,8 @@ export default class RemoteTrackPublication extends TrackPublication {
24
24
 
25
25
  protected fps?: number;
26
26
 
27
+ protected subscriptionError?: SubscriptionError;
28
+
27
29
  constructor(kind: Track.Kind, ti: TrackInfo, autoSubscribe: boolean | undefined) {
28
30
  super(kind, ti.sid, ti.name);
29
31
  this.subscribed = autoSubscribe;
@@ -205,6 +207,11 @@ export default class RemoteTrackPublication extends TrackPublication {
205
207
  this.emitSubscriptionUpdateIfChanged(prevStatus);
206
208
  }
207
209
 
210
+ /** @internal */
211
+ setSubscriptionError(error: SubscriptionError) {
212
+ this.emit(TrackEvent.SubscriptionFailed, error);
213
+ }
214
+
208
215
  /** @internal */
209
216
  updateInfo(info: TrackInfo) {
210
217
  super.updateInfo(info);
@@ -1,11 +1,11 @@
1
1
  import { debounce } from 'ts-debounce';
2
2
  import log from '../../logger';
3
3
  import { TrackEvent } from '../events';
4
- import { computeBitrate } from '../stats';
5
4
  import type { VideoReceiverStats } from '../stats';
5
+ import { computeBitrate } from '../stats';
6
6
  import CriticalTimers from '../timers';
7
- import { getDevicePixelRatio, getIntersectionObserver, getResizeObserver, isWeb } from '../utils';
8
7
  import type { ObservableMediaElement } from '../utils';
8
+ import { getDevicePixelRatio, getIntersectionObserver, getResizeObserver, isWeb } from '../utils';
9
9
  import RemoteTrack from './RemoteTrack';
10
10
  import { Track, attachToElement, detachTrack } from './Track';
11
11
  import type { AdaptiveStreamSettings } from './types';
@@ -248,11 +248,10 @@ export default class RemoteVideoTrack extends RemoteTrack {
248
248
  private updateDimensions() {
249
249
  let maxWidth = 0;
250
250
  let maxHeight = 0;
251
+ const pixelDensity = this.getPixelDensity();
251
252
  for (const info of this.elementInfos) {
252
- const pixelDensity = this.adaptiveStreamSettings?.pixelDensity ?? 1;
253
- const pixelDensityValue = pixelDensity === 'screen' ? getDevicePixelRatio() : pixelDensity;
254
- const currentElementWidth = info.width() * pixelDensityValue;
255
- const currentElementHeight = info.height() * pixelDensityValue;
253
+ const currentElementWidth = info.width() * pixelDensity;
254
+ const currentElementHeight = info.height() * pixelDensity;
256
255
  if (currentElementWidth + currentElementHeight > maxWidth + maxHeight) {
257
256
  maxWidth = currentElementWidth;
258
257
  maxHeight = currentElementHeight;
@@ -270,6 +269,24 @@ export default class RemoteVideoTrack extends RemoteTrack {
270
269
 
271
270
  this.emit(TrackEvent.VideoDimensionsChanged, this.lastDimensions, this);
272
271
  }
272
+
273
+ private getPixelDensity(): number {
274
+ const pixelDensity = this.adaptiveStreamSettings?.pixelDensity;
275
+ if (pixelDensity === 'screen') {
276
+ return getDevicePixelRatio();
277
+ } else if (!pixelDensity) {
278
+ // when unset, we'll pick a sane default here.
279
+ // for higher pixel density devices (mobile phones, etc), we'll use 2
280
+ // otherwise it defaults to 1
281
+ const devicePixelRatio = getDevicePixelRatio();
282
+ if (devicePixelRatio > 2) {
283
+ return 2;
284
+ } else {
285
+ return 1;
286
+ }
287
+ }
288
+ return pixelDensity;
289
+ }
273
290
  }
274
291
 
275
292
  export interface ElementInfo {
@@ -1,5 +1,4 @@
1
- import { EventEmitter } from 'events';
2
- import type TypedEventEmitter from 'typed-emitter';
1
+ import EventEmitter from 'eventemitter3';
3
2
  import type { SignalClient } from '../../api/SignalClient';
4
3
  import log from '../../logger';
5
4
  import { TrackSource, TrackType } from '../../proto/livekit_models';
@@ -13,7 +12,7 @@ const BACKGROUND_REACTION_DELAY = 5000;
13
12
  // Safari tracks which audio elements have been "blessed" by the user.
14
13
  const recycledElements: Array<HTMLAudioElement> = [];
15
14
 
16
- export abstract class Track extends (EventEmitter as new () => TypedEventEmitter<TrackEventCallbacks>) {
15
+ export abstract class Track extends EventEmitter<TrackEventCallbacks> {
17
16
  kind: Track.Kind;
18
17
 
19
18
  attachedElements: HTMLMediaElement[] = [];
@@ -52,7 +51,6 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
52
51
 
53
52
  protected constructor(mediaTrack: MediaStreamTrack, kind: Track.Kind) {
54
53
  super();
55
- this.setMaxListeners(100);
56
54
  this.kind = kind;
57
55
  this._mediaStreamTrack = mediaTrack;
58
56
  this._mediaStreamID = mediaTrack.id;
@@ -118,7 +116,7 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
118
116
  // even if we believe it's already attached to the element, it's possible
119
117
  // the element's srcObject was set to something else out of band.
120
118
  // we'll want to re-attach it in that case
121
- attachToElement(this._mediaStreamTrack, element);
119
+ attachToElement(this.mediaStreamTrack, element);
122
120
 
123
121
  // handle auto playback failures
124
122
  const allMediaStreamTracks = (element.srcObject as MediaStream).getTracks();
@@ -167,7 +165,7 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
167
165
  try {
168
166
  // detach from a single element
169
167
  if (element) {
170
- detachTrack(this._mediaStreamTrack, element);
168
+ detachTrack(this.mediaStreamTrack, element);
171
169
  const idx = this.attachedElements.indexOf(element);
172
170
  if (idx >= 0) {
173
171
  this.attachedElements.splice(idx, 1);
@@ -179,7 +177,7 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
179
177
 
180
178
  const detached: HTMLMediaElement[] = [];
181
179
  this.attachedElements.forEach((elm) => {
182
- detachTrack(this._mediaStreamTrack, elm);
180
+ detachTrack(this.mediaStreamTrack, elm);
183
181
  detached.push(elm);
184
182
  this.recycleElement(elm);
185
183
  this.emit(TrackEvent.ElementDetached, elm);
@@ -1,7 +1,6 @@
1
- import { EventEmitter } from 'events';
2
- import type TypedEventEmitter from 'typed-emitter';
1
+ import EventEmitter from 'eventemitter3';
3
2
  import log from '../../logger';
4
- import type { TrackInfo } from '../../proto/livekit_models';
3
+ import type { SubscriptionError, TrackInfo } from '../../proto/livekit_models';
5
4
  import type { UpdateSubscription, UpdateTrackSettings } from '../../proto/livekit_rtc';
6
5
  import { TrackEvent } from '../events';
7
6
  import LocalAudioTrack from './LocalAudioTrack';
@@ -11,7 +10,7 @@ import type RemoteTrack from './RemoteTrack';
11
10
  import RemoteVideoTrack from './RemoteVideoTrack';
12
11
  import { Track } from './Track';
13
12
 
14
- export class TrackPublication extends (EventEmitter as new () => TypedEventEmitter<PublicationEventCallbacks>) {
13
+ export class TrackPublication extends EventEmitter<PublicationEventCallbacks> {
15
14
  kind: Track.Kind;
16
15
 
17
16
  trackName: string;
@@ -38,7 +37,6 @@ export class TrackPublication extends (EventEmitter as new () => TypedEventEmitt
38
37
 
39
38
  constructor(kind: Track.Kind, id: string, name: string) {
40
39
  super();
41
- this.setMaxListeners(100);
42
40
  this.kind = kind;
43
41
  this.trackSid = id;
44
42
  this.trackName = name;
@@ -146,4 +144,5 @@ export type PublicationEventCallbacks = {
146
144
  status: TrackPublication.SubscriptionStatus,
147
145
  prevStatus: TrackPublication.SubscriptionStatus,
148
146
  ) => void;
147
+ subscriptionFailed: (error: SubscriptionError) => void;
149
148
  };
@@ -63,10 +63,19 @@ export interface TrackPublishDefaults {
63
63
  scalabilityMode?: ScalabilityMode;
64
64
 
65
65
  /**
66
- * custom video simulcast layers for camera tracks, defaults to h180, h360
67
- * You can specify up to two custom layers that will be used instead of
68
- * the LiveKit default layers.
69
- * Note: the layers need to be ordered from lowest to highest quality
66
+ * Up to two additional simulcast layers to publish in addition to the original
67
+ * Track.
68
+ * When left blank, it defaults to h180, h360.
69
+ * If a SVC codec is used (VP9 or AV1), this field has no effect.
70
+ *
71
+ * To publish three total layers, you would specify:
72
+ * {
73
+ * videoEncoding: {...}, // encoding of the primary layer
74
+ * videoSimulcastLayers: [
75
+ * VideoPresets.h540,
76
+ * VideoPresets.h216,
77
+ * ],
78
+ * }
70
79
  */
71
80
  videoSimulcastLayers?: Array<VideoPreset>;
72
81
 
@@ -284,7 +293,7 @@ export function isCodecEqual(c1: string | undefined, c2: string | undefined): bo
284
293
  /**
285
294
  * scalability modes for svc, only supprot l3t3 now.
286
295
  */
287
- export type ScalabilityMode = 'L3T3';
296
+ export type ScalabilityMode = 'L3T3' | 'L3T3_KEY';
288
297
 
289
298
  export namespace AudioPresets {
290
299
  export const telephone: AudioPreset = {