livekit-client 1.7.1 → 1.8.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 (73) hide show
  1. package/README.md +20 -1
  2. package/dist/livekit-client.esm.mjs +2178 -1060
  3. package/dist/livekit-client.esm.mjs.map +1 -1
  4. package/dist/livekit-client.umd.js +1 -1
  5. package/dist/livekit-client.umd.js.map +1 -1
  6. package/dist/src/index.d.ts +3 -1
  7. package/dist/src/index.d.ts.map +1 -1
  8. package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -1
  9. package/dist/src/proto/livekit_models.d.ts +32 -0
  10. package/dist/src/proto/livekit_models.d.ts.map +1 -1
  11. package/dist/src/proto/livekit_rtc.d.ts +315 -75
  12. package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
  13. package/dist/src/room/RTCEngine.d.ts +8 -1
  14. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  15. package/dist/src/room/ReconnectPolicy.d.ts +1 -0
  16. package/dist/src/room/ReconnectPolicy.d.ts.map +1 -1
  17. package/dist/src/room/RegionUrlProvider.d.ts +14 -0
  18. package/dist/src/room/RegionUrlProvider.d.ts.map +1 -0
  19. package/dist/src/room/Room.d.ts +4 -0
  20. package/dist/src/room/Room.d.ts.map +1 -1
  21. package/dist/src/room/errors.d.ts +2 -1
  22. package/dist/src/room/errors.d.ts.map +1 -1
  23. package/dist/src/room/events.d.ts +8 -2
  24. package/dist/src/room/events.d.ts.map +1 -1
  25. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  26. package/dist/src/room/track/LocalTrack.d.ts +3 -2
  27. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  28. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  29. package/dist/src/room/track/RemoteTrackPublication.d.ts +1 -1
  30. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  31. package/dist/src/room/track/RemoteVideoTrack.d.ts +1 -1
  32. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  33. package/dist/src/room/track/Track.d.ts +3 -1
  34. package/dist/src/room/track/Track.d.ts.map +1 -1
  35. package/dist/src/room/types.d.ts +4 -0
  36. package/dist/src/room/types.d.ts.map +1 -1
  37. package/dist/src/room/utils.d.ts +4 -0
  38. package/dist/src/room/utils.d.ts.map +1 -1
  39. package/dist/ts4.2/src/index.d.ts +3 -1
  40. package/dist/ts4.2/src/proto/livekit_models.d.ts +32 -0
  41. package/dist/ts4.2/src/proto/livekit_rtc.d.ts +348 -84
  42. package/dist/ts4.2/src/room/RTCEngine.d.ts +8 -1
  43. package/dist/ts4.2/src/room/ReconnectPolicy.d.ts +1 -0
  44. package/dist/ts4.2/src/room/RegionUrlProvider.d.ts +14 -0
  45. package/dist/ts4.2/src/room/Room.d.ts +4 -0
  46. package/dist/ts4.2/src/room/errors.d.ts +2 -1
  47. package/dist/ts4.2/src/room/events.d.ts +8 -2
  48. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +3 -2
  49. package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +1 -1
  50. package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +1 -1
  51. package/dist/ts4.2/src/room/track/Track.d.ts +3 -1
  52. package/dist/ts4.2/src/room/types.d.ts +4 -0
  53. package/dist/ts4.2/src/room/utils.d.ts +4 -0
  54. package/package.json +19 -19
  55. package/src/api/SignalClient.ts +4 -4
  56. package/src/index.ts +3 -0
  57. package/src/proto/google/protobuf/timestamp.ts +15 -6
  58. package/src/proto/livekit_models.ts +903 -222
  59. package/src/proto/livekit_rtc.ts +1053 -279
  60. package/src/room/RTCEngine.ts +143 -40
  61. package/src/room/ReconnectPolicy.ts +2 -0
  62. package/src/room/RegionUrlProvider.ts +73 -0
  63. package/src/room/Room.ts +201 -132
  64. package/src/room/errors.ts +1 -0
  65. package/src/room/events.ts +7 -0
  66. package/src/room/track/LocalAudioTrack.ts +13 -6
  67. package/src/room/track/LocalTrack.ts +22 -8
  68. package/src/room/track/LocalVideoTrack.ts +12 -6
  69. package/src/room/track/RemoteTrackPublication.ts +4 -3
  70. package/src/room/track/RemoteVideoTrack.ts +5 -4
  71. package/src/room/track/Track.ts +46 -31
  72. package/src/room/types.ts +6 -0
  73. package/src/room/utils.ts +53 -0
