livekit-client 1.10.0 → 1.11.1

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.esm.mjs +504 -527
  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/connectionHelper/ConnectionCheck.d.ts +2 -3
  6. package/dist/src/connectionHelper/ConnectionCheck.d.ts.map +1 -1
  7. package/dist/src/connectionHelper/checks/Checker.d.ts +2 -3
  8. package/dist/src/connectionHelper/checks/Checker.d.ts.map +1 -1
  9. package/dist/src/room/PCTransport.d.ts +1 -1
  10. package/dist/src/room/PCTransport.d.ts.map +1 -1
  11. package/dist/src/room/RTCEngine.d.ts +2 -4
  12. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  13. package/dist/src/room/Room.d.ts +3 -4
  14. package/dist/src/room/Room.d.ts.map +1 -1
  15. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  16. package/dist/src/room/participant/Participant.d.ts +2 -4
  17. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  18. package/dist/src/room/participant/RemoteParticipant.d.ts +2 -1
  19. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  20. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  21. package/dist/src/room/track/LocalTrack.d.ts +3 -2
  22. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  23. package/dist/src/room/track/LocalVideoTrack.d.ts +1 -1
  24. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  25. package/dist/src/room/track/RemoteVideoTrack.d.ts +1 -0
  26. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  27. package/dist/src/room/track/Track.d.ts +2 -4
  28. package/dist/src/room/track/Track.d.ts.map +1 -1
  29. package/dist/src/room/track/TrackPublication.d.ts +2 -4
  30. package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
  31. package/dist/src/room/track/options.d.ts +13 -4
  32. package/dist/src/room/track/options.d.ts.map +1 -1
  33. package/dist/src/room/track/types.d.ts +2 -1
  34. package/dist/src/room/track/types.d.ts.map +1 -1
  35. package/dist/src/room/utils.d.ts.map +1 -1
  36. package/dist/ts4.2/src/connectionHelper/ConnectionCheck.d.ts +2 -3
  37. package/dist/ts4.2/src/connectionHelper/checks/Checker.d.ts +2 -3
  38. package/dist/ts4.2/src/room/PCTransport.d.ts +1 -1
  39. package/dist/ts4.2/src/room/RTCEngine.d.ts +2 -4
  40. package/dist/ts4.2/src/room/Room.d.ts +3 -4
  41. package/dist/ts4.2/src/room/participant/Participant.d.ts +2 -4
  42. package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +2 -1
  43. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +3 -2
  44. package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +1 -1
  45. package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +1 -0
  46. package/dist/ts4.2/src/room/track/Track.d.ts +2 -4
  47. package/dist/ts4.2/src/room/track/TrackPublication.d.ts +2 -4
  48. package/dist/ts4.2/src/room/track/options.d.ts +13 -4
  49. package/dist/ts4.2/src/room/track/types.d.ts +2 -1
  50. package/package.json +2 -3
  51. package/src/connectionHelper/ConnectionCheck.ts +2 -3
  52. package/src/connectionHelper/checks/Checker.ts +2 -3
  53. package/src/logger.ts +4 -4
  54. package/src/room/PCTransport.ts +1 -1
  55. package/src/room/RTCEngine.ts +4 -4
  56. package/src/room/Room.ts +41 -11
  57. package/src/room/participant/LocalParticipant.ts +11 -3
  58. package/src/room/participant/Participant.ts +2 -4
  59. package/src/room/participant/RemoteParticipant.ts +4 -3
  60. package/src/room/participant/publishUtils.ts +16 -18
  61. package/src/room/track/LocalTrack.ts +68 -46
  62. package/src/room/track/LocalVideoTrack.ts +9 -7
  63. package/src/room/track/RemoteVideoTrack.ts +23 -6
  64. package/src/room/track/Track.ts +2 -4
  65. package/src/room/track/TrackPublication.ts +2 -4
  66. package/src/room/track/options.ts +13 -4
  67. package/src/room/track/types.ts +2 -1
  68. package/src/room/utils.ts +6 -3
