livekit-client 1.7.1 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
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) {