package/src/room/Room.ts CHANGED
@@ -37,14 +37,13 @@ import {
37
37
  videoDefaults,
38
38
  } from './defaults';
39
39
  import DeviceManager from './DeviceManager';
40
- import { ConnectionError, UnsupportedServer } from './errors';
40
+ import { ConnectionError, ConnectionErrorReason, UnsupportedServer } from './errors';
41
41
  import { EngineEvent, ParticipantEvent, RoomEvent, TrackEvent } from './events';
42
42
  import LocalParticipant from './participant/LocalParticipant';
43
43
  import type Participant from './participant/Participant';
44
44
  import type { ConnectionQuality } from './participant/Participant';
45
45
  import RemoteParticipant from './participant/RemoteParticipant';
46
46
  import RTCEngine from './RTCEngine';
47
- import CriticalTimers from './timers';
48
47
  import LocalAudioTrack from './track/LocalAudioTrack';
49
48
  import LocalTrackPublication from './track/LocalTrackPublication';
50
49
  import LocalVideoTrack from './track/LocalVideoTrack';
@@ -59,11 +58,13 @@ import {
59
58
  createDummyVideoStreamTrack,
60
59
  Future,
61
60
  getEmptyAudioStreamTrack,
61
+ isCloud,
62
62
  isWeb,
63
63
  Mutex,
64
64
  supportsSetSinkId,
65
65
  unpackStreamId,
66
66
  } from './utils';
67
+ import { RegionUrlProvider } from './RegionUrlProvider';
67
68
 
68
69
  export enum ConnectionState {
69
70
  Disconnected = 'disconnected',
@@ -206,7 +207,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
206
207
  }
207
208
  })
208
209
  .on(EngineEvent.Restarting, this.handleRestarting)
209
- .on(EngineEvent.Restarted, this.handleRestarted);
210
+ .on(EngineEvent.Restarted, this.handleRestarted)
211
+ .on(EngineEvent.DCBufferStatusChanged, (status, kind) => {
212
+ this.emit(RoomEvent.DCBufferStatusChanged, status, kind);
213
+ });
210
214
 
