livekit-client 1.7.0 → 1.8.0

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