@@ -47,12 +47,16 @@ export default abstract class LocalTrack extends Track {
47
47
  userProvidedTrack = false,
48
48
  ) {
49
49
  super(mediaTrack, kind);
50
- this._mediaStreamTrack.addEventListener('ended', this.handleEnded);
51
- this.constraints = constraints ?? mediaTrack.getConstraints();
52
50
  this.reacquireTrack = false;
53
51
  this.providedByUser = userProvidedTrack;
54
52
  this.muteLock = new Mutex();
55
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
+ }
56
60
  }
57
61
 
58
62
  get id(): string {
@@ -88,6 +92,50 @@ export default abstract class LocalTrack extends Track {
88
92
  return this.processor?.processedTrack ?? this._mediaStreamTrack;
89
93
  }
90
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', this.pauseUpstream);
121
+ newTrack.addEventListener('unmute', this.resumeUpstream);
122
+ this.constraints = newTrack.getConstraints();
123
+ }
124
+ if (this.sender) {
125
+ await this.sender.replaceTrack(newTrack);
126
+ }
127
+ this._mediaStreamTrack = newTrack;
128
+ if (newTrack) {
129
+ // sync muted state with the enabled state of the newly provided track
130
+ this._mediaStreamTrack.enabled = !this.isMuted;
131
+ // when a valid track is replace, we'd want to start producing
132
+ await this.resumeUpstream();
133
+ this.attachedElements.forEach((el) => {
134
+ attachToElement(newTrack, el);
135
+ });
136
+ }
137
+ }
138
+
91
139
  async waitForDimensions(timeout = defaultDimensionsTimeout): Promise<Track.Dimensions> {
92
140
  if (this.kind === Track.Kind.Audio) {
93
141
  throw new Error('cannot get dimensions for audio tracks');
@@ -133,37 +181,12 @@ export default abstract class LocalTrack extends Track {
133
181
  throw new TrackInvalidError('unable to replace an unpublished track');
134
182
  }
135
183
 
136
- // detach
137
- this.attachedElements.forEach((el) => {
138
- detachTrack(this._mediaStreamTrack, el);
139
- });
140
- this._mediaStreamTrack.removeEventListener('ended', this.handleEnded);
141
- // on Safari, the old audio track must be stopped before attempting to acquire
142
- // the new track, otherwise the new track will stop with
143
- // 'A MediaStreamTrack ended due to a capture failure`
144
- if (!this.providedByUser) {
145
- this._mediaStreamTrack.stop();
146
- }
147
-
148
- track.addEventListener('ended', this.handleEnded);
149
184
  log.debug('replace MediaStreamTrack');
150
-
151
- if (this.sender) {
152
- await this.sender.replaceTrack(track);
153
- }
154
- this._mediaStreamTrack = track;
155
-
156
- // sync muted state with the enabled state of the newly provided track
157
- this._mediaStreamTrack.enabled = !this.isMuted;
158
-
159
- await this.resumeUpstream();
160
-
161
- this.attachedElements.forEach((el) => {
162
- attachToElement(track, el);
163
- });
164
-
165
- this.mediaStream = new MediaStream([track]);
185
+ this.setMediaStreamTrack(track);
186
+ // this must be synced *after* setting mediaStreamTrack above, since it relies
187
+ // on the previous state in order to cleanup
166
188
  this.providedByUser = userProvidedTrack;
189
+
167
190
  if (this.processor) {
168
191
  await this.stopProcessor();
169
192
  }
@@ -187,7 +210,8 @@ export default abstract class LocalTrack extends Track {
187
210
  streamConstraints.audio = constraints;
188
211
  }
189
212
 
190
- // detach
213
+ // these steps are duplicated from setMediaStreamTrack because we must stop
214
+ // the previous tracks before new tracks can be acquired
191
215
  this.attachedElements.forEach((el) => {
192
216
  detachTrack(this.mediaStreamTrack, el);
193
217
  });
@@ -203,16 +227,7 @@ export default abstract class LocalTrack extends Track {
203
227
  newTrack.addEventListener('ended', this.handleEnded);
204
228
  log.debug('re-acquired MediaStreamTrack');
205
229
 
206
- if (this.sender) {
207
- // Track can be restarted after it's unpublished
208
- await this.sender.replaceTrack(newTrack);
209
- }
210
-
211
- this._mediaStreamTrack = newTrack;
212
-
213
- await this.resumeUpstream();
214
-
215
- this.mediaStream = mediaStream;
230
+ this.setMediaStreamTrack(newTrack);
216
231
  this.constraints = constraints;
217
232
  if (this.processor) {
218
233
  const processor = this.processor;
@@ -263,11 +278,17 @@ export default abstract class LocalTrack extends Track {
263
278
  if (this.isInBackground) {
264
279
  this.reacquireTrack = true;
265
280
  }
281
+ this._mediaStreamTrack.removeEventListener('mute', this.pauseUpstream);
282
+ this._mediaStreamTrack.removeEventListener('unmute', this.resumeUpstream);
266
283
  this.emit(TrackEvent.Ended, this);
267
284
  };
268
285
 
269
286
  stop() {
270
287
  super.stop();
288
+
289
+ this._mediaStreamTrack.removeEventListener('ended', this.handleEnded);
290
+ this._mediaStreamTrack.removeEventListener('mute', this.pauseUpstream);
291
+ this._mediaStreamTrack.removeEventListener('unmute', this.resumeUpstream);
271
292
  this.processor?.destroy();
272
293
  this.processor = undefined;
273
294
  }
@@ -278,7 +299,7 @@ export default abstract class LocalTrack extends Track {
278
299
  * the server.
279
300
  * this API is unsupported on Safari < 12 due to a bug
280
301
  **/
281
- async pauseUpstream() {
302
+ pauseUpstream = async () => {
282
303
  const unlock = await this.pauseUpstreamLock.lock();
283
304
  try {
284
305
  if (this._isUpstreamPaused === true) {
@@ -300,9 +321,9 @@ export default abstract class LocalTrack extends Track {
300
321
  } finally {
301
322
  unlock();
302
323
  }
303
- }
324
+ };
304
325
 
305
- async resumeUpstream() {
326
+ resumeUpstream = async () => {
306
327
  const unlock = await this.pauseUpstreamLock.lock();
307
328
  try {
308
329
  if (this._isUpstreamPaused === false) {
@@ -315,11 +336,12 @@ export default abstract class LocalTrack extends Track {
315
336
  this._isUpstreamPaused = false;
316
337
  this.emit(TrackEvent.UpstreamResumed, this);
317
338
 
339
+ // this operation is noop if mediastreamtrack is already being sent
318
340
  await this.sender.replaceTrack(this._mediaStreamTrack);
319
341
  } finally {
320
342
  unlock();
321
343
  }
322
- }
344
+ };
323
345
 
324
346
  /**
325
347
  * Sets a processor on this track.
@@ -351,8 +351,13 @@ async function setPublishingLayersForSender(
351
351
 
352
352
  let hasChanged = false;
353
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;
354
359
  /* @ts-ignore */
355
- if (encodings.length === 1 && encodings[0].scalabilityMode) {
360
+ if (closableSpatial && encodings[0].scalabilityMode) {
356
361
  // svc dynacast encodings
357
362
  const encoding = encodings[0];
358
363
  /* @ts-ignore */
@@ -372,10 +377,7 @@ async function setPublishingLayersForSender(
372
377
  } else if (!encoding.active /* || mode.spatial !== maxQuality + 1*/) {
373
378
  hasChanged = true;
374
379
  encoding.active = true;
375
- /* disable closable spatial layer as it has video blur/frozen issue with current server/client
376
- 1. chrome 113: when switching to up layer with scalability Mode change, it will generate a
377
- low resolution frame and recover very quickly, but noticable
378
- 2. livekit sfu: additional pli request cause video frozen for a few frames, also noticable
380
+ /*
379
381
  @ts-ignore
380
382
  const originalMode = new ScalabilityMode(senderEncodings[0].scalabilityMode)
381
383
  mode.spatial = maxQuality + 1;
@@ -456,6 +458,7 @@ export function videoLayersFromEncodings(
456
458
  width: number,
457
459
  height: number,
458
460
  encodings?: RTCRtpEncodingParameters[],
461
+ svc?: boolean,
459
462
  ): VideoLayer[] {
460
463
  // default to a single layer, HQ
461
464
  if (!encodings) {
@@ -470,8 +473,7 @@ export function videoLayersFromEncodings(
470
473
  ];
471
474
  }
472
475
 
473
- /* @ts-ignore */
474
- if (encodings.length === 1 && encodings[0].scalabilityMode) {
476
+ if (svc) {
475
477
  // svc layers
476
478
  /* @ts-ignore */
477
479
  const sm = new ScalabilityMode(encodings[0].scalabilityMode);
@@ -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;
@@ -1,5 +1,4 @@
1
- import { EventEmitter } from 'events';
2
- import type TypedEventEmitter from 'typed-emitter';
1
+ import EventEmitter from 'eventemitter3';
3
2
  import log from '../../logger';
4
3
  import type { SubscriptionError, TrackInfo } from '../../proto/livekit_models';
5
4
  import type { UpdateSubscription, UpdateTrackSettings } from '../../proto/livekit_rtc';
@@ -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;
@@ -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
 
@@ -8,7 +8,8 @@ export type VideoTrack = RemoteVideoTrack | LocalVideoTrack;
8
8
 
9
9
  export type AdaptiveStreamSettings = {
10
10
  /**
11
- * Set a custom pixel density, defaults to 1
11
+ * Set a custom pixel density. Defaults to 2 for high density screens (3+) or
12
+ * 1 otherwise.
12
13
  * When streaming videos on a ultra high definition screen this setting
13
14
  * let's you account for the devicePixelRatio of those screens.
14
15
  * Set it to `screen` to use the actual pixel density of the screen
package/src/room/utils.ts CHANGED
@@ -261,7 +261,7 @@ export function getEmptyVideoStreamTrack() {
261
261
  if (!emptyVideoStreamTrack) {
262
262
  emptyVideoStreamTrack = createDummyVideoStreamTrack();
263
263
  }
264
- return emptyVideoStreamTrack;
264
+ return emptyVideoStreamTrack.clone();
265
265
  }
266
266
 
267
267
  export function createDummyVideoStreamTrack(
@@ -301,8 +301,11 @@ export function getEmptyAudioStreamTrack() {
301
301
  // implementation adapted from https://blog.mozilla.org/webrtc/warm-up-with-replacetrack/
302
302
  const ctx = new AudioContext();
303
303
  const oscillator = ctx.createOscillator();
304
+ const gain = ctx.createGain();
305
+ gain.gain.setValueAtTime(0, 0);
304
306
  const dst = ctx.createMediaStreamDestination();
305
- oscillator.connect(dst);
307
+ oscillator.connect(gain);
308
+ gain.connect(dst);
306
309
  oscillator.start();
307
310
  [emptyAudioStreamTrack] = dst.stream.getAudioTracks();
308
311
  if (!emptyAudioStreamTrack) {
@@ -310,7 +313,7 @@ export function getEmptyAudioStreamTrack() {
310
313
  }
311
314
  emptyAudioStreamTrack.enabled = false;
312
315
  }
313
- return emptyAudioStreamTrack;
316
+ return emptyAudioStreamTrack.clone();
314
317
  }
315
318
 
316
319
  export class Future<T> {