livekit-client 1.7.0 → 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 (84) hide show
  1. package/README.md +20 -1
  2. package/dist/livekit-client.esm.mjs +2240 -1067
  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/options.d.ts +5 -0
  9. package/dist/src/options.d.ts.map +1 -1
  10. package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -1
  11. package/dist/src/proto/livekit_models.d.ts +32 -0
  12. package/dist/src/proto/livekit_models.d.ts.map +1 -1
  13. package/dist/src/proto/livekit_rtc.d.ts +315 -75
  14. package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
  15. package/dist/src/room/RTCEngine.d.ts +9 -1
  16. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  17. package/dist/src/room/ReconnectPolicy.d.ts +1 -0
  18. package/dist/src/room/ReconnectPolicy.d.ts.map +1 -1
  19. package/dist/src/room/RegionUrlProvider.d.ts +14 -0
  20. package/dist/src/room/RegionUrlProvider.d.ts.map +1 -0
  21. package/dist/src/room/Room.d.ts +6 -1
  22. package/dist/src/room/Room.d.ts.map +1 -1
  23. package/dist/src/room/defaults.d.ts.map +1 -1
  24. package/dist/src/room/errors.d.ts +2 -1
  25. package/dist/src/room/errors.d.ts.map +1 -1
  26. package/dist/src/room/events.d.ts +15 -2
  27. package/dist/src/room/events.d.ts.map +1 -1
  28. package/dist/src/room/track/LocalAudioTrack.d.ts +1 -1
  29. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  30. package/dist/src/room/track/LocalTrack.d.ts +3 -2
  31. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  32. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  33. package/dist/src/room/track/RemoteTrackPublication.d.ts +1 -1
  34. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  35. package/dist/src/room/track/RemoteVideoTrack.d.ts +2 -1
  36. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  37. package/dist/src/room/track/Track.d.ts +3 -1
  38. package/dist/src/room/track/Track.d.ts.map +1 -1
  39. package/dist/src/room/track/utils.d.ts.map +1 -1
  40. package/dist/src/room/types.d.ts +4 -0
  41. package/dist/src/room/types.d.ts.map +1 -1
  42. package/dist/src/room/utils.d.ts +4 -0
  43. package/dist/src/room/utils.d.ts.map +1 -1
  44. package/dist/ts4.2/src/index.d.ts +3 -1
  45. package/dist/ts4.2/src/options.d.ts +5 -0
  46. package/dist/ts4.2/src/proto/livekit_models.d.ts +32 -0
  47. package/dist/ts4.2/src/proto/livekit_rtc.d.ts +348 -84
  48. package/dist/ts4.2/src/room/RTCEngine.d.ts +9 -1
  49. package/dist/ts4.2/src/room/ReconnectPolicy.d.ts +1 -0
  50. package/dist/ts4.2/src/room/RegionUrlProvider.d.ts +14 -0
  51. package/dist/ts4.2/src/room/Room.d.ts +6 -1
  52. package/dist/ts4.2/src/room/errors.d.ts +2 -1
  53. package/dist/ts4.2/src/room/events.d.ts +15 -2
  54. package/dist/ts4.2/src/room/track/LocalAudioTrack.d.ts +1 -1
  55. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +3 -2
  56. package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +1 -1
  57. package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +2 -1
  58. package/dist/ts4.2/src/room/track/Track.d.ts +3 -1
  59. package/dist/ts4.2/src/room/types.d.ts +4 -0
  60. package/dist/ts4.2/src/room/utils.d.ts +4 -0
  61. package/package.json +19 -19
  62. package/src/api/SignalClient.ts +4 -4
  63. package/src/index.ts +3 -0
  64. package/src/options.ts +6 -0
  65. package/src/proto/google/protobuf/timestamp.ts +15 -6
  66. package/src/proto/livekit_models.ts +903 -222
  67. package/src/proto/livekit_rtc.ts +1053 -279
  68. package/src/room/RTCEngine.ts +168 -56
  69. package/src/room/ReconnectPolicy.ts +2 -0
  70. package/src/room/RegionUrlProvider.ts +73 -0
  71. package/src/room/Room.ts +212 -133
  72. package/src/room/defaults.ts +1 -0
  73. package/src/room/errors.ts +1 -0
  74. package/src/room/events.ts +15 -0
  75. package/src/room/track/LocalAudioTrack.ts +14 -6
  76. package/src/room/track/LocalTrack.ts +22 -8
  77. package/src/room/track/LocalVideoTrack.ts +12 -6
  78. package/src/room/track/RemoteTrackPublication.ts +10 -4
  79. package/src/room/track/RemoteVideoTrack.test.ts +2 -0
  80. package/src/room/track/RemoteVideoTrack.ts +53 -9
  81. package/src/room/track/Track.ts +46 -31
  82. package/src/room/track/utils.ts +3 -2
  83. package/src/room/types.ts +6 -0
  84. 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,153 +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()) {
395
- window.addEventListener('beforeunload', this.onBeforeUnload);
396
- navigator.mediaDevices?.addEventListener('devicechange', this.handleDeviceChange);
397
- }
398
- this.setAndEmitConnectionState(ConnectionState.Connected);
399
- this.emit(RoomEvent.Connected);
400
- resolve();
401
- });
402
- };
403
- this.connectFuture = new Future(connectFn, () => {
404
- this.clearConnectionFutures();
405
- });
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
+ }
406
451
 
