livekit-client 1.4.4 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. package/dist/livekit-client.esm.mjs +2478 -5368
  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 +25 -0
  8. package/dist/src/connectionHelper/ConnectionCheck.d.ts.map +1 -0
  9. package/dist/src/connectionHelper/checks/Checker.d.ts +59 -0
  10. package/dist/src/connectionHelper/checks/Checker.d.ts.map +1 -0
  11. package/dist/src/connectionHelper/checks/publishAudio.d.ts +6 -0
  12. package/dist/src/connectionHelper/checks/publishAudio.d.ts.map +1 -0
  13. package/dist/src/connectionHelper/checks/publishVideo.d.ts +6 -0
  14. package/dist/src/connectionHelper/checks/publishVideo.d.ts.map +1 -0
  15. package/dist/src/connectionHelper/checks/reconnect.d.ts +6 -0
  16. package/dist/src/connectionHelper/checks/reconnect.d.ts.map +1 -0
  17. package/dist/src/connectionHelper/checks/turn.d.ts +6 -0
  18. package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -0
  19. package/dist/src/connectionHelper/checks/webrtc.d.ts +6 -0
  20. package/dist/src/connectionHelper/checks/webrtc.d.ts.map +1 -0
  21. package/dist/src/connectionHelper/checks/websocket.d.ts +6 -0
  22. package/dist/src/connectionHelper/checks/websocket.d.ts.map +1 -0
  23. package/dist/src/index.d.ts +6 -3
  24. package/dist/src/index.d.ts.map +1 -1
  25. package/dist/src/logger.d.ts +3 -3
  26. package/dist/src/logger.d.ts.map +1 -1
  27. package/dist/src/options.d.ts +4 -1
  28. package/dist/src/options.d.ts.map +1 -1
  29. package/dist/src/proto/google/protobuf/timestamp.d.ts +4 -4
  30. package/dist/src/proto/google/protobuf/timestamp.d.ts.map +1 -1
  31. package/dist/src/proto/livekit_models.d.ts +4 -4
  32. package/dist/src/proto/livekit_models.d.ts.map +1 -1
  33. package/dist/src/proto/livekit_rtc.d.ts +12 -4
  34. package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
  35. package/dist/src/room/DeviceManager.d.ts.map +1 -1
  36. package/dist/src/room/RTCEngine.d.ts +4 -3
  37. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  38. package/dist/src/room/Room.d.ts +27 -4
  39. package/dist/src/room/Room.d.ts.map +1 -1
  40. package/dist/src/room/events.d.ts +9 -1
  41. package/dist/src/room/events.d.ts.map +1 -1
  42. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  43. package/dist/src/room/participant/Participant.d.ts +1 -1
  44. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  45. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  46. package/dist/src/room/track/RemoteTrackPublication.d.ts +2 -0
  47. package/dist/src/room/track/RemoteTrackPublication.d.ts.map +1 -1
  48. package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
  49. package/dist/src/room/track/Track.d.ts +2 -1
  50. package/dist/src/room/track/Track.d.ts.map +1 -1
  51. package/dist/src/room/track/TrackPublication.d.ts +1 -1
  52. package/dist/src/room/track/TrackPublication.d.ts.map +1 -1
  53. package/dist/src/room/track/options.d.ts +3 -3
  54. package/dist/src/room/track/options.d.ts.map +1 -1
  55. package/dist/src/room/track/types.d.ts +3 -3
  56. package/dist/src/room/track/types.d.ts.map +1 -1
  57. package/dist/src/room/types.d.ts +13 -0
  58. package/dist/src/room/types.d.ts.map +1 -0
  59. package/dist/src/room/utils.d.ts +44 -0
  60. package/dist/src/room/utils.d.ts.map +1 -1
  61. package/dist/ts4.2/src/api/SignalClient.d.ts +86 -0
  62. package/dist/ts4.2/src/connectionHelper/ConnectionCheck.d.ts +25 -0
  63. package/dist/ts4.2/src/connectionHelper/checks/Checker.d.ts +59 -0
  64. package/dist/ts4.2/src/connectionHelper/checks/publishAudio.d.ts +6 -0
  65. package/dist/ts4.2/src/connectionHelper/checks/publishVideo.d.ts +6 -0
  66. package/dist/ts4.2/src/connectionHelper/checks/reconnect.d.ts +6 -0
  67. package/dist/ts4.2/src/connectionHelper/checks/turn.d.ts +6 -0
  68. package/dist/ts4.2/src/connectionHelper/checks/webrtc.d.ts +6 -0
  69. package/dist/ts4.2/src/connectionHelper/checks/websocket.d.ts +6 -0
  70. package/dist/ts4.2/src/index.d.ts +31 -0
  71. package/dist/ts4.2/src/logger.d.ts +26 -0
  72. package/dist/ts4.2/src/options.d.ts +94 -0
  73. package/dist/ts4.2/src/proto/google/protobuf/timestamp.d.ts +141 -0
  74. package/dist/ts4.2/src/proto/livekit_models.d.ts +1421 -0
  75. package/dist/ts4.2/src/proto/livekit_rtc.d.ts +7122 -0
  76. package/dist/ts4.2/src/room/DefaultReconnectPolicy.d.ts +8 -0
  77. package/dist/ts4.2/src/room/DeviceManager.d.ts +9 -0
  78. package/dist/ts4.2/src/room/PCTransport.d.ts +33 -0
  79. package/dist/ts4.2/src/room/RTCEngine.d.ts +97 -0
  80. package/dist/ts4.2/src/room/ReconnectPolicy.d.ts +23 -0
  81. package/dist/ts4.2/src/room/Room.d.ts +220 -0
  82. package/dist/ts4.2/src/room/defaults.d.ts +8 -0
  83. package/dist/ts4.2/src/room/errors.d.ts +39 -0
  84. package/dist/ts4.2/src/room/events.d.ts +426 -0
  85. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +141 -0
  86. package/dist/ts4.2/src/room/participant/Participant.d.ts +92 -0
  87. package/dist/ts4.2/src/room/participant/ParticipantTrackPermission.d.ts +26 -0
  88. package/dist/ts4.2/src/room/participant/RemoteParticipant.d.ts +52 -0
  89. package/dist/ts4.2/src/room/participant/publishUtils.d.ts +19 -0
  90. package/dist/ts4.2/src/room/stats.d.ts +67 -0
  91. package/dist/ts4.2/src/room/track/LocalAudioTrack.d.ts +25 -0
  92. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +42 -0
  93. package/dist/ts4.2/src/room/track/LocalTrackPublication.d.ts +38 -0
  94. package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +53 -0
  95. package/dist/ts4.2/src/room/track/RemoteAudioTrack.d.ts +53 -0
  96. package/dist/ts4.2/src/room/track/RemoteTrack.d.ts +15 -0
  97. package/dist/ts4.2/src/room/track/RemoteTrackPublication.d.ts +61 -0
  98. package/dist/ts4.2/src/room/track/RemoteVideoTrack.d.ts +52 -0
  99. package/dist/ts4.2/src/room/track/Track.d.ts +122 -0
  100. package/dist/ts4.2/src/room/track/TrackPublication.d.ts +68 -0
  101. package/dist/ts4.2/src/room/track/create.d.ts +24 -0
  102. package/dist/ts4.2/src/room/track/options.d.ts +241 -0
  103. package/dist/ts4.2/src/room/track/types.d.ts +23 -0
  104. package/dist/ts4.2/src/room/track/utils.d.ts +14 -0
  105. package/dist/ts4.2/src/room/types.d.ts +13 -0
  106. package/dist/ts4.2/src/room/utils.d.ts +79 -0
  107. package/dist/ts4.2/src/test/MockMediaStreamTrack.d.ts +26 -0
  108. package/dist/ts4.2/src/test/mocks.d.ts +11 -0
  109. package/dist/ts4.2/src/version.d.ts +3 -0
  110. package/package.json +32 -22
  111. package/src/api/SignalClient.ts +41 -17
  112. package/src/connectionHelper/ConnectionCheck.ts +90 -0
  113. package/src/connectionHelper/checks/Checker.ts +164 -0
  114. package/src/connectionHelper/checks/publishAudio.ts +33 -0
  115. package/src/connectionHelper/checks/publishVideo.ts +33 -0
  116. package/src/connectionHelper/checks/reconnect.ts +45 -0
  117. package/src/connectionHelper/checks/turn.ts +53 -0
  118. package/src/connectionHelper/checks/webrtc.ts +18 -0
  119. package/src/connectionHelper/checks/websocket.ts +22 -0
  120. package/src/index.ts +8 -1
  121. package/src/options.ts +5 -1
  122. package/src/proto/livekit_rtc.ts +12 -1
  123. package/src/room/DeviceManager.ts +0 -17
  124. package/src/room/RTCEngine.ts +35 -26
  125. package/src/room/Room.ts +231 -63
  126. package/src/room/events.ts +9 -0
  127. package/src/room/participant/LocalParticipant.ts +18 -11
  128. package/src/room/participant/publishUtils.ts +1 -1
  129. package/src/room/track/LocalAudioTrack.ts +1 -1
  130. package/src/room/track/LocalTrack.ts +4 -0
  131. package/src/room/track/LocalVideoTrack.ts +1 -1
  132. package/src/room/track/RemoteTrackPublication.ts +20 -0
  133. package/src/room/track/RemoteVideoTrack.ts +4 -0
  134. package/src/room/track/Track.ts +1 -0
  135. package/src/room/types.ts +12 -0
  136. package/src/room/utils.ts +150 -12
