livekit-client 1.6.0 → 1.6.1

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.
@@ -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 */