livekit-client 1.5.0 → 1.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. package/dist/livekit-client.esm.mjs +2257 -5488
  2. package/dist/livekit-client.esm.mjs.map +1 -1
  3. package/dist/livekit-client.umd.js +1 -1
  4. package/dist/livekit-client.umd.js.map +1 -1
  5. package/dist/src/api/SignalClient.d.ts +3 -2
  6. package/dist/src/api/SignalClient.d.ts.map +1 -1
  7. package/dist/src/connectionHelper/ConnectionCheck.d.ts +1 -1
  8. package/dist/src/connectionHelper/ConnectionCheck.d.ts.map +1 -1
  9. package/dist/src/connectionHelper/checks/Checker.d.ts +4 -4
  10. package/dist/src/connectionHelper/checks/Checker.d.ts.map +1 -1
  11. package/dist/src/index.d.ts +3 -2
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/logger.d.ts +3 -3
  14. package/dist/src/logger.d.ts.map +1 -1
  15. package/dist/src/options.d.ts +4 -1
  16. package/dist/src/options.d.ts.map +1 -1
  17. package/dist/src/proto/google/protobuf/timestamp.d.ts +4 -4
  18. package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -1
  19. package/dist/src/proto/livekit_models.d.ts +4 -4
  20. package/dist/src/proto/livekit_models.d.ts.map +1 -1
  21. package/dist/src/proto/livekit_rtc.d.ts +4 -4
  22. package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
  23. package/dist/src/room/PCTransport.d.ts +7 -1
  24. package/dist/src/room/PCTransport.d.ts.map +1 -1
  25. package/dist/src/room/RTCEngine.d.ts +10 -4
  26. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  27. package/dist/src/room/Room.d.ts +21 -4
  28. package/dist/src/room/Room.d.ts.map +1 -1
  29. package/dist/src/room/events.d.ts +5 -0
  30. package/dist/src/room/events.d.ts.map +1 -1
  31. package/dist/src/room/participant/LocalParticipant.d.ts +3 -2
  32. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  33. package/dist/src/room/participant/Participant.d.ts +1 -1
  34. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  35. package/dist/src/room/track/LocalTrack.d.ts +1 -0
  36. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  37. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  38. package/dist/src/room/track/Track.d.ts +2 -1
  39. package/dist/src/room/track/Track.d.ts.map +1 -1
  40. package/dist/src/room/track/TrackPublication.d.ts +1 -1
  41. package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
  42. package/dist/src/room/track/options.d.ts +3 -3
  43. package/dist/src/room/track/options.d.ts.map +1 -1
  44. package/dist/src/room/track/types.d.ts +3 -3
  45. package/dist/src/room/track/types.d.ts.map +1 -1
  46. package/dist/src/room/types.d.ts +13 -0
  47. package/dist/src/room/types.d.ts.map +1 -0
  48. package/dist/src/room/utils.d.ts +44 -0
  49. package/dist/src/room/utils.d.ts.map +1 -1
  50. package/dist/ts4.2/src/api/SignalClient.d.ts +3 -2
  51. package/dist/ts4.2/src/connectionHelper/ConnectionCheck.d.ts +1 -1
  52. package/dist/ts4.2/src/connectionHelper/checks/Checker.d.ts +4 -4
  53. package/dist/ts4.2/src/index.d.ts +3 -2
  54. package/dist/ts4.2/src/logger.d.ts +3 -3
  55. package/dist/ts4.2/src/options.d.ts +4 -1
  56. package/dist/ts4.2/src/proto/google/protobuf/timestamp.d.ts +4 -4
  57. package/dist/ts4.2/src/proto/livekit_models.d.ts +4 -4
  58. package/dist/ts4.2/src/proto/livekit_rtc.d.ts +4 -4
  59. package/dist/ts4.2/src/room/PCTransport.d.ts +7 -1
  60. package/dist/ts4.2/src/room/RTCEngine.d.ts +10 -4
  61. package/dist/ts4.2/src/room/Room.d.ts +21 -4
  62. package/dist/ts4.2/src/room/events.d.ts +5 -0
  63. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +3 -2
  64. package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -1
  65. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +1 -0
  66. package/dist/ts4.2/src/room/track/Track.d.ts +2 -1
  67. package/dist/ts4.2/src/room/track/TrackPublication.d.ts +1 -1
  68. package/dist/ts4.2/src/room/track/options.d.ts +3 -3
  69. package/dist/ts4.2/src/room/track/types.d.ts +3 -3
  70. package/dist/ts4.2/src/room/types.d.ts +13 -0
  71. package/dist/ts4.2/src/room/utils.d.ts +44 -0
  72. package/package.json +23 -23
  73. package/src/api/SignalClient.ts +40 -16
  74. package/src/connectionHelper/checks/turn.ts +1 -1
  75. package/src/connectionHelper/checks/websocket.ts +1 -1
  76. package/src/index.ts +5 -0
  77. package/src/options.ts +5 -1
  78. package/src/room/PCTransport.ts +11 -1
  79. package/src/room/RTCEngine.ts +111 -49
  80. package/src/room/Room.ts +234 -63
  81. package/src/room/events.ts +5 -0
  82. package/src/room/participant/LocalParticipant.ts +46 -22
  83. package/src/room/participant/RemoteParticipant.ts +5 -5
  84. package/src/room/participant/publishUtils.ts +1 -1
  85. package/src/room/track/LocalAudioTrack.ts +1 -1
  86. package/src/room/track/LocalTrack.ts +20 -1
  87. package/src/room/track/LocalVideoTrack.ts +1 -1
  88. package/src/room/track/RemoteVideoTrack.ts +4 -0
  89. package/src/room/track/Track.ts +22 -5
  90. package/src/room/types.ts +12 -0
  91. package/src/room/utils.ts +150 -12
