livekit-client 1.6.0 → 1.6.2

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