211
215
  if (this.localParticipant) {
212
216
  this.localParticipant.setupEngine(this.engine);
@@ -258,155 +262,219 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
258
262
 
259
263
  this.setAndEmitConnectionState(ConnectionState.Connecting);
260
264
 
261
- const connectFn = async (resolve: () => void, reject: (reason: any) => void) => {
262
- if (!this.abortController || this.abortController.signal.aborted) {
263
- this.abortController = new AbortController();
265
+ const urlProvider = new RegionUrlProvider(url, token);
266
+
267
+ const connectFn = async (
268
+ resolve: () => void,
269
+ reject: (reason: any) => void,
270
+ regionUrl?: string,
271
+ ) => {
272
+ if (this.abortController) {
273
+ this.abortController.abort();
264
274
  }
275
+ this.abortController = new AbortController();
276
+
265
277
  // at this point the intention to connect has been signalled so we can allow cancelling of the connection via disconnect() again
266
- unlockDisconnect();
278
+ unlockDisconnect?.();
267
279
 
268
- if (this.state === ConnectionState.Reconnecting) {
269
- log.info('Reconnection attempt replaced by new connection attempt');
270
- // make sure we close and recreate the existing engine in order to get rid of any potentially ongoing reconnection attempts
271
- this.recreateEngine();
272
- } else {
273
- // create engine if previously disconnected
274
- this.maybeCreateEngine();
280
+ try {
281
+ await this.attemptConnection(regionUrl ?? url, token, opts, this.abortController);
282
+ this.abortController = undefined;
283
+ resolve();
284
+ } catch (e) {
285
+ if (
286
+ isCloud(new URL(url)) &&
287
+ e instanceof ConnectionError &&
288
+ e.reason !== ConnectionErrorReason.Cancelled
289
+ ) {
290
+ let nextUrl: string | null = null;
291
+ try {
292
+ nextUrl = await urlProvider.getNextBestRegionUrl(this.abortController?.signal);
293
+ } catch (error) {
294
+ if (
295
+ error instanceof ConnectionError &&
296
+ (error.status === 401 || error.reason === ConnectionErrorReason.Cancelled)
297
+ ) {
298
+ reject(error);
299
+ return;
300
+ }
301
+ }
302
+ if (nextUrl) {
303
+ log.debug('initial connection failed, retrying with another region');
304
+ await connectFn(resolve, reject, nextUrl);
305
+ } else {
306
+ reject(e);
307
+ }
308
+ } else {
309
+ reject(e);
310
+ }
275
311
  }
312
+ };
313
+ this.connectFuture = new Future(connectFn, () => {
314
+ this.clearConnectionFutures();
315
+ });
276
316
 
277
- this.acquireAudioContext();
278
-
279
- this.connOptions = { ...roomConnectOptionDefaults, ...opts } as InternalRoomConnectOptions;
317
+ return this.connectFuture.promise;
318
+ };
280
319
 
281
- if (this.connOptions.rtcConfig) {
282
- this.engine.rtcConfig = this.connOptions.rtcConfig;
283
- }
284
- if (this.connOptions.peerConnectionTimeout) {
285
- this.engine.peerConnectionTimeout = this.connOptions.peerConnectionTimeout;
286
- }
320
+ private connectSignal = async (
321
+ url: string,
322
+ token: string,
323
+ engine: RTCEngine,
324
+ connectOptions: InternalRoomConnectOptions,
325
+ roomOptions: InternalRoomOptions,
326
+ abortController: AbortController,
327
+ ): Promise<JoinResponse> => {
328
+ const joinResponse = await engine.join(
329
+ url,
330
+ token,
331
+ {
332
+ autoSubscribe: connectOptions.autoSubscribe,
333
+ publishOnly: connectOptions.publishOnly,
334
+ adaptiveStream:
335
+ typeof roomOptions.adaptiveStream === 'object' ? true : roomOptions.adaptiveStream,
336
+ maxRetries: connectOptions.maxRetries,
337
+ },
338
+ abortController.signal,
339
+ );
287
340
 
288
- try {
289
- const joinResponse = await this.engine.join(
290
- url,
291
- token,
292
- {
293
- autoSubscribe: this.connOptions.autoSubscribe,
294
- publishOnly: this.connOptions.publishOnly,
295
- adaptiveStream:
296
- typeof this.options.adaptiveStream === 'object' ? true : this.options.adaptiveStream,
297
- maxRetries: this.connOptions.maxRetries,
298
- },
299
- this.abortController.signal,
300
- );
341
+ let serverInfo: Partial<ServerInfo> | undefined = joinResponse.serverInfo;
342
+ if (!serverInfo) {
343
+ serverInfo = { version: joinResponse.serverVersion, region: joinResponse.serverRegion };
344
+ }
301
345
 
302
- let serverInfo: Partial<ServerInfo> | undefined = joinResponse.serverInfo;
303
- if (!serverInfo) {
304
- serverInfo = { version: joinResponse.serverVersion, region: joinResponse.serverRegion };
305
- }
346
+ log.debug(
347
+ `connected to Livekit Server ${Object.entries(serverInfo)
348
+ .map(([key, value]) => `${key}: ${value}`)
349
+ .join(', ')}`,
350
+ );
306
351
 
307
- log.debug(
308
- `connected to Livekit Server ${Object.entries(serverInfo)
309
- .map(([key, value]) => `${key}: ${value}`)
310
- .join(', ')}`,
311
- );
352
+ if (!joinResponse.serverVersion) {
353
+ throw new UnsupportedServer('unknown server version');
354
+ }
312
355
 
313
- if (!joinResponse.serverVersion) {
314
- throw new UnsupportedServer('unknown server version');
315
- }
356
+ if (joinResponse.serverVersion === '0.15.1' && this.options.dynacast) {
357
+ log.debug('disabling dynacast due to server version');
358
+ // dynacast has a bug in 0.15.1, so we cannot use it then
359
+ roomOptions.dynacast = false;
360
+ }
316
361
 
317
- if (joinResponse.serverVersion === '0.15.1' && this.options.dynacast) {
318
- log.debug('disabling dynacast due to server version');
319
- // dynacast has a bug in 0.15.1, so we cannot use it then
320
- this.options.dynacast = false;
321
- }
362
+ return joinResponse;
363
+ };
322
364
 
323
- const pi = joinResponse.participant!;
365
+ private applyJoinResponse = (joinResponse: JoinResponse) => {
366
+ const pi = joinResponse.participant!;
324
367
 
325
- this.localParticipant.sid = pi.sid;
326
- this.localParticipant.identity = pi.identity;
368
+ this.localParticipant.sid = pi.sid;
369
+ this.localParticipant.identity = pi.identity;
327
370
 
328
- this.localParticipant.updateInfo(pi);
329
- // forward metadata changed for the local participant
330
- this.setupLocalParticipantEvents();
371
+ this.localParticipant.updateInfo(pi);
372
+ // forward metadata changed for the local participant
373
+ this.setupLocalParticipantEvents();
331
374
 
332
- // populate remote participants, these should not trigger new events
333
- joinResponse.otherParticipants.forEach((info) => {
334
- if (
335
- info.sid !== this.localParticipant.sid &&
336
- info.identity !== this.localParticipant.identity
337
- ) {
338
- this.getOrCreateParticipant(info.sid, info);
339
- } else {
340
- log.warn('received info to create local participant as remote participant', {
341
- info,
342
- localParticipant: this.localParticipant,
343
- });
344
- }
375
+ // populate remote participants, these should not trigger new events
376
+ joinResponse.otherParticipants.forEach((info) => {
377
+ if (
378
+ info.sid !== this.localParticipant.sid &&
379
+ info.identity !== this.localParticipant.identity
380
+ ) {
381
+ this.getOrCreateParticipant(info.sid, info);
382
+ } else {
383
+ log.warn('received info to create local participant as remote participant', {
384
+ info,
385
+ localParticipant: this.localParticipant,
345
386
  });
346
-
347
- this.name = joinResponse.room!.name;
348
- this.sid = joinResponse.room!.sid;
349
- this.metadata = joinResponse.room!.metadata;
350
- if (this._isRecording !== joinResponse.room!.activeRecording) {
351
- this._isRecording = joinResponse.room!.activeRecording;
352
- this.emit(RoomEvent.RecordingStatusChanged, joinResponse.room!.activeRecording);
353
- }
354
- this.emit(RoomEvent.SignalConnected);
355
- } catch (err) {
356
- this.recreateEngine();
357
- this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
358
- const resultingError = new ConnectionError(`could not establish signal connection`);
359
- if (err instanceof Error) {
360
- resultingError.message = `${resultingError.message}: ${err.message}`;
361
- }
362
- if (err instanceof ConnectionError) {
363
- resultingError.reason = err.reason;
364
- resultingError.status = err.status;
365
- }
366
- log.debug(`error trying to establish signal connection`, { error: err });
367
- reject(resultingError);
368
- return;
369
387
  }
388
+ });
370
389
 
371
- // don't return until ICE connected
372
- const connectTimeout = CriticalTimers.setTimeout(() => {
373
- // timeout
374
- this.recreateEngine();
375
- this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
376
- reject(new ConnectionError('could not connect PeerConnection after timeout'));
377
- }, this.connOptions.peerConnectionTimeout);
378
- const abortHandler = () => {
379
- log.warn('closing engine');
380
- CriticalTimers.clearTimeout(connectTimeout);
381
- this.recreateEngine();
382
- this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
383
- reject(new ConnectionError('room connection has been cancelled'));
384
- };
385
- if (this.abortController?.signal.aborted) {
386
- abortHandler();
390
+ this.name = joinResponse.room!.name;
391
+ this.sid = joinResponse.room!.sid;
392
+ this.metadata = joinResponse.room!.metadata;
393
+ if (this._isRecording !== joinResponse.room!.activeRecording) {
394
+ this._isRecording = joinResponse.room!.activeRecording;
395
+ this.emit(RoomEvent.RecordingStatusChanged, joinResponse.room!.activeRecording);
396
+ }
397
+ };
398
+
399
+ private attemptConnection = async (
400
+ url: string,
401
+ token: string,
402
+ opts: RoomConnectOptions | undefined,
403
+ abortController: AbortController,
404
+ ) => {
405
+ if (this.state === ConnectionState.Reconnecting) {
406
+ log.info('Reconnection attempt replaced by new connection attempt');
407
+ // make sure we close and recreate the existing engine in order to get rid of any potentially ongoing reconnection attempts
408
+ this.recreateEngine();
409
+ } else {
410
+ // create engine if previously disconnected
411
+ this.maybeCreateEngine();
412
+ }
413
+
414
+ this.acquireAudioContext();
415
+
416
+ this.connOptions = { ...roomConnectOptionDefaults, ...opts } as InternalRoomConnectOptions;
417
+
418
+ if (this.connOptions.rtcConfig) {
419
+ this.engine.rtcConfig = this.connOptions.rtcConfig;
420
+ }
421
+ if (this.connOptions.peerConnectionTimeout) {
422
+ this.engine.peerConnectionTimeout = this.connOptions.peerConnectionTimeout;
423
+ }
424
+
425
+ try {
426
+ const joinResponse = await this.connectSignal(
427
+ url,
428
+ token,
429
+ this.engine,
430
+ this.connOptions,
431
+ this.options,
432
+ abortController,
433
+ );
434
+
435
+ this.applyJoinResponse(joinResponse);
436
+ this.emit(RoomEvent.SignalConnected);
437
+ } catch (err) {
438
+ this.recreateEngine();
439
+ this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
440
+ const resultingError = new ConnectionError(`could not establish signal connection`);
441
+ if (err instanceof Error) {
442
+ resultingError.message = `${resultingError.message}: ${err.message}`;
387
443
  }
388
- this.abortController?.signal.addEventListener('abort', abortHandler);
389
-
390
- this.engine.once(EngineEvent.Connected, () => {
391
- CriticalTimers.clearTimeout(connectTimeout);
392
- this.abortController?.signal.removeEventListener('abort', abortHandler);
393
- // also hook unload event
394
- if (isWeb() && this.options.disconnectOnPageLeave) {
395
- // capturing both 'pagehide' and 'beforeunload' to capture broadest set of browser behaviors
396
- window.addEventListener('pagehide', this.onPageLeave);
397
- window.addEventListener('beforeunload', this.onPageLeave);
398
- navigator.mediaDevices?.addEventListener('devicechange', this.handleDeviceChange);
399
- }
400
- this.setAndEmitConnectionState(ConnectionState.Connected);
401
- this.emit(RoomEvent.Connected);
402
- resolve();
403
- });
404
- };
405
- this.connectFuture = new Future(connectFn, () => {
406
- this.clearConnectionFutures();
407
- });
444
+ if (err instanceof ConnectionError) {
445
+ resultingError.reason = err.reason;
446
+ resultingError.status = err.status;
447
+ }
448
+ log.debug(`error trying to establish signal connection`, { error: err });
449
+ throw resultingError;
450
+ }
408
451
 
409
- return this.connectFuture.promise;
452
+ if (abortController.signal.aborted) {
453
+ this.recreateEngine();
454
+ this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
455
+ throw new ConnectionError(`Connection attempt aborted`);
456
+ }
457
+
458
+ try {
459
+ await this.engine.waitForPCInitialConnection(
460
+ this.connOptions.peerConnectionTimeout,
461
+ abortController,
462
+ );
463
+ } catch (e) {
464
+ this.recreateEngine();
465
+ this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
466
+ throw e;
467
+ }
468
+
469
+ // also hook unload event
470
+ if (isWeb() && this.options.disconnectOnPageLeave) {
471
+ // capturing both 'pagehide' and 'beforeunload' to capture broadest set of browser behaviors
472
+ window.addEventListener('pagehide', this.onPageLeave);
473
+ window.addEventListener('beforeunload', this.onPageLeave);
474
+ navigator.mediaDevices?.addEventListener('devicechange', this.handleDeviceChange);
475
+ }
476
+ this.setAndEmitConnectionState(ConnectionState.Connected);
477
+ this.emit(RoomEvent.Connected);
410
478
  };
411
479
 
412
480
  /**
@@ -1501,4 +1569,5 @@ export type RoomEventCallbacks = {
1501
1569
  audioPlaybackChanged: (playing: boolean) => void;
1502
1570
  signalConnected: () => void;
1503
1571
  recordingStatusChanged: (recording: boolean) => void;
1572
+ dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
1504
1573
  };
@@ -11,6 +11,7 @@ export const enum ConnectionErrorReason {
11
11
  NotAllowed,
12
12
  ServerUnreachable,
13
13
  InternalError,
14
+ Cancelled,
14
15
  }
15
16
 
16
17
  export class ConnectionError extends LivekitError {
@@ -264,6 +264,12 @@ export enum RoomEvent {
264
264
  * args: (isRecording: boolean)
265
265
  */
266
266
  RecordingStatusChanged = 'recordingStatusChanged',
267
+
268
+ /**
269
+ * Emits whenever the current buffer status of a data channel changes
270
+ * args: (isLow: boolean, kind: [[DataPacket_Kind]])
271
+ */
272
+ DCBufferStatusChanged = 'dcBufferStatusChanged',
267
273
  }
268
274
 
269
275
  export enum ParticipantEvent {
@@ -431,6 +437,7 @@ export enum EngineEvent {
431
437
  MediaTrackAdded = 'mediaTrackAdded',
432
438
  ActiveSpeakersUpdate = 'activeSpeakersUpdate',
433
439
  DataPacketReceived = 'dataPacketReceived',
440
+ DCBufferStatusChanged = 'dcBufferStatusChanged',
434
441
  }
435
442
 
436
443
  export enum TrackEvent {
@@ -39,7 +39,8 @@ export default class LocalAudioTrack extends LocalTrack {
39
39
  }
40
40
 
41
41
  async mute(): Promise<LocalAudioTrack> {
42
- await this.muteQueue.run(async () => {
42
+ const unlock = await this.muteLock.lock();
43
+ try {
43
44
  // disabled special handling as it will cause BT headsets to switch communication modes
44
45
  if (this.source === Track.Source.Microphone && this.stopOnMute && !this.isUserProvided) {
45
46
  log.debug('stopping mic track');
@@ -47,12 +48,15 @@ export default class LocalAudioTrack extends LocalTrack {
47
48
  this._mediaStreamTrack.stop();
48
49
  }
49
50
  await super.mute();
50
- });
51
- return this;
51
+ return this;
52
+ } finally {
53
+ unlock();
54
+ }
52
55
  }
53
56
 
54
57
  async unmute(): Promise<LocalAudioTrack> {
55
- await this.muteQueue.run(async () => {
58
+ const unlock = await this.muteLock.lock();
59
+ try {
56
60
  if (
57
61
  this.source === Track.Source.Microphone &&
58
62
  (this.stopOnMute || this._mediaStreamTrack.readyState === 'ended') &&
@@ -62,8 +66,11 @@ export default class LocalAudioTrack extends LocalTrack {
62
66
  await this.restartTrack();
63
67
  }
64
68
  await super.unmute();
65
- });
66
- return this;
69
+
70
+ return this;
71
+ } finally {
72
+ unlock();
73
+ }
67
74
  }
68
75
 
69
76
  async restartTrack(options?: AudioCaptureOptions) {
@@ -1,9 +1,14 @@
1
- import Queue from 'async-await-queue';
2
1
  import log from '../../logger';
3
2
  import DeviceManager from '../DeviceManager';
4
3
  import { TrackInvalidError } from '../errors';
5
4
  import { TrackEvent } from '../events';
6
- import { getEmptyAudioStreamTrack, getEmptyVideoStreamTrack, isMobile, sleep } from '../utils';
5
+ import {
6
+ getEmptyAudioStreamTrack,
7
+ getEmptyVideoStreamTrack,
8
+ isMobile,
9
+ Mutex,
10
+ sleep,
11
+ } from '../utils';
7
12
  import type { VideoCodec } from './options';
8
13
  import { attachToElement, detachTrack, Track } from './Track';
9
14
 
@@ -22,7 +27,9 @@ export default abstract class LocalTrack extends Track {
22
27
 
23
28
  protected providedByUser: boolean;
24
29
 
25
- protected muteQueue: Queue;
30
+ protected muteLock: Mutex;
31
+
32
+ protected pauseUpstreamLock: Mutex;
26
33
 
27
34
  /**
28
35
  *
@@ -42,7 +49,8 @@ export default abstract class LocalTrack extends Track {
42
49
  this.constraints = constraints ?? mediaTrack.getConstraints();
43
50
  this.reacquireTrack = false;
44
51
  this.providedByUser = userProvidedTrack;
45
- this.muteQueue = new Queue();
52
+ this.muteLock = new Mutex();
53
+ this.pauseUpstreamLock = new Mutex();
46
54
  }
47
55
 
48
56
  get id(): string {
@@ -246,7 +254,8 @@ export default abstract class LocalTrack extends Track {
246
254
  };
247
255
 
248
256
  async pauseUpstream() {
249
- this.muteQueue.run(async () => {
257
+ const unlock = await this.pauseUpstreamLock.lock();
258
+ try {
250
259
  if (this._isUpstreamPaused === true) {
251
260
  return;
252
261
  }
@@ -260,11 +269,14 @@ export default abstract class LocalTrack extends Track {
260
269
  const emptyTrack =
261
270
  this.kind === Track.Kind.Audio ? getEmptyAudioStreamTrack() : getEmptyVideoStreamTrack();
262
271
  await this.sender.replaceTrack(emptyTrack);
263
- });
272
+ } finally {
273
+ unlock();
274
+ }
264
275
  }
265
276
 
266
277
  async resumeUpstream() {
267
- this.muteQueue.run(async () => {
278
+ const unlock = await this.pauseUpstreamLock.lock();
279
+ try {
268
280
  if (this._isUpstreamPaused === false) {
269
281
  return;
270
282
  }
@@ -276,7 +288,9 @@ export default abstract class LocalTrack extends Track {
276
288
  this.emit(TrackEvent.UpstreamResumed, this);
277
289
 
278
290
  await this.sender.replaceTrack(this._mediaStreamTrack);
279
- });
291
+ } finally {
292
+ unlock();
293
+ }
280
294
  }
281
295
 
282
296
  protected abstract monitorSender(): void;
@@ -97,26 +97,32 @@ export default class LocalVideoTrack extends LocalTrack {
97
97
  }
98
98
 
99
99
  async mute(): Promise<LocalVideoTrack> {
100
- await this.muteQueue.run(async () => {
100
+ const unlock = await this.muteLock.lock();
101
+ try {
101
102
  if (this.source === Track.Source.Camera && !this.isUserProvided) {
102
103
  log.debug('stopping camera track');
103
104
  // also stop the track, so that camera indicator is turned off
104
105
  this._mediaStreamTrack.stop();
105
106
  }
106
107
  await super.mute();
107
- });
108
- return this;
108
+ return this;
109
+ } finally {
110
+ unlock();
111
+ }
109
112
  }
110
113
 
111
114
  async unmute(): Promise<LocalVideoTrack> {
112
- await this.muteQueue.run(async () => {
115
+ const unlock = await this.muteLock.lock();
116
+ try {
113
117
  if (this.source === Track.Source.Camera && !this.isUserProvided) {
114
118
  log.debug('reacquiring camera track');
115
119
  await this.restartTrack();
116
120
  }
117
121
  await super.unmute();
118
- });
119
- return this;
122
+ return this;
123
+ } finally {
124
+ unlock();
125
+ }
120
126
  }
121
127
 
122
128
  async getSenderStats(): Promise<VideoSenderStats[]> {
@@ -4,7 +4,7 @@ import { UpdateSubscription, UpdateTrackSettings } from '../../proto/livekit_rtc
4
4
  import { TrackEvent } from '../events';
5
5
  import type RemoteTrack from './RemoteTrack';
6
6
  import RemoteVideoTrack from './RemoteVideoTrack';
7
- import type { Track } from './Track';
7
+ import { Track } from './Track';
8
8
  import { TrackPublication } from './TrackPublication';
9
9
 
10
10
  export default class RemoteTrackPublication extends TrackPublication {
@@ -181,6 +181,7 @@ export default class RemoteTrackPublication extends TrackPublication {
181
181
  prevTrack.off(TrackEvent.VisibilityChanged, this.handleVisibilityChange);
182
182
  prevTrack.off(TrackEvent.Ended, this.handleEnded);
183
183
  prevTrack.detach();
184
+ prevTrack.stopMonitor();
184
185
  this.emit(TrackEvent.Unsubscribed, prevTrack);
185
186
  }
186
187
  super.setTrack(track);
@@ -238,8 +239,8 @@ export default class RemoteTrackPublication extends TrackPublication {
238
239
  }
239
240
 
240
241
  private isManualOperationAllowed(): boolean {
241
- if (this.isAdaptiveStream) {
242
- log.warn('adaptive stream is enabled, cannot change track settings', {
242
+ if (this.kind === Track.Kind.Video && this.isAdaptiveStream) {
243
+ log.warn('adaptive stream is enabled, cannot change video track settings', {
243
244
  trackSid: this.trackSid,
244
245
  });
245
246
  return false;
@@ -4,6 +4,7 @@ import { TrackEvent } from '../events';
4
4
  import { computeBitrate, VideoReceiverStats } from '../stats';
5
5
  import CriticalTimers from '../timers';
6
6
  import {
7
+ getDevicePixelRatio,
7
8
  getIntersectionObserver,
8
9
  getResizeObserver,
9
10
  isWeb,
@@ -26,7 +27,7 @@ export default class RemoteVideoTrack extends RemoteTrack {
26
27
 
27
28
  private lastDimensions?: Track.Dimensions;
28
29
 
29
- private hasUsedAttach: boolean = false;
30
+ private isObserved: boolean = false;
30
31
 
31
32
  constructor(
32
33
  mediaTrack: MediaStreamTrack,
@@ -43,7 +44,7 @@ export default class RemoteVideoTrack extends RemoteTrack {
43
44
  }
44
45
 
45
46
  get mediaStreamTrack() {
46
- if (this.isAdaptiveStream && !this.hasUsedAttach) {
47
+ if (this.isAdaptiveStream && !this.isObserved) {
47
48
  log.warn(
48
49
  'When using adaptiveStream, you need to use remoteVideoTrack.attach() to add the track to a HTMLVideoElement, otherwise your video tracks might never start',
49
50
  );
@@ -83,7 +84,6 @@ export default class RemoteVideoTrack extends RemoteTrack {
83
84
  const elementInfo = new HTMLElementInfo(element);
84
85
  this.observeElementInfo(elementInfo);
85
86
  }
86
- this.hasUsedAttach = true;
87
87
  return element;
88
88
  }
89
89
 
@@ -110,6 +110,7 @@ export default class RemoteVideoTrack extends RemoteTrack {
110
110
  // the tab comes into focus for the first time.
111
111
  this.debouncedHandleResize();
112
112
  this.updateVisibility();
113
+ this.isObserved = true;
113
114
  } else {
114
115
  log.warn('visibility resize observer not triggered');
115
116
  }
@@ -253,7 +254,7 @@ export default class RemoteVideoTrack extends RemoteTrack {
253
254
  let maxHeight = 0;
254
255
  for (const info of this.elementInfos) {
255
256
  const pixelDensity = this.adaptiveStreamSettings?.pixelDensity ?? 1;
256
- const pixelDensityValue = pixelDensity === 'screen' ? window.devicePixelRatio : pixelDensity;
257
+ const pixelDensityValue = pixelDensity === 'screen' ? getDevicePixelRatio() : pixelDensity;
257
258
  const currentElementWidth = info.width() * pixelDensityValue;
258
259
  const currentElementHeight = info.height() * pixelDensityValue;
259
260
  if (currentElementWidth + currentElementHeight > maxWidth + maxHeight) {