package/src/room/Room.ts CHANGED
@@ -17,6 +17,9 @@ import {
17
17
  Room as RoomModel,
18
18
  ServerInfo,
19
19
  SpeakerInfo,
20
+ TrackInfo,
21
+ TrackSource,
22
+ TrackType,
20
23
  UserPacket,
21
24
  } from '../proto/livekit_models';
22
25
  import {
@@ -42,7 +45,7 @@ import type { ConnectionQuality } from './participant/Participant';
42
45
  import RemoteParticipant from './participant/RemoteParticipant';
43
46
  import RTCEngine from './RTCEngine';
44
47
  import LocalAudioTrack from './track/LocalAudioTrack';
45
- import type LocalTrackPublication from './track/LocalTrackPublication';
48
+ import LocalTrackPublication from './track/LocalTrackPublication';
46
49
  import LocalVideoTrack from './track/LocalVideoTrack';
47
50
  import type RemoteTrack from './track/RemoteTrack';
48
51
  import RemoteTrackPublication from './track/RemoteTrackPublication';
@@ -50,7 +53,16 @@ import { Track } from './track/Track';
50
53
  import type { TrackPublication } from './track/TrackPublication';
51
54
  import type { AdaptiveStreamSettings } from './track/types';
52
55
  import { getNewAudioContext } from './track/utils';
53
- import { Future, isWeb, supportsSetSinkId, unpackStreamId } from './utils';
56
+ import type { SimulationOptions } from './types';
57
+ import {
58
+ Future,
59
+ createDummyVideoStreamTrack,
60
+ getEmptyAudioStreamTrack,
61
+ isWeb,
62
+ Mutex,
63
+ supportsSetSinkId,
64
+ unpackStreamId,
65
+ } from './utils';
54
66
 
55
67
  export enum ConnectionState {
56
68
  Disconnected = 'disconnected',
@@ -118,6 +130,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
118
130
  /** future holding client initiated connection attempt */
119
131
  private connectFuture?: Future<void>;
120
132
 
133
+ private disconnectLock: Mutex;
134
+
121
135
  /**
122
136
  * Creates a new Room, the primary construct for a LiveKit session.
123
137
  * @param options
@@ -144,6 +158,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
144
158
 
145
159
  this.maybeCreateEngine();
146
160
 
161
+ this.disconnectLock = new Mutex();
162
+
147
163
  this.localParticipant = new LocalParticipant('', '', this.engine, this.options);
148
164
  }
149
165
 
@@ -223,14 +239,19 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
223
239
  await fetch(`http${url.substring(2)}`, { method: 'HEAD' });
224
240
  }
225
241
 
226
- connect = (url: string, token: string, opts?: RoomConnectOptions): Promise<void> => {
242
+ connect = async (url: string, token: string, opts?: RoomConnectOptions): Promise<void> => {
243
+ // In case a disconnect called happened right before the connect call, make sure the disconnect is completed first by awaiting its lock
244
+ const unlockDisconnect = await this.disconnectLock.lock();
245
+
227
246
  if (this.state === ConnectionState.Connected) {
228
247
  // when the state is reconnecting or connected, this function returns immediately
229
248
  log.info(`already connected to room ${this.name}`);
249
+ unlockDisconnect();
230
250
  return Promise.resolve();
231
251
  }
232
252
 
233
253
  if (this.connectFuture) {
254
+ unlockDisconnect();
234
255
  return this.connectFuture.promise;
235
256
  }
236
257
 
@@ -240,6 +261,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
240
261
  if (!this.abortController || this.abortController.signal.aborted) {
241
262
  this.abortController = new AbortController();
242
263
  }
264
+ // at this point the intention to connect has been signalled so we can allow cancelling of the connection via disconnect() again
265
+ unlockDisconnect();
243
266
 
244
267
  if (this.state === ConnectionState.Reconnecting) {
245
268
  log.info('Reconnection attempt replaced by new connection attempt');
@@ -303,18 +326,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
303
326
 
304
327
  this.localParticipant.updateInfo(pi);
305
328
  // forward metadata changed for the local participant
306
- this.localParticipant
307
- .on(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
308
- .on(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
309
- .on(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
310
- .on(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
311
- .on(ParticipantEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished)
312
- .on(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged)
313
- .on(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError)
314
- .on(
315
- ParticipantEvent.ParticipantPermissionsChanged,
316
- this.onLocalParticipantPermissionsChanged,
317
- );
329
+ this.setupLocalParticipantEvents();
318
330
 
319
331
  // populate remote participants, these should not trigger new events
320
332
  joinResponse.otherParticipants.forEach((info) => {
@@ -342,7 +354,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
342
354
  } catch (err) {
343
355
  this.recreateEngine();
344
356
  this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
345
- reject(new ConnectionError('could not establish signal connection'));
357
+ let errorMessage = '';
358
+ if (err instanceof Error) {
359
+ errorMessage = err.message;
360
+ log.debug(`error trying to establish signal connection`, { error: err });
361
+ }
362
+ reject(new ConnectionError(`could not establish signal connection: ${errorMessage}`));
346
363
  return;
347
364
  }
348
365
 
@@ -389,26 +406,38 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
389
406
  * disconnects the room, emits [[RoomEvent.Disconnected]]
390
407
  */
391
408
  disconnect = async (stopTracks = true) => {
392
- log.info('disconnect from room', { identity: this.localParticipant.identity });
393
- if (this.state === ConnectionState.Connecting || this.state === ConnectionState.Reconnecting) {
394
- // try aborting pending connection attempt
395
- log.warn('abort connection attempt');
396
- this.abortController?.abort();
397
- // in case the abort controller didn't manage to cancel the connection attempt, reject the connect promise explicitly
398
- this.connectFuture?.reject?.(new ConnectionError('Client initiated disconnect'));
399
- this.connectFuture = undefined;
400
- }
401
- // send leave
402
- if (this.engine?.client.isConnected) {
403
- await this.engine.client.sendLeave();
404
- }
405
- // close engine (also closes client)
406
- if (this.engine) {
407
- this.engine.close();
409
+ const unlock = await this.disconnectLock.lock();
410
+ try {
411
+ if (this.state === ConnectionState.Disconnected) {
412
+ log.debug('already disconnected');
413
+ return;
414
+ }
415
+ log.info('disconnect from room', { identity: this.localParticipant.identity });
416
+ if (
417
+ this.state === ConnectionState.Connecting ||
418
+ this.state === ConnectionState.Reconnecting
419
+ ) {
420
+ // try aborting pending connection attempt
421
+ log.warn('abort connection attempt');
422
+ this.abortController?.abort();
423
+ // in case the abort controller didn't manage to cancel the connection attempt, reject the connect promise explicitly
424
+ this.connectFuture?.reject?.(new ConnectionError('Client initiated disconnect'));
425
+ this.connectFuture = undefined;
426
+ }
427
+ // send leave
428
+ if (this.engine?.client.isConnected) {
429
+ await this.engine.client.sendLeave();
430
+ }
431
+ // close engine (also closes client)
432
+ if (this.engine) {
433
+ await this.engine.close();
434
+ }
435
+ this.handleDisconnect(stopTracks, DisconnectReason.CLIENT_INITIATED);
436
+ /* @ts-ignore */
437
+ this.engine = undefined;
438
+ } finally {
439
+ unlock();
408
440
  }
409
- this.handleDisconnect(stopTracks, DisconnectReason.CLIENT_INITIATED);
410
- /* @ts-ignore */
411
- this.engine = undefined;
412
441
  };
413
442
 
414
443
  /**
@@ -440,12 +469,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
440
469
  /**
441
470
  * @internal for testing
442
471
  */
443
- simulateScenario(scenario: string) {
472
+ async simulateScenario(scenario: string) {
444
473
  let postAction = () => {};
445
474
  let req: SimulateScenario | undefined;
446
475
  switch (scenario) {
447
476
  case 'signal-reconnect':
448
- this.engine.client.close();
477
+ await this.engine.client.close();
449
478
  if (this.engine.client.onClose) {
450
479
  this.engine.client.onClose('simulate disconnect');
451
480
  }
@@ -508,8 +537,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
508
537
  }
509
538
  }
510
539
 
511
- private onBeforeUnload = () => {
512
- this.disconnect();
540
+ private onBeforeUnload = async () => {
541
+ await this.disconnect();
513
542
  };
514
543
 
515
544
  /**
@@ -520,7 +549,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
520
549
  * - `getUserMedia`
521
550
  */
522
551
  async startAudio() {
523
- this.acquireAudioContext();
552
+ await this.acquireAudioContext();
524
553
 
525
554
  const elements: Array<HTMLMediaElement> = [];
526
555
  this.participants.forEach((p) => {
@@ -534,7 +563,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
534
563
  });
535
564
 
536
565
  try {
537
- await Promise.all(elements.map((e) => e.play()));
566
+ await Promise.all(
567
+ elements.map((e) => {
568
+ e.muted = false;
569
+ return e.play();
570
+ }),
571
+ );
538
572
  this.handleAudioPlaybackStarted();
539
573
  } catch (err) {
540
574
  this.handleAudioPlaybackFailed(err);
@@ -550,7 +584,18 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
550
584
  }
551
585
 
552
586
  /**
553
- * Switches all active device used in this room to the given device.
587
+ * Returns the active audio output device used in this room.
588
+ *
589
+ * Note: to get the active `audioinput` or `videoinput` use [[LocalTrack.getDeviceId()]]
590
+ *
591
+ * @return the previously successfully set audio output device ID or an empty string if the default device is used.
592
+ */
593
+ getActiveAudioOutputDevice(): string {
594
+ return this.options.audioOutput?.deviceId ?? '';
595
+ }
596
+
597
+ /**
598
+ * Switches all active devices used in this room to the given device.
554
599
  *
555
600
  * Note: setting AudioOutput is not supported on some browsers. See [setSinkId](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId#browser_compatibility)
556
601
  *
@@ -592,16 +637,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
592
637
  this.options.audioOutput ??= {};
593
638
  const prevDeviceId = this.options.audioOutput.deviceId;
594
639
  this.options.audioOutput.deviceId = deviceId;
595
- const promises: Promise<void>[] = [];
596
- this.participants.forEach((p) => {
597
- promises.push(
598
- p.setAudioOutput({
599
- deviceId,
600
- }),
601
- );
602
- });
603
640
  try {
604
- await Promise.all(promises);
641
+ await Promise.all(
642
+ Array.from(this.participants.values()).map((p) => p.setAudioOutput({ deviceId })),
643
+ );
605
644
  } catch (e) {
606
645
  this.options.audioOutput.deviceId = prevDeviceId;
607
646
  throw e;
@@ -609,6 +648,21 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
609
648
  }
610
649
  }
611
650
 
651
+ private setupLocalParticipantEvents() {
652
+ this.localParticipant
653
+ .on(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
654
+ .on(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
655
+ .on(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
656
+ .on(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
657
+ .on(ParticipantEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished)
658
+ .on(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged)
659
+ .on(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError)
660
+ .on(
661
+ ParticipantEvent.ParticipantPermissionsChanged,
662
+ this.onLocalParticipantPermissionsChanged,
663
+ );
664
+ }
665
+
612
666
  private recreateEngine() {
613
667
  this.engine?.close();
614
668
  /* @ts-ignore */
@@ -773,7 +827,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
773
827
 
774
828
  this.participants.clear();
775
829
  this.activeSpeakers = [];
776
- if (this.audioContext) {
830
+ if (this.audioContext && typeof this.options.expWebAudioMix === 'boolean') {
777
831
  this.audioContext.close();
778
832
  this.audioContext = undefined;
779
833
  }
@@ -986,18 +1040,28 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
986
1040
  });
987
1041
  };
988
1042
 
989
- private acquireAudioContext() {
990
- if (this.audioContext) {
991
- this.audioContext.close();
1043
+ private async acquireAudioContext() {
1044
+ if (
1045
+ typeof this.options.expWebAudioMix !== 'boolean' &&
1046
+ this.options.expWebAudioMix.audioContext
1047
+ ) {
1048
+ // override audio context with custom audio context if supplied by user
1049
+ this.audioContext = this.options.expWebAudioMix.audioContext;
1050
+ await this.audioContext.resume();
1051
+ } else {
1052
+ // by using an AudioContext, it reduces lag on audio elements
1053
+ // https://stackoverflow.com/questions/9811429/html5-audio-tag-on-safari-has-a-delay/54119854#54119854
1054
+ this.audioContext = getNewAudioContext() ?? undefined;
992
1055
  }
993
- // by using an AudioContext, it reduces lag on audio elements
994
- // https://stackoverflow.com/questions/9811429/html5-audio-tag-on-safari-has-a-delay/54119854#54119854
995
- const ctx = getNewAudioContext();
996
- if (ctx) {
997
- this.audioContext = ctx;
998
- if (this.options.expWebAudioMix) {
999
- this.participants.forEach((participant) => participant.setAudioContext(this.audioContext));
1000
- }
1056
+
1057
+ if (this.options.expWebAudioMix) {
1058
+ this.participants.forEach((participant) => participant.setAudioContext(this.audioContext));
1059
+ }
1060
+
1061
+ const newContextIsRunning = this.audioContext?.state === 'running';
1062
+ if (newContextIsRunning !== this.canPlaybackAudio) {
1063
+ this.audioEnabled = newContextIsRunning;
1064
+ this.emit(RoomEvent.AudioPlaybackStatusChanged, newContextIsRunning);
1001
1065
  }
1002
1066
  }
1003
1067
 
@@ -1212,6 +1276,113 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1212
1276
  this.emit(RoomEvent.ParticipantPermissionsChanged, prevPermissions, this.localParticipant);
1213
1277
  };
1214
1278
 
1279
+ /**
1280
+ * Allows to populate a room with simulated participants.
1281
+ * No actual connection to a server will be established, all state is
1282
+ * @experimental
1283
+ */
1284
+ simulateParticipants(options: SimulationOptions) {
1285
+ const publishOptions = {
1286
+ audio: true,
1287
+ video: true,
1288
+ ...options.publish,
1289
+ };
1290
+ const participantOptions = {
1291
+ count: 9,
1292
+ audio: false,
1293
+ video: true,
1294
+ aspectRatios: [1.66, 1.7, 1.3],
1295
+ ...options.participants,
1296
+ };
1297
+ this.handleDisconnect();
1298
+ this.name = 'simulated-room';
1299
+
1300
+ this.localParticipant.updateInfo(
1301
+ ParticipantInfo.fromPartial({
1302
+ identity: 'simulated-local',
1303
+ name: 'local-name',
1304
+ }),
1305
+ );
1306
+ this.setupLocalParticipantEvents();
1307
+ this.emit(RoomEvent.SignalConnected);
1308
+ this.emit(RoomEvent.Connected);
1309
+ this.setAndEmitConnectionState(ConnectionState.Connected);
1310
+ if (publishOptions.video) {
1311
+ const camPub = new LocalTrackPublication(
1312
+ Track.Kind.Video,
1313
+ TrackInfo.fromPartial({
1314
+ source: TrackSource.CAMERA,
1315
+ sid: Math.floor(Math.random() * 10_000).toString(),
1316
+ type: TrackType.AUDIO,
1317
+ name: 'video-dummy',
1318
+ }),
1319
+ new LocalVideoTrack(
1320
+ createDummyVideoStreamTrack(
1321
+ 160 * participantOptions.aspectRatios[0] ?? 1,
1322
+ 160,
1323
+ true,
1324
+ true,
1325
+ ),
1326
+ ),
1327
+ );
1328
+ // @ts-ignore
1329
+ this.localParticipant.addTrackPublication(camPub);
1330
+ this.localParticipant.emit(ParticipantEvent.LocalTrackPublished, camPub);
1331
+ }
1332
+ if (publishOptions.audio) {
1333
+ const audioPub = new LocalTrackPublication(
1334
+ Track.Kind.Audio,
1335
+ TrackInfo.fromPartial({
1336
+ source: TrackSource.MICROPHONE,
1337
+ sid: Math.floor(Math.random() * 10_000).toString(),
1338
+ type: TrackType.AUDIO,
1339
+ }),
1340
+ new LocalAudioTrack(getEmptyAudioStreamTrack()),
1341
+ );
1342
+ // @ts-ignore
1343
+ this.localParticipant.addTrackPublication(audioPub);
1344
+ this.localParticipant.emit(ParticipantEvent.LocalTrackPublished, audioPub);
1345
+ }
1346
+
1347
+ for (let i = 0; i < participantOptions.count - 1; i += 1) {
1348
+ let info: ParticipantInfo = ParticipantInfo.fromPartial({
1349
+ sid: Math.floor(Math.random() * 10_000).toString(),
1350
+ identity: `simulated-${i}`,
1351
+ state: ParticipantInfo_State.ACTIVE,
1352
+ tracks: [],
1353
+ joinedAt: Date.now(),
1354
+ });
1355
+ const p = this.getOrCreateParticipant(info.identity, info);
1356
+ if (participantOptions.video) {
1357
+ const dummyVideo = createDummyVideoStreamTrack(
1358
+ 160 * participantOptions.aspectRatios[i % participantOptions.aspectRatios.length] ?? 1,
1359
+ 160,
1360
+ false,
1361
+ true,
1362
+ );
1363
+ const videoTrack = TrackInfo.fromPartial({
1364
+ source: TrackSource.CAMERA,
1365
+ sid: Math.floor(Math.random() * 10_000).toString(),
1366
+ type: TrackType.AUDIO,
1367
+ });
1368
+ p.addSubscribedMediaTrack(dummyVideo, videoTrack.sid, new MediaStream([dummyVideo]));
1369
+ info.tracks = [...info.tracks, videoTrack];
1370
+ }
1371
+ if (participantOptions.audio) {
1372
+ const dummyTrack = getEmptyAudioStreamTrack();
1373
+ const audioTrack = TrackInfo.fromPartial({
1374
+ source: TrackSource.MICROPHONE,
1375
+ sid: Math.floor(Math.random() * 10_000).toString(),
1376
+ type: TrackType.AUDIO,
1377
+ });
1378
+ p.addSubscribedMediaTrack(dummyTrack, audioTrack.sid, new MediaStream([dummyTrack]));
1379
+ info.tracks = [...info.tracks, audioTrack];
1380
+ }
1381
+
1382
+ p.updateInfo(info);
1383
+ }
1384
+ }
1385
+
1215
1386
  // /** @internal */
1216
1387
  emit<E extends keyof RoomEventCallbacks>(
1217
1388
  event: E,
@@ -418,6 +418,7 @@ export enum EngineEvent {
418
418
  Restarting = 'restarting',
419
419
  Restarted = 'restarted',
420
420
  SignalResumed = 'signalResumed',
421
+ Closing = 'closing',
421
422
  MediaTrackAdded = 'mediaTrackAdded',
422
423
  ActiveSpeakersUpdate = 'activeSpeakersUpdate',
423
424
  DataPacketReceived = 'dataPacketReceived',
@@ -427,6 +428,10 @@ export enum TrackEvent {
427
428
  Message = 'message',
428
429
  Muted = 'muted',
429
430
  Unmuted = 'unmuted',
431
+ /**
432
+ * Only fires on LocalTracks
433
+ */
434
+ Restarted = 'restarted',
430
435
  Ended = 'ended',
431
436
  Subscribed = 'subscribed',
432
437
  Unsubscribed = 'unsubscribed',
@@ -262,7 +262,7 @@ export default class LocalParticipant extends Participant {
262
262
  } else if (track && track.track) {
263
263
  // screenshare cannot be muted, unpublish instead
264
264
  if (source === Track.Source.ScreenShare) {
265
- track = this.unpublishTrack(track.track);
265
+ track = await this.unpublishTrack(track.track);
266
266
  const screenAudioTrack = this.getTrack(Track.Source.ScreenShareAudio);
267
267
  if (screenAudioTrack && screenAudioTrack.track) {
268
268
  this.unpublishTrack(screenAudioTrack.track);
@@ -528,13 +528,19 @@ export default class LocalParticipant extends Participant {
528
528
  let encodings: RTCRtpEncodingParameters[] | undefined;
529
529
  let simEncodings: RTCRtpEncodingParameters[] | undefined;
530
530
  if (track.kind === Track.Kind.Video) {
531
- // TODO: support react native, which doesn't expose getSettings
532
- const settings = track.mediaStreamTrack.getSettings();
533
- const width = settings.width ?? track.dimensions?.width;
534
- const height = settings.height ?? track.dimensions?.height;
531
+ let dims: Track.Dimensions = {
532
+ width: 0,
533
+ height: 0,
534
+ };
535
+ try {
536
+ dims = await track.waitForDimensions();
537
+ } catch (e) {
538
+ // log failure
539
+ log.error('could not determine track dimensions');
540
+ }
535
541
  // width and height should be defined for video
536
- req.width = width ?? 0;
537
- req.height = height ?? 0;
542
+ req.width = dims.width;
543
+ req.height = dims.height;
538
544
  // for svc codecs, disable simulcast and use vp8 for backup codec
539
545
  if (track instanceof LocalVideoTrack) {
540
546
  if (opts?.videoCodec === 'av1') {
@@ -565,8 +571,8 @@ export default class LocalParticipant extends Participant {
565
571
 
566
572
  encodings = computeVideoEncodings(
567
573
  track.source === Track.Source.ScreenShare,
568
- width,
569
- height,
574
+ dims.width,
575
+ dims.height,
570
576
  opts,
571
577
  );
572
578
  req.layers = videoLayersFromEncodings(req.width, req.height, simEncodings ?? encodings);
@@ -694,10 +700,10 @@ export default class LocalParticipant extends Participant {
694
700
  log.debug(`published ${videoCodec} for track ${track.sid}`, { encodings, trackInfo: ti });
695
701
  }
696
702
 
697
- unpublishTrack(
703
+ async unpublishTrack(
698
704
  track: LocalTrack | MediaStreamTrack,
699
705
  stopOnUnpublish?: boolean,
700
- ): LocalTrackPublication | undefined {
706
+ ): Promise<LocalTrackPublication | undefined> {
701
707
  // look through all published tracks to find the right ones
702
708
  const publication = this.getPublicationForTrack(track);
703
709
 
@@ -744,7 +750,7 @@ export default class LocalParticipant extends Participant {
744
750
  } catch (e) {
745
751
  log.warn('failed to unpublish track', { error: e, method: 'unpublishTrack' });
746
752
  } finally {
747
- this.engine.negotiate();
753
+ await this.engine.negotiate();
748
754
  }
749
755
  }
750
756
 
@@ -769,15 +775,33 @@ export default class LocalParticipant extends Participant {
769
775
  return publication;
770
776
  }
771
777
 
772
- unpublishTracks(tracks: LocalTrack[] | MediaStreamTrack[]): LocalTrackPublication[] {
773
- const publications: LocalTrackPublication[] = [];
774
- tracks.forEach((track: LocalTrack | MediaStreamTrack) => {
775
- const pub = this.unpublishTrack(track);
776
- if (pub) {
777
- publications.push(pub);
778
+ async unpublishTracks(
779
+ tracks: LocalTrack[] | MediaStreamTrack[],
780
+ ): Promise<LocalTrackPublication[]> {
781
+ const results = await Promise.all(tracks.map((track) => this.unpublishTrack(track)));
782
+ return results.filter(
783
+ (track) => track instanceof LocalTrackPublication,
784
+ ) as LocalTrackPublication[];
785
+ }
786
+
787
+ async republishAllTracks(options?: TrackPublishOptions) {
788
+ const localPubs: LocalTrackPublication[] = [];
789
+ this.tracks.forEach((pub) => {
790
+ if (pub.track) {
791
+ if (options) {
792
+ pub.options = { ...pub.options, ...options };
793
+ }
794
+ localPubs.push(pub);
778
795
  }
779
796
  });
780
- return publications;
797
+
798
+ await Promise.all(
799
+ localPubs.map(async (pub) => {
800
+ const track = pub.track!;
801
+ await this.unpublishTrack(track, false);
802
+ await this.publishTrack(track, pub.options);
803
+ }),
804
+ );
781
805
  }
782
806
 
783
807
  /**
@@ -964,7 +988,7 @@ export default class LocalParticipant extends Participant {
964
988
  });
965
989
  this.unpublishTrack(track);
966
990
  } else if (track.isUserProvided) {
967
- await track.pauseUpstream();
991
+ await track.mute();
968
992
  } else if (track instanceof LocalAudioTrack || track instanceof LocalVideoTrack) {
969
993
  try {
970
994
  if (isWeb()) {
@@ -993,8 +1017,8 @@ export default class LocalParticipant extends Participant {
993
1017
  log.debug('track ended, attempting to use a different device');
994
1018
  await track.restartTrack();
995
1019
  } catch (e) {
996
- log.warn(`could not restart track, pausing upstream instead`);
997
- await track.pauseUpstream();
1020
+ log.warn(`could not restart track, muting instead`);
1021
+ await track.mute();
998
1022
  }
999
1023
  }
1000
1024
  };
@@ -263,11 +263,6 @@ export default class RemoteParticipant extends Participant {
263
263
  validTracks.set(ti.sid, publication);
264
264
  });
265
265
 
266
- // always emit events for new publications, Room will not forward them unless it's ready
267
- newTracks.forEach((publication) => {
268
- this.emit(ParticipantEvent.TrackPublished, publication);
269
- });
270
-
271
266
  // detect removed tracks
272
267
  this.tracks.forEach((publication) => {
273
268
  if (!validTracks.has(publication.trackSid)) {
@@ -278,6 +273,11 @@ export default class RemoteParticipant extends Participant {
278
273
  this.unpublishTrack(publication.trackSid, true);
279
274
  }
280
275
  });
276
+
277
+ // always emit events for new publications, Room will not forward them unless it's ready
278
+ newTracks.forEach((publication) => {
279
+ this.emit(ParticipantEvent.TrackPublished, publication);
280
+ });
281
281
  }
282
282
 
283
283
  /** @internal */
@@ -305,7 +305,7 @@ function encodingsFromPresets(
305
305
  const rid = videoRids[idx];
306
306
  encodings.push({
307
307
  rid,
308
- scaleResolutionDownBy: size / Math.min(preset.width, preset.height),
308
+ scaleResolutionDownBy: Math.max(1, size / Math.min(preset.width, preset.height)),
309
309
  maxBitrate: preset.encoding.maxBitrate,
310
310
  /* @ts-ignore */
311
311
  maxFramerate: preset.encoding.maxFramerate,
@@ -114,7 +114,7 @@ export default class LocalAudioTrack extends LocalTrack {
114
114
  };
115
115
 
116
116
  async getSenderStats(): Promise<AudioSenderStats | undefined> {
117
- if (!this.sender) {
117
+ if (!this.sender?.getStats) {
118
118
  return undefined;
119
119
  }
120
120
 
@@ -3,10 +3,12 @@ import log from '../../logger';
3
3
  import DeviceManager from '../DeviceManager';
4
4
  import { TrackInvalidError } from '../errors';
5
5
  import { TrackEvent } from '../events';
6
- import { getEmptyAudioStreamTrack, getEmptyVideoStreamTrack, isMobile } from '../utils';
6
+ import { getEmptyAudioStreamTrack, getEmptyVideoStreamTrack, isMobile, sleep } from '../utils';
7
7
  import type { VideoCodec } from './options';
8
8
  import { attachToElement, detachTrack, Track } from './Track';
9
9
 
10
+ const defaultDimensionsTimeout = 2 * 1000;
11
+
10
12
  export default abstract class LocalTrack extends Track {
11
13
  /** @internal */
12
14
  sender?: RTCRtpSender;
@@ -72,6 +74,22 @@ export default abstract class LocalTrack extends Track {
72
74
  return this.providedByUser;
73
75
  }
74
76
 
77
+ async waitForDimensions(timeout = defaultDimensionsTimeout): Promise<Track.Dimensions> {
78
+ if (this.kind === Track.Kind.Audio) {
79
+ throw new Error('cannot get dimensions for audio tracks');
80
+ }
81
+
82
+ const started = Date.now();
83
+ while (Date.now() - started < timeout) {
84
+ const dims = this.dimensions;
85
+ if (dims) {
86
+ return dims;
87
+ }
88
+ await sleep(50);
89
+ }
90
+ throw new TrackInvalidError('unable to get track dimensions after timeout');
91
+ }
92
+
75
93
  /**
76
94
  * @returns DeviceID of the device that is currently being used for this track
77
95
  */
@@ -183,6 +201,7 @@ export default abstract class LocalTrack extends Track {
183
201
 
184
202
  this.mediaStream = mediaStream;
185
203
  this.constraints = constraints;
204
+ this.emit(TrackEvent.Restarted, this);
186
205
  return this;
187
206
  }
188
207