livekit-client 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/livekit-client.esm.mjs +245 -109
  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/room/PCTransport.d.ts +7 -1
  6. package/dist/src/room/PCTransport.d.ts.map +1 -1
  7. package/dist/src/room/RTCEngine.d.ts +6 -1
  8. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  9. package/dist/src/room/Room.d.ts +1 -1
  10. package/dist/src/room/Room.d.ts.map +1 -1
  11. package/dist/src/room/events.d.ts +1 -0
  12. package/dist/src/room/events.d.ts.map +1 -1
  13. package/dist/src/room/participant/LocalParticipant.d.ts +3 -2
  14. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  15. package/dist/src/room/track/LocalTrack.d.ts +1 -0
  16. package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
  17. package/dist/src/room/track/Track.d.ts.map +1 -1
  18. package/dist/src/room/types.d.ts +1 -0
  19. package/dist/src/room/types.d.ts.map +1 -1
  20. package/dist/ts4.2/src/room/PCTransport.d.ts +7 -1
  21. package/dist/ts4.2/src/room/RTCEngine.d.ts +6 -1
  22. package/dist/ts4.2/src/room/Room.d.ts +1 -1
  23. package/dist/ts4.2/src/room/events.d.ts +1 -0
  24. package/dist/ts4.2/src/room/participant/LocalParticipant.d.ts +3 -2
  25. package/dist/ts4.2/src/room/track/LocalTrack.d.ts +1 -0
  26. package/dist/ts4.2/src/room/types.d.ts +1 -0
  27. package/package.json +15 -15
  28. package/src/room/PCTransport.ts +11 -1
  29. package/src/room/RTCEngine.ts +76 -23
  30. package/src/room/Room.ts +42 -12
  31. package/src/room/events.ts +1 -0
  32. package/src/room/participant/LocalParticipant.ts +43 -19
  33. package/src/room/participant/RemoteParticipant.ts +5 -5
  34. package/src/room/track/LocalTrack.ts +19 -1
  35. package/src/room/track/Track.ts +21 -5
  36. package/src/room/types.ts +1 -0
package/src/room/Room.ts CHANGED
@@ -239,14 +239,19 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
239
239
  await fetch(`http${url.substring(2)}`, { method: 'HEAD' });
240
240
  }
241
241
 
242
- 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
+
243
246
  if (this.state === ConnectionState.Connected) {
244
247
  // when the state is reconnecting or connected, this function returns immediately
245
248
  log.info(`already connected to room ${this.name}`);
249
+ unlockDisconnect();
246
250
  return Promise.resolve();
247
251
  }
248
252
 
249
253
  if (this.connectFuture) {
254
+ unlockDisconnect();
250
255
  return this.connectFuture.promise;
251
256
  }
252
257
 
@@ -256,6 +261,8 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
256
261
  if (!this.abortController || this.abortController.signal.aborted) {
257
262
  this.abortController = new AbortController();
258
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();
259
266
 
260
267
  if (this.state === ConnectionState.Reconnecting) {
261
268
  log.info('Reconnection attempt replaced by new connection attempt');
@@ -556,7 +563,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
556
563
  });
557
564
 
558
565
  try {
559
- 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
+ );
560
572
  this.handleAudioPlaybackStarted();
561
573
  } catch (err) {
562
574
  this.handleAudioPlaybackFailed(err);
@@ -1045,6 +1057,12 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1045
1057
  if (this.options.expWebAudioMix) {
1046
1058
  this.participants.forEach((participant) => participant.setAudioContext(this.audioContext));
1047
1059
  }
1060
+
1061
+ const newContextIsRunning = this.audioContext?.state === 'running';
1062
+ if (newContextIsRunning !== this.canPlaybackAudio) {
1063
+ this.audioEnabled = newContextIsRunning;
1064
+ this.emit(RoomEvent.AudioPlaybackStatusChanged, newContextIsRunning);
1065
+ }
1048
1066
  }
1049
1067
 