407
- 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);
408
478
  };
409
479
 
410
480
  /**
@@ -549,7 +619,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
549
619
  }
550
620
  }
551
621
 
552
- private onBeforeUnload = async () => {
622
+ private onPageLeave = async () => {
553
623
  await this.disconnect();
554
624
  };
555
625
 
@@ -853,7 +923,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
853
923
  this.audioContext = undefined;
854
924
  }
855
925
  if (isWeb()) {
856
- window.removeEventListener('beforeunload', this.onBeforeUnload);
926
+ window.removeEventListener('beforeunload', this.onPageLeave);
927
+ window.removeEventListener('pagehide', this.onPageLeave);
857
928
  navigator.mediaDevices?.removeEventListener('devicechange', this.handleDeviceChange);
858
929
  }
859
930
  this.setAndEmitConnectionState(ConnectionState.Disconnected);
@@ -1275,8 +1346,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1275
1346
  this.emit(RoomEvent.TrackUnmuted, pub, this.localParticipant);
1276
1347
  };
1277
1348
 
1278
- private onLocalTrackPublished = (pub: LocalTrackPublication) => {
1349
+ private onLocalTrackPublished = async (pub: LocalTrackPublication) => {
1279
1350
  this.emit(RoomEvent.LocalTrackPublished, pub, this.localParticipant);
1351
+ if (pub.track instanceof LocalAudioTrack) {
1352
+ const trackIsSilent = await pub.track.checkForSilence();
1353
+ if (trackIsSilent) {
1354
+ this.emit(RoomEvent.LocalAudioSilenceDetected, pub);
1355
+ }
1356
+ }
1280
1357
  };
1281
1358
 
1282
1359
  private onLocalTrackUnpublished = (pub: LocalTrackPublication) => {
@@ -1455,6 +1532,7 @@ export type RoomEventCallbacks = {
1455
1532
  publication: LocalTrackPublication,
1456
1533
  participant: LocalParticipant,
1457
1534
  ) => void;
1535
+ localAudioSilenceDetected: (publication: LocalTrackPublication) => void;
1458
1536
  participantMetadataChanged: (
1459
1537
  metadata: string | undefined,
1460
1538
  participant: RemoteParticipant | LocalParticipant,
@@ -1491,4 +1569,5 @@ export type RoomEventCallbacks = {
1491
1569
  audioPlaybackChanged: (playing: boolean) => void;
1492
1570
  signalConnected: () => void;
1493
1571
  recordingStatusChanged: (recording: boolean) => void;
1572
+ dcBufferStatusChanged: (isLow: boolean, kind: DataPacket_Kind) => void;
1494
1573
  };
@@ -36,6 +36,7 @@ export const roomOptionDefaults: InternalRoomOptions = {
36
36
  dynacast: false,
37
37
  stopLocalTrackOnUnpublish: true,
38
38
  reconnectPolicy: new DefaultReconnectPolicy(),
39
+ disconnectOnPageLeave: true,
39
40
  expWebAudioMix: false,
40
41
  } as const;
41
42
 
@@ -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 {
@@ -139,6 +139,14 @@ export enum RoomEvent {
139
139
  */
140
140
  LocalTrackUnpublished = 'localTrackUnpublished',
141
141
 
142
+ /**
143
+ * When a local audio track is published the SDK checks whether there is complete silence
144
+ * on that track and emits the LocalAudioSilenceDetected event in that case.
145
+ * This allows for applications to show UI informing users that they might have to
146
+ * reset their audio hardware or check for proper device connectivity.
147
+ */
148
+ LocalAudioSilenceDetected = 'localAudioSilenceDetected',
149
+
142
150
  /**
143
151
  * Active speakers changed. List of speakers are ordered by their audio level.
144
152
  * loudest speakers first. This will include the LocalParticipant too.
@@ -256,6 +264,12 @@ export enum RoomEvent {
256
264
  * args: (isRecording: boolean)
257
265
  */
258
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',
259
273
  }
260
274
 
261
275
  export enum ParticipantEvent {
@@ -423,6 +437,7 @@ export enum EngineEvent {
423
437
  MediaTrackAdded = 'mediaTrackAdded',
424
438
  ActiveSpeakersUpdate = 'activeSpeakersUpdate',
425
439
  DataPacketReceived = 'dataPacketReceived',
440
+ DCBufferStatusChanged = 'dcBufferStatusChanged',
426
441
  }
427
442
 
428
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) {
@@ -150,5 +157,6 @@ export default class LocalAudioTrack extends LocalTrack {
150
157
  }
151
158
  this.emit(TrackEvent.AudioSilenceDetected);
152
159
  }
160
+ return trackIsSilent;
153
161
  }
154
162
  }
@@ -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[]> {