livekit-client 1.6.0 → 1.6.1

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