1050
1068
  private createParticipant(id: string, info?: ParticipantInfo): RemoteParticipant {
@@ -1263,10 +1281,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1263
1281
  * No actual connection to a server will be established, all state is
1264
1282
  * @experimental
1265
1283
  */
1266
- simulateParticipants(options: SimulationOptions) {
1284
+ async simulateParticipants(options: SimulationOptions) {
1267
1285
  const publishOptions = {
1268
1286
  audio: true,
1269
1287
  video: true,
1288
+ useRealTracks: false,
1270
1289
  ...options.publish,
1271
1290
  };
1272
1291
  const participantOptions = {
@@ -1278,8 +1297,13 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1278
1297
  };
1279
1298
  this.handleDisconnect();
1280
1299
  this.name = 'simulated-room';
1281
- this.localParticipant.identity = 'simulated-local';
1282
- this.localParticipant.name = 'simulated-local';
1300
+
1301
+ this.localParticipant.updateInfo(
1302
+ ParticipantInfo.fromPartial({
1303
+ identity: 'simulated-local',
1304
+ name: 'local-name',
1305
+ }),
1306
+ );
1283
1307
  this.setupLocalParticipantEvents();
1284
1308
  this.emit(RoomEvent.SignalConnected);
1285
1309
  this.emit(RoomEvent.Connected);
@@ -1294,12 +1318,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1294
1318
  name: 'video-dummy',
1295
1319
  }),
1296
1320
  new LocalVideoTrack(
1297
- createDummyVideoStreamTrack(
1298
- 160 * participantOptions.aspectRatios[0] ?? 1,
1299
- 160,
1300
- true,
1301
- true,
1302
- ),
1321
+ publishOptions.useRealTracks
1322
+ ? (await navigator.mediaDevices.getUserMedia({ video: true })).getVideoTracks()[0]
1323
+ : createDummyVideoStreamTrack(
1324
+ 160 * participantOptions.aspectRatios[0] ?? 1,
1325
+ 160,
1326
+ true,
1327
+ true,
1328
+ ),
1303
1329
  ),
1304
1330
  );
1305
1331
  // @ts-ignore
@@ -1314,7 +1340,11 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
1314
1340
  sid: Math.floor(Math.random() * 10_000).toString(),
1315
1341
  type: TrackType.AUDIO,
1316
1342
  }),
1317
- new LocalAudioTrack(getEmptyAudioStreamTrack()),
1343
+ new LocalAudioTrack(
1344
+ publishOptions.useRealTracks
1345
+ ? (await navigator.mediaDevices.getUserMedia({ audio: true })).getAudioTracks()[0]
1346
+ : getEmptyAudioStreamTrack(),
1347
+ ),
1318
1348
  );
1319
1349
  // @ts-ignore
1320
1350
  this.localParticipant.addTrackPublication(audioPub);
@@ -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',
@@ -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
  /**
@@ -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 */
@@ -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
  */
@@ -121,7 +121,9 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
121
121
  // we'll want to re-attach it in that case
122
122
  attachToElement(this._mediaStreamTrack, element);
123
123
 
124
- if (element instanceof HTMLAudioElement) {
124
+ // handle auto playback failures
125
+ const allMediaStreamTracks = (element.srcObject as MediaStream).getTracks();
126
+ if (allMediaStreamTracks.some((tr) => tr.kind === 'audio')) {
125
127
  // manually play audio to detect audio playback status
126
128
  element
127
129
  .play()
@@ -130,6 +132,17 @@ export abstract class Track extends (EventEmitter as new () => TypedEventEmitter
130
132
  })
131
133
  .catch((e) => {
132
134
  this.emit(TrackEvent.AudioPlaybackFailed, e);
135
+ // If audio playback isn't allowed make sure we still play back the video
136
+ if (
137
+ element &&
138
+ allMediaStreamTracks.some((tr) => tr.kind === 'video') &&
139
+ e.name === 'NotAllowedError'
140
+ ) {
141
+ element.muted = true;
142
+ element.play().catch(() => {
143
+ // catch for Safari, exceeded options at this point to automatically play the media element
144
+ });
145
+ }
133
146
  });
134
147
  }
135
148
 
@@ -259,6 +272,13 @@ export function attachToElement(track: MediaStreamTrack, element: HTMLMediaEleme
259
272
  mediaStream.addTrack(track);
260
273
  }
261
274
 
275
+ element.autoplay = true;
276
+ // In case there are no audio tracks present on the mediastream, we set the element as muted to ensure autoplay works
277
+ element.muted = mediaStream.getAudioTracks().length === 0;
278
+ if (element instanceof HTMLVideoElement) {
279
+ element.playsInline = true;
280
+ }
281
+
262
282
  // avoid flicker
263
283
  if (element.srcObject !== mediaStream) {
264
284
  element.srcObject = mediaStream;
@@ -280,10 +300,6 @@ export function attachToElement(track: MediaStreamTrack, element: HTMLMediaEleme
280
300
  }, 0);
281
301
  }
282
302
  }
283
- element.autoplay = true;
284
- if (element instanceof HTMLVideoElement) {
285
- element.playsInline = true;
286
- }
287
303
  }
288
304
 
289
305
  /** @internal */
package/src/room/types.ts CHANGED
@@ -2,6 +2,7 @@ export type SimulationOptions = {
2
2
  publish?: {
3
3
  audio?: boolean;
4
4
  video?: boolean;
5
+ useRealTracks?: boolean;
5
6
  };
6
7
  participants?: {
7
8
  count?: number;