@@ -38,6 +38,7 @@ import type { TrackPublishOptions, VideoCodec } from './track/options';
38
38
  import { Track } from './track/Track';
39
39
  import {
40
40
  isWeb,
41
+ Mutex,
41
42
  sleep,
42
43
  supportsAddTrack,
43
44
  supportsSetCodecPreferences,
@@ -130,12 +131,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
130
131
  /** specifies how often an initial join connection is allowed to retry */
131
132
  private maxJoinAttempts: number = 1;
132
133
 
134
+ private closingLock: Mutex;
135
+
133
136
  constructor(private options: InternalRoomOptions) {
134
137
  super();
135
138
  this.client = new SignalClient();
136
139
  this.client.signalLatency = this.options.expSignalLatency;
137
140
  this.reconnectPolicy = this.options.reconnectPolicy;
138
141
  this.registerOnLineListener();
142
+ this.closingLock = new Mutex();
139
143
  }
140
144
 
141
145
  async join(
@@ -179,30 +183,35 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
179
183
  }
180
184
  }
181
185
 
182
- close() {
183
- this._isClosed = true;
184
- this.removeAllListeners();
185
- this.deregisterOnLineListener();
186
- this.clearPendingReconnect();
187
- if (this.publisher && this.publisher.pc.signalingState !== 'closed') {
188
- this.publisher.pc.getSenders().forEach((sender) => {
189
- try {
190
- // TODO: react-native-webrtc doesn't have removeTrack yet.
191
- if (this.publisher?.pc.removeTrack) {
192
- this.publisher?.pc.removeTrack(sender);
186
+ async close() {
187
+ const unlock = await this.closingLock.lock();
188
+ try {
189
+ this._isClosed = true;
190
+ this.removeAllListeners();
191
+ this.deregisterOnLineListener();
192
+ this.clearPendingReconnect();
193
+ if (this.publisher && this.publisher.pc.signalingState !== 'closed') {
194
+ this.publisher.pc.getSenders().forEach((sender) => {
195
+ try {
196
+ // TODO: react-native-webrtc doesn't have removeTrack yet.
197
+ if (this.publisher?.pc.removeTrack) {
198
+ this.publisher?.pc.removeTrack(sender);
199
+ }
200
+ } catch (e) {
201
+ log.warn('could not removeTrack', { error: e });
193
202
  }
194
- } catch (e) {
195
- log.warn('could not removeTrack', { error: e });
196
- }
197
- });
198
- this.publisher.close();
199
- this.publisher = undefined;
200
- }
201
- if (this.subscriber) {
202
- this.subscriber.close();
203
- this.subscriber = undefined;
203
+ });
204
+ this.publisher.close();
205
+ this.publisher = undefined;
206
+ }
207
+ if (this.subscriber) {
208
+ this.subscriber.close();
209
+ this.subscriber = undefined;
210
+ }
211
+ await this.client.close();
212
+ } finally {
213
+ unlock();
204
214
  }
205
- this.client.close();
206
215
  }
207
216
 
208
217
  addTrack(req: AddTrackRequest): Promise<TrackInfo> {
@@ -335,7 +344,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
335
344
  const shouldEmit = this.pcState === PCState.New;
336
345
  this.pcState = PCState.Connected;
337
346
  if (shouldEmit) {
338
- this.emit(EngineEvent.Connected);
347
+ this.emit(EngineEvent.Connected, joinResponse);
339
348
  }
340
349
  } else if (primaryPC.connectionState === 'failed') {
341
350
  // on Safari, PeerConnection will switch to 'disconnected' during renegotiation
@@ -784,9 +793,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
784
793
  }
785
794
 
786
795
  if (this.client.isConnected) {
787
- this.client.sendLeave();
796
+ await this.client.sendLeave();
788
797
  }
789
- this.client.close();
798
+ await this.client.close();
790
799
  this.primaryPC = undefined;
791
800
  this.publisher?.close();
792
801
  this.publisher = undefined;
@@ -1040,7 +1049,7 @@ async function getConnectedAddress(pc: RTCPeerConnection): Promise<string | unde
1040
1049
  class SignalReconnectError extends Error {}
1041
1050
 
1042
1051
  export type EngineEventCallbacks = {
1043
- connected: () => void;
1052
+ connected: (joinResp: JoinResponse) => void;
1044
1053
  disconnected: (reason?: DisconnectReason) => void;
1045
1054
  resuming: () => void;
1046
1055
  resumed: () => void;
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',
@@ -101,6 +113,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
101
113
  /** options of room */
102
114
  options: InternalRoomOptions;
103
115
 
116
+ private _isRecording: boolean = false;
117
+
104
118
  private identityToSid: Map<string, string>;
105
119
 
106
120
  /** connect options of room */
@@ -116,6 +130,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
116
130
  /** future holding client initiated connection attempt */
117
131
  private connectFuture?: Future<void>;
118
132
 
133
+ private disconnectLock: Mutex;
134
+
119
135
  /**
120
136
  * Creates a new Room, the primary construct for a LiveKit session.
121
137
  * @param options
@@ -142,6 +158,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
142
158
 
143
159
  this.maybeCreateEngine();
144
160
 
161
+ this.disconnectLock = new Mutex();
162
+
145
163
  this.localParticipant = new LocalParticipant('', '', this.engine, this.options);
146
164
  }
147
165
 
@@ -301,18 +319,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
301
319
 
302
320
  this.localParticipant.updateInfo(pi);
303
321
  // forward metadata changed for the local participant
304
- this.localParticipant
305
- .on(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
306
- .on(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
307
- .on(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
308
- .on(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
309
- .on(ParticipantEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished)
310
- .on(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged)
311
- .on(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError)
312
- .on(
313
- ParticipantEvent.ParticipantPermissionsChanged,
314
- this.onLocalParticipantPermissionsChanged,
315
- );
322
+ this.setupLocalParticipantEvents();
316
323
 
317
324
  // populate remote participants, these should not trigger new events
318
325
  joinResponse.otherParticipants.forEach((info) => {
@@ -332,11 +339,20 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
332
339
  this.name = joinResponse.room!.name;
333
340
  this.sid = joinResponse.room!.sid;
334
341
  this.metadata = joinResponse.room!.metadata;
342
+ if (this._isRecording !== joinResponse.room!.activeRecording) {
343
+ this._isRecording = joinResponse.room!.activeRecording;
344
+ this.emit(RoomEvent.RecordingStatusChanged, joinResponse.room!.activeRecording);
345
+ }
335
346
  this.emit(RoomEvent.SignalConnected);
336
347
  } catch (err) {
337
348
  this.recreateEngine();
338
349
  this.handleDisconnect(this.options.stopLocalTrackOnUnpublish);
339
- reject(new ConnectionError('could not establish signal connection'));
350
+ let errorMessage = '';
351
+ if (err instanceof Error) {
352
+ errorMessage = err.message;
353
+ log.debug(`error trying to establish signal connection`, { error: err });
354
+ }
355
+ reject(new ConnectionError(`could not establish signal connection: ${errorMessage}`));
340
356
  return;
341
357
  }
342
358
 
@@ -383,26 +399,38 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
383
399
  * disconnects the room, emits [[RoomEvent.Disconnected]]
384
400
  */
385
401
  disconnect = async (stopTracks = true) => {
386
- log.info('disconnect from room', { identity: this.localParticipant.identity });
387
- if (this.state === ConnectionState.Connecting || this.state === ConnectionState.Reconnecting) {
388
- // try aborting pending connection attempt
389
- log.warn('abort connection attempt');
390
- this.abortController?.abort();
391
- // in case the abort controller didn't manage to cancel the connection attempt, reject the connect promise explicitly
392
- this.connectFuture?.reject?.(new ConnectionError('Client initiated disconnect'));
393
- this.connectFuture = undefined;
394
- }
395
- // send leave
396
- if (this.engine?.client.isConnected) {
397
- await this.engine.client.sendLeave();
398
- }
399
- // close engine (also closes client)
400
- if (this.engine) {
401
- this.engine.close();
402
+ const unlock = await this.disconnectLock.lock();
403
+ try {
404
+ if (this.state === ConnectionState.Disconnected) {
405
+ log.debug('already disconnected');
406
+ return;
407
+ }
408
+ log.info('disconnect from room', { identity: this.localParticipant.identity });
409
+ if (
410
+ this.state === ConnectionState.Connecting ||
411
+ this.state === ConnectionState.Reconnecting
412
+ ) {
413
+ // try aborting pending connection attempt
414
+ log.warn('abort connection attempt');
415
+ this.abortController?.abort();
416
+ // in case the abort controller didn't manage to cancel the connection attempt, reject the connect promise explicitly
417
+ this.connectFuture?.reject?.(new ConnectionError('Client initiated disconnect'));
418
+ this.connectFuture = undefined;
419
+ }
420
+ // send leave
421
+ if (this.engine?.client.isConnected) {
422
+ await this.engine.client.sendLeave();
423
+ }
424
+ // close engine (also closes client)
425
+ if (this.engine) {
426
+ await this.engine.close();
427
+ }
428
+ this.handleDisconnect(stopTracks, DisconnectReason.CLIENT_INITIATED);
429
+ /* @ts-ignore */
430
+ this.engine = undefined;
431
+ } finally {
432
+ unlock();
402
433
  }
403
- this.handleDisconnect(stopTracks, DisconnectReason.CLIENT_INITIATED);
404
- /* @ts-ignore */
405
- this.engine = undefined;
406
434
  };
407
435
 
408
436
  /**
@@ -424,15 +452,22 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
424
452
  this.connectFuture = undefined;
425
453
  }
426
454
 
455
+ /**
456
+ * if the current room has a participant with `recorder: true` in its JWT grant
457
+ **/
458
+ get isRecording() {
459
+ return this._isRecording;
460
+ }
461
+
427
462
  /**
428
463
  * @internal for testing
429
464
  */
430
- simulateScenario(scenario: string) {
465
+ async simulateScenario(scenario: string) {
431
466
  let postAction = () => {};
432
467
  let req: SimulateScenario | undefined;
433
468
  switch (scenario) {
434
469
  case 'signal-reconnect':
435
- this.engine.client.close();
470
+ await this.engine.client.close();
436
471
  if (this.engine.client.onClose) {
437
472
  this.engine.client.onClose('simulate disconnect');
438
473
  }
@@ -495,8 +530,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
495
530
  }
496
531
  }
497
532
 
498
- private onBeforeUnload = () => {
499
- this.disconnect();
533
+ private onBeforeUnload = async () => {
534
+ await this.disconnect();
500
535
  };
501
536
 
502
537
  /**
@@ -507,7 +542,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
507
542
  * - `getUserMedia`
508
543
  */
509
544
  async startAudio() {
510
- this.acquireAudioContext();
545
+ await this.acquireAudioContext();
511
546
 
512
547
  const elements: Array<HTMLMediaElement> = [];
513
548
  this.participants.forEach((p) => {
@@ -537,7 +572,18 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
537
572
  }
538
573
 
539
574
  /**
540
- * Switches all active device used in this room to the given device.
575
+ * Returns the active audio output device used in this room.
576
+ *
577
+ * Note: to get the active `audioinput` or `videoinput` use [[LocalTrack.getDeviceId()]]
578
+ *
579
+ * @return the previously successfully set audio output device ID or an empty string if the default device is used.
580
+ */
581
+ getActiveAudioOutputDevice(): string {
582
+ return this.options.audioOutput?.deviceId ?? '';
583
+ }
584
+
585
+ /**
586
+ * Switches all active devices used in this room to the given device.
541
587
  *
542
588
  * Note: setting AudioOutput is not supported on some browsers. See [setSinkId](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId#browser_compatibility)
543
589
  *
@@ -579,16 +625,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
579
625
  this.options.audioOutput ??= {};
580
626
  const prevDeviceId = this.options.audioOutput.deviceId;
581
627
  this.options.audioOutput.deviceId = deviceId;
582
- const promises: Promise<void>[] = [];
583
- this.participants.forEach((p) => {
584
- promises.push(
585
- p.setAudioOutput({
586
- deviceId,
587
- }),
588
- );
589
- });
590
628
  try {
591
- await Promise.all(promises);
629
+ await Promise.all(
630
+ Array.from(this.participants.values()).map((p) => p.setAudioOutput({ deviceId })),
631
+ );
592
632
  } catch (e) {
593
633
  this.options.audioOutput.deviceId = prevDeviceId;
594
634
  throw e;
@@ -596,6 +636,21 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
596
636
  }
597
637
  }
598
638
 
639
+ private setupLocalParticipantEvents() {
640
+ this.localParticipant
641
+ .on(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
642
+ .on(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
643
+ .on(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
644
+ .on(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
645
+ .on(ParticipantEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished)
646
+ .on(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged)
647
+ .on(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError)
648
+ .on(
649
+ ParticipantEvent.ParticipantPermissionsChanged,
650
+ this.onLocalParticipantPermissionsChanged,
651
+ );
652
+ }
653
+
599
654
  private recreateEngine() {
600
655
  this.engine?.close();
601
656
  /* @ts-ignore */
@@ -760,7 +815,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
760
815
 
761
816
  this.participants.clear();
762
817
  this.activeSpeakers = [];
763
- if (this.audioContext) {
818
+ if (this.audioContext && typeof this.options.expWebAudioMix === 'boolean') {
764
819
  this.audioContext.close();
765
820
  this.audioContext = undefined;
766
821
  }
@@ -950,8 +1005,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
950
1005
  };
951
1006
 
952
1007
  private handleRoomUpdate = (r: RoomModel) => {
953
- this.metadata = r.metadata;
954
- this.emitWhenConnected(RoomEvent.RoomMetadataChanged, r.metadata);
1008
+ if (this._isRecording !== r.activeRecording) {
1009
+ this._isRecording = r.activeRecording;
1010
+ this.emit(RoomEvent.RecordingStatusChanged, r.activeRecording);
1011
+ }
1012
+ if (this.metadata !== r.metadata) {
1013
+ this.metadata = r.metadata;
1014
+ this.emitWhenConnected(RoomEvent.RoomMetadataChanged, r.metadata);
1015
+ }
955
1016
  };
956
1017
 
957
1018
  private handleConnectionQualityUpdate = (update: ConnectionQualityUpdate) => {
@@ -967,18 +1028,22 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
967
1028
  });
968
1029
  };
969
1030
 
970
- private acquireAudioContext() {
971
- if (this.audioContext) {
972
- this.audioContext.close();
1031
+ private async acquireAudioContext() {
1032
+ if (
1033
+ typeof this.options.expWebAudioMix !== 'boolean' &&
1034
+ this.options.expWebAudioMix.audioContext
1035
+ ) {
1036
+ // override audio context with custom audio context if supplied by user
1037
+ this.audioContext = this.options.expWebAudioMix.audioContext;
1038
+ await this.audioContext.resume();
1039
+ } else {
1040
+ // by using an AudioContext, it reduces lag on audio elements
1041
+ // https://stackoverflow.com/questions/9811429/html5-audio-tag-on-safari-has-a-delay/54119854#54119854
1042
+ this.audioContext = getNewAudioContext() ?? undefined;
973
1043
  }
974
- // by using an AudioContext, it reduces lag on audio elements
975
- // https://stackoverflow.com/questions/9811429/html5-audio-tag-on-safari-has-a-delay/54119854#54119854
976
- const ctx = getNewAudioContext();
977
- if (ctx) {
978
- this.audioContext = ctx;
979
- if (this.options.expWebAudioMix) {
980
- this.participants.forEach((participant) => participant.setAudioContext(this.audioContext));
981
- }
1044
+
1045
+ if (this.options.expWebAudioMix) {
1046
+ this.participants.forEach((participant) => participant.setAudioContext(this.audioContext));
982
1047
  }
983
1048
  }
984
1049
 
@@ -1193,6 +1258,108 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1193
1258
  this.emit(RoomEvent.ParticipantPermissionsChanged, prevPermissions, this.localParticipant);
1194
1259
  };
1195
1260
 
1261
+ /**
1262
+ * Allows to populate a room with simulated participants.
1263
+ * No actual connection to a server will be established, all state is
1264
+ * @experimental
1265
+ */
1266
+ simulateParticipants(options: SimulationOptions) {
1267
+ const publishOptions = {
1268
+ audio: true,
1269
+ video: true,
1270
+ ...options.publish,
1271
+ };
1272
+ const participantOptions = {
1273
+ count: 9,
1274
+ audio: false,
1275
+ video: true,
1276
+ aspectRatios: [1.66, 1.7, 1.3],
1277
+ ...options.participants,
1278
+ };
1279
+ this.handleDisconnect();
1280
+ this.name = 'simulated-room';
1281
+ this.localParticipant.identity = 'simulated-local';
1282
+ this.localParticipant.name = 'simulated-local';
1283
+ this.setupLocalParticipantEvents();
1284
+ this.emit(RoomEvent.SignalConnected);
1285
+ this.emit(RoomEvent.Connected);
1286
+ this.setAndEmitConnectionState(ConnectionState.Connected);
1287
+ if (publishOptions.video) {
1288
+ const camPub = new LocalTrackPublication(
1289
+ Track.Kind.Video,
1290
+ TrackInfo.fromPartial({
1291
+ source: TrackSource.CAMERA,
1292
+ sid: Math.floor(Math.random() * 10_000).toString(),
1293
+ type: TrackType.AUDIO,
1294
+ name: 'video-dummy',
1295
+ }),
1296
+ new LocalVideoTrack(
1297
+ createDummyVideoStreamTrack(
1298
+ 160 * participantOptions.aspectRatios[0] ?? 1,
1299
+ 160,
1300
+ true,
1301
+ true,
1302
+ ),
1303
+ ),
1304
+ );
1305
+ // @ts-ignore
1306
+ this.localParticipant.addTrackPublication(camPub);
1307
+ this.localParticipant.emit(ParticipantEvent.LocalTrackPublished, camPub);
1308
+ }
1309
+ if (publishOptions.audio) {
1310
+ const audioPub = new LocalTrackPublication(
1311
+ Track.Kind.Audio,
1312
+ TrackInfo.fromPartial({
1313
+ source: TrackSource.MICROPHONE,
1314
+ sid: Math.floor(Math.random() * 10_000).toString(),
1315
+ type: TrackType.AUDIO,
1316
+ }),
1317
+ new LocalAudioTrack(getEmptyAudioStreamTrack()),
1318
+ );
1319
+ // @ts-ignore
1320
+ this.localParticipant.addTrackPublication(audioPub);
1321
+ this.localParticipant.emit(ParticipantEvent.LocalTrackPublished, audioPub);
1322
+ }
1323
+
1324
+ for (let i = 0; i < participantOptions.count - 1; i += 1) {
1325
+ let info: ParticipantInfo = ParticipantInfo.fromPartial({
1326
+ sid: Math.floor(Math.random() * 10_000).toString(),
1327
+ identity: `simulated-${i}`,
1328
+ state: ParticipantInfo_State.ACTIVE,
1329
+ tracks: [],
1330
+ joinedAt: Date.now(),
1331
+ });
1332
+ const p = this.getOrCreateParticipant(info.identity, info);
1333
+ if (participantOptions.video) {
1334
+ const dummyVideo = createDummyVideoStreamTrack(
1335
+ 160 * participantOptions.aspectRatios[i % participantOptions.aspectRatios.length] ?? 1,
1336
+ 160,
1337
+ false,
1338
+ true,
1339
+ );
1340
+ const videoTrack = TrackInfo.fromPartial({
1341
+ source: TrackSource.CAMERA,
1342
+ sid: Math.floor(Math.random() * 10_000).toString(),
1343
+ type: TrackType.AUDIO,
1344
+ });
1345
+ p.addSubscribedMediaTrack(dummyVideo, videoTrack.sid, new MediaStream([dummyVideo]));
1346
+ info.tracks = [...info.tracks, videoTrack];
1347
+ }
1348
+ if (participantOptions.audio) {
1349
+ const dummyTrack = getEmptyAudioStreamTrack();
1350
+ const audioTrack = TrackInfo.fromPartial({
1351
+ source: TrackSource.MICROPHONE,
1352
+ sid: Math.floor(Math.random() * 10_000).toString(),
1353
+ type: TrackType.AUDIO,
1354
+ });
1355
+ p.addSubscribedMediaTrack(dummyTrack, audioTrack.sid, new MediaStream([dummyTrack]));
1356
+ info.tracks = [...info.tracks, audioTrack];
1357
+ }
1358
+
1359
+ p.updateInfo(info);
1360
+ }
1361
+ }
1362
+
1196
1363
  // /** @internal */
1197
1364
  emit<E extends keyof RoomEventCallbacks>(
1198
1365
  event: E,
@@ -1273,4 +1440,5 @@ export type RoomEventCallbacks = {
1273
1440
  ) => void;
1274
1441
  audioPlaybackChanged: (playing: boolean) => void;
1275
1442
  signalConnected: () => void;
1443
+ recordingStatusChanged: (recording: boolean) => void;
1276
1444
  };
@@ -250,6 +250,11 @@ export enum RoomEvent {
250
250
  * Signal connected, can publish tracks.
251
251
  */
252
252
  SignalConnected = 'signalConnected',
253
+
254
+ /**
255
+ * Recording of a room has started/stopped.
256
+ */
257
+ RecordingStatusChanged = 'recordingStatusChanged',
253
258
  }
254
259
 
255
260
  export enum ParticipantEvent {
@@ -422,6 +427,10 @@ export enum TrackEvent {
422
427
  Message = 'message',
423
428
  Muted = 'muted',
424
429
  Unmuted = 'unmuted',
430
+ /**
431
+ * Only fires on LocalTracks
432
+ */
433
+ Restarted = 'restarted',
425
434
  Ended = 'ended',
426
435
  Subscribed = 'subscribed',
427
436
  Unsubscribed = 'unsubscribed',
@@ -34,7 +34,7 @@ import {
34
34
  } from '../track/options';
35
35
  import { Track } from '../track/Track';
36
36
  import { constraintsForOptions, mergeDefaultOptions } from '../track/utils';
37
- import { isFireFox, isWeb, supportsAV1 } from '../utils';
37
+ import { isFireFox, isSafari, isWeb, supportsAV1 } from '../utils';
38
38
  import Participant from './Participant';
39
39
  import { ParticipantTrackPermission, trackPermissionToProto } from './ParticipantTrackPermission';
40
40
  import {
@@ -243,6 +243,7 @@ export default class LocalParticipant extends Participant {
243
243
  }
244
244
  const publishPromises: Array<Promise<LocalTrackPublication>> = [];
245
245
  for (const localTrack of localTracks) {
246
+ log.info('publishing track', { localTrack });
246
247
  publishPromises.push(this.publishTrack(localTrack, publishOptions));
247
248
  }
248
249
  const publishedTracks = await Promise.all(publishPromises);
@@ -374,14 +375,20 @@ export default class LocalParticipant extends Participant {
374
375
 
375
376
  let videoConstraints: MediaTrackConstraints | boolean = true;
376
377
  if (options.resolution) {
377
- videoConstraints = {
378
- width: options.resolution.width,
379
- height: options.resolution.height,
380
- frameRate: options.resolution.frameRate,
381
- };
378
+ if (isSafari()) {
379
+ videoConstraints = {
380
+ width: { max: options.resolution.width },
381
+ height: { max: options.resolution.height },
382
+ frameRate: options.resolution.frameRate,
383
+ };
384
+ } else {
385
+ videoConstraints = {
386
+ width: { ideal: options.resolution.width },
387
+ height: { ideal: options.resolution.height },
388
+ frameRate: options.resolution.frameRate,
389
+ };
390
+ }
382
391
  }
383
- // typescript definition is missing getDisplayMedia: https://github.com/microsoft/TypeScript/issues/33232
384
- // @ts-ignore
385
392
  const stream: MediaStream = await navigator.mediaDevices.getDisplayMedia({
386
393
  audio: options.audio ?? false,
387
394
  video: videoConstraints,
@@ -957,7 +964,7 @@ export default class LocalParticipant extends Participant {
957
964
  });
958
965
  this.unpublishTrack(track);
959
966
  } else if (track.isUserProvided) {
960
- await track.pauseUpstream();
967
+ await track.mute();
961
968
  } else if (track instanceof LocalAudioTrack || track instanceof LocalVideoTrack) {
962
969
  try {
963
970
  if (isWeb()) {
@@ -986,8 +993,8 @@ export default class LocalParticipant extends Participant {
986
993
  log.debug('track ended, attempting to use a different device');
987
994
  await track.restartTrack();
988
995
  } catch (e) {
989
- log.warn(`could not restart track, pausing upstream instead`);
990
- await track.pauseUpstream();
996
+ log.warn(`could not restart track, muting instead`);
997
+ await track.mute();
991
998
  }
992
999
  }
993
1000
  };
@@ -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
 
@@ -121,6 +121,9 @@ export default abstract class LocalTrack extends Track {
121
121
  }
122
122
  this._mediaStreamTrack = track;
123
123
 
124
+ // sync muted state with the enabled state of the newly provided track
125
+ this._mediaStreamTrack.enabled = !this.isMuted;
126
+
124
127
  await this.resumeUpstream();
125
128
 
126
129
  this.attachedElements.forEach((el) => {
@@ -180,6 +183,7 @@ export default abstract class LocalTrack extends Track {
180
183
 
181
184
  this.mediaStream = mediaStream;
182
185
  this.constraints = constraints;
186
+ this.emit(TrackEvent.Restarted, this);
183
187
  return this;
184
188
  }
185
189