livekit-client 1.1.0 → 1.1.3

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.
@@ -15,10 +15,7 @@ import RTCEngine from '../RTCEngine';
15
15
  import LocalAudioTrack from '../track/LocalAudioTrack';
16
16
  import LocalTrack from '../track/LocalTrack';
17
17
  import LocalTrackPublication from '../track/LocalTrackPublication';
18
- import LocalVideoTrack, {
19
- SimulcastTrackInfo,
20
- videoLayersFromEncodings,
21
- } from '../track/LocalVideoTrack';
18
+ import LocalVideoTrack, { videoLayersFromEncodings } from '../track/LocalVideoTrack';
22
19
  import {
23
20
  AudioCaptureOptions,
24
21
  CreateLocalTracksOptions,
@@ -36,7 +33,7 @@ import { ParticipantTrackPermission, trackPermissionToProto } from './Participan
36
33
  import { computeVideoEncodings, mediaTrackToLocalTrack } from './publishUtils';
37
34
  import RemoteParticipant from './RemoteParticipant';
38
35
 
39
- const compatibleCodecForSVC = 'vp8';
36
+ const compatibleCodec = 'vp8';
40
37
  export default class LocalParticipant extends Participant {
41
38
  audioTracks: Map<string, LocalTrackPublication>;
42
39
 
@@ -455,7 +452,6 @@ export default class LocalParticipant extends Participant {
455
452
  // compute encodings and layers for video
456
453
  let encodings: RTCRtpEncodingParameters[] | undefined;
457
454
  let simEncodings: RTCRtpEncodingParameters[] | undefined;
458
- let simulcastTracks: SimulcastTrackInfo[] | undefined;
459
455
  if (track.kind === Track.Kind.Video) {
460
456
  // TODO: support react native, which doesn't expose getSettings
461
457
  const settings = track.mediaStreamTrack.getSettings();
@@ -465,37 +461,38 @@ export default class LocalParticipant extends Participant {
465
461
  req.width = width ?? 0;
466
462
  req.height = height ?? 0;
467
463
  // for svc codecs, disable simulcast and use vp8 for backup codec
468
- if (
469
- track instanceof LocalVideoTrack &&
470
- (opts?.videoCodec === 'vp9' || opts?.videoCodec === 'av1')
471
- ) {
472
- // set scalabilityMode to 'L3T3' by default
473
- opts.scalabilityMode = opts.scalabilityMode ?? 'L3T3';
474
-
475
- // add backup codec track
476
- const simOpts = { ...opts };
477
- simOpts.simulcast = true;
478
- simOpts.scalabilityMode = undefined;
479
- simEncodings = computeVideoEncodings(
480
- track.source === Track.Source.ScreenShare,
481
- width,
482
- height,
483
- simOpts,
484
- );
485
- const simulcastTrack = track.addSimulcastTrack(compatibleCodecForSVC, simEncodings);
486
- simulcastTracks = [simulcastTrack];
487
- req.simulcastCodecs = [
488
- {
489
- codec: opts.videoCodec,
490
- cid: track.mediaStreamTrack.id,
491
- enableSimulcastLayers: true,
492
- },
493
- {
494
- codec: simulcastTrack.codec,
495
- cid: simulcastTrack.mediaStreamTrack.id,
496
- enableSimulcastLayers: true,
497
- },
498
- ];
464
+ if (track instanceof LocalVideoTrack) {
465
+ if (opts?.videoCodec === 'vp9' || opts?.videoCodec === 'av1') {
466
+ // set scalabilityMode to 'L3T3' by default
467
+ opts.scalabilityMode = opts.scalabilityMode ?? 'L3T3';
468
+
469
+ // add backup codec track
470
+ const simOpts = { ...opts };
471
+ simOpts.simulcast = true;
472
+ simOpts.scalabilityMode = undefined;
473
+ simEncodings = computeVideoEncodings(
474
+ track.source === Track.Source.ScreenShare,
475
+ width,
476
+ height,
477
+ simOpts,
478
+ );
479
+ }
480
+
481
+ // set vp8 codec as backup for any other codecs
482
+ if (opts.videoCodec && opts.videoCodec !== 'vp8') {
483
+ req.simulcastCodecs = [
484
+ {
485
+ codec: opts.videoCodec,
486
+ cid: track.mediaStreamTrack.id,
487
+ enableSimulcastLayers: true,
488
+ },
489
+ {
490
+ codec: compatibleCodec,
491
+ cid: '',
492
+ enableSimulcastLayers: true,
493
+ },
494
+ ];
495
+ }
499
496
  }
500
497
 
501
498
  encodings = computeVideoEncodings(
@@ -541,22 +538,6 @@ export default class LocalParticipant extends Participant {
541
538
  track.codec = opts.videoCodec;
542
539
  }
543
540
 
544
- const localTrack = track as LocalVideoTrack;
545
- if (simulcastTracks) {
546
- for await (const simulcastTrack of simulcastTracks) {
547
- const simTransceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
548
- if (simulcastTrack.encodings) {
549
- simTransceiverInit.sendEncodings = simulcastTrack.encodings;
550
- }
551
- const simTransceiver = await this.engine.publisher!.pc.addTransceiver(
552
- simulcastTrack.mediaStreamTrack,
553
- simTransceiverInit,
554
- );
555
- this.setPreferredCodec(simTransceiver, localTrack.kind, simulcastTrack.codec);
556
- localTrack.setSimulcastTrackSender(simulcastTrack.codec, simTransceiver.sender);
557
- }
558
- }
559
-
560
541
  this.engine.negotiate();
561
542
 
562
543
  // store RTPSender
@@ -574,6 +555,91 @@ export default class LocalParticipant extends Participant {
574
555
  return publication;
575
556
  }
576
557
 
558
+ /** @internal
559
+ * publish additional codec to existing track
560
+ */
561
+ async publishAdditionalCodecForTrack(
562
+ track: LocalTrack | MediaStreamTrack,
563
+ videoCodec: VideoCodec,
564
+ options?: TrackPublishOptions,
565
+ ) {
566
+ const opts: TrackPublishOptions = {
567
+ ...this.roomOptions?.publishDefaults,
568
+ ...options,
569
+ };
570
+ // clear scalabilityMode setting for backup codec
571
+ opts.scalabilityMode = undefined;
572
+ opts.videoCodec = videoCodec;
573
+ // is it not published? if so skip
574
+ let existingPublication: LocalTrackPublication | undefined;
575
+ this.tracks.forEach((publication) => {
576
+ if (!publication.track) {
577
+ return;
578
+ }
579
+ if (publication.track === track) {
580
+ existingPublication = <LocalTrackPublication>publication;
581
+ }
582
+ });
583
+ if (!existingPublication) {
584
+ throw new TrackInvalidError('track is not published');
585
+ }
586
+
587
+ if (!(track instanceof LocalVideoTrack)) {
588
+ throw new TrackInvalidError('track is not a video track');
589
+ }
590
+
591
+ const settings = track.mediaStreamTrack.getSettings();
592
+ const width = settings.width ?? track.dimensions?.width;
593
+ const height = settings.height ?? track.dimensions?.height;
594
+
595
+ const encodings = computeVideoEncodings(
596
+ track.source === Track.Source.ScreenShare,
597
+ width,
598
+ height,
599
+ opts,
600
+ );
601
+ const simulcastTrack = track.addSimulcastTrack(opts.videoCodec, encodings);
602
+ const req = AddTrackRequest.fromPartial({
603
+ cid: simulcastTrack.mediaStreamTrack.id,
604
+ type: Track.kindToProto(track.kind),
605
+ muted: track.isMuted,
606
+ source: Track.sourceToProto(track.source),
607
+ sid: track.sid,
608
+ simulcastCodecs: [
609
+ {
610
+ codec: opts.videoCodec,
611
+ cid: simulcastTrack.mediaStreamTrack.id,
612
+ enableSimulcastLayers: opts.simulcast,
613
+ },
614
+ ],
615
+ });
616
+ req.layers = videoLayersFromEncodings(req.width, req.height, encodings);
617
+
618
+ if (!this.engine || this.engine.isClosed) {
619
+ throw new UnexpectedConnectionState('cannot publish track when not connected');
620
+ }
621
+
622
+ const ti = await this.engine.addTrack(req);
623
+
624
+ if (!this.engine.publisher) {
625
+ throw new UnexpectedConnectionState('publisher is closed');
626
+ }
627
+ const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
628
+ if (encodings) {
629
+ transceiverInit.sendEncodings = encodings;
630
+ }
631
+ // addTransceiver for react-native is async. web is synchronous, but await won't effect it.
632
+ const transceiver = await this.engine.publisher.pc.addTransceiver(
633
+ simulcastTrack.mediaStreamTrack,
634
+ transceiverInit,
635
+ );
636
+ this.setPreferredCodec(transceiver, track.kind, opts.videoCodec);
637
+ track.setSimulcastTrackSender(opts.videoCodec, transceiver.sender);
638
+
639
+ this.engine.negotiate();
640
+ log.debug(`published ${opts.videoCodec} for track ${track.sid}`, { encodings, trackInfo: ti });
641
+ }
642
+
577
643
  unpublishTrack(
578
644
  track: LocalTrack | MediaStreamTrack,
579
645
  stopOnUnpublish?: boolean,
@@ -761,7 +827,7 @@ export default class LocalParticipant extends Participant {
761
827
  this.onTrackMuted(track, track.isMuted);
762
828
  };
763
829
 
764
- private handleSubscribedQualityUpdate = (update: SubscribedQualityUpdate) => {
830
+ private handleSubscribedQualityUpdate = async (update: SubscribedQualityUpdate) => {
765
831
  if (!this.roomOptions?.dynacast) {
766
832
  return;
767
833
  }
@@ -774,7 +840,14 @@ export default class LocalParticipant extends Participant {
774
840
  return;
775
841
  }
776
842
  if (update.subscribedCodecs.length > 0) {
777
- pub.videoTrack?.setPublishingCodecs(update.subscribedCodecs);
843
+ if (!pub.videoTrack) {
844
+ return;
845
+ }
846
+ const newCodecs = await pub.videoTrack.setPublishingCodecs(update.subscribedCodecs);
847
+ for await (const codec of newCodecs) {
848
+ log.debug(`publish ${codec} for ${pub.videoTrack.sid}`);
849
+ await this.publishAdditionalCodecForTrack(pub.videoTrack, codec, pub.options);
850
+ }
778
851
  } else if (update.subscribedQualities.length > 0) {
779
852
  pub.videoTrack?.setPublishingLayers(update.subscribedQualities);
780
853
  }
@@ -23,9 +23,7 @@ export default class RemoteParticipant extends Participant {
23
23
 
24
24
  /** @internal */
25
25
  static fromParticipantInfo(signalClient: SignalClient, pi: ParticipantInfo): RemoteParticipant {
26
- const rp = new RemoteParticipant(signalClient, pi.sid, pi.identity);
27
- rp.updateInfo(pi);
28
- return rp;
26
+ return new RemoteParticipant(signalClient, pi.sid, pi.identity);
29
27
  }
30
28
 
31
29
  /** @internal */
@@ -182,8 +180,6 @@ export default class RemoteParticipant extends Participant {
182
180
 
183
181
  /** @internal */
184
182
  updateInfo(info: ParticipantInfo) {
185
- const alreadyHasMetadata = this.hasMetadata;
186
-
187
183
  super.updateInfo(info);
188
184
 
189
185
  // we are getting a list of all available tracks, reconcile in here
@@ -212,12 +208,10 @@ export default class RemoteParticipant extends Participant {
212
208
  validTracks.set(ti.sid, publication);
213
209
  });
214
210
 
215
- // send new tracks
216
- if (alreadyHasMetadata) {
217
- newTracks.forEach((publication) => {
218
- this.emit(ParticipantEvent.TrackPublished, publication);
219
- });
220
- }
211
+ // always emit events for new publications, Room will not forward them unless it's ready
212
+ newTracks.forEach((publication) => {
213
+ this.emit(ParticipantEvent.TrackPublished, publication);
214
+ });
221
215
 
222
216
  // detect removed tracks
223
217
  this.tracks.forEach((publication) => {
@@ -24,6 +24,8 @@ export class SimulcastTrackInfo {
24
24
  }
25
25
  }
26
26
 
27
+ const refreshSubscribedCodecAfterNewCodec = 5000;
28
+
27
29
  export default class LocalVideoTrack extends LocalTrack {
28
30
  /* internal */
29
31
  signalClient?: SignalClient;
@@ -37,6 +39,8 @@ export default class LocalVideoTrack extends LocalTrack {
37
39
  SimulcastTrackInfo
38
40
  >();
39
41
 
42
+ private subscribedCodecs?: SubscribedCodec[];
43
+
40
44
  constructor(mediaTrack: MediaStreamTrack, constraints?: MediaTrackConstraints) {
41
45
  super(mediaTrack, Track.Kind.Video, constraints);
42
46
  }
@@ -194,26 +198,48 @@ export default class LocalVideoTrack extends LocalTrack {
194
198
  return;
195
199
  }
196
200
  simulcastCodecInfo.sender = sender;
201
+
202
+ // browser will reenable disabled codec/layers after new codec has been published,
203
+ // so refresh subscribedCodecs after publish a new codec
204
+ setTimeout(() => {
205
+ if (this.subscribedCodecs) {
206
+ this.setPublishingCodecs(this.subscribedCodecs);
207
+ }
208
+ }, refreshSubscribedCodecAfterNewCodec);
197
209
  }
198
210
 
199
211
  /**
200
212
  * @internal
201
213
  * Sets codecs that should be publishing
202
214
  */
203
- async setPublishingCodecs(codecs: SubscribedCodec[]) {
204
- log.debug('setting publishing codecs', codecs);
215
+ async setPublishingCodecs(codecs: SubscribedCodec[]): Promise<VideoCodec[]> {
216
+ log.debug('setting publishing codecs', {
217
+ codecs,
218
+ currentCodec: this.codec,
219
+ });
220
+ // only enable simulcast codec for preference codec setted
221
+ if (!this.codec && codecs.length > 0) {
222
+ await this.setPublishingLayers(codecs[0].qualities);
223
+ return [];
224
+ }
225
+
226
+ this.subscribedCodecs = codecs;
205
227
 
228
+ const newCodecs: VideoCodec[] = [];
206
229
  for await (const codec of codecs) {
207
- if (this.codec === codec.codec) {
230
+ if (!this.codec || this.codec === codec.codec) {
208
231
  await this.setPublishingLayers(codec.qualities);
209
232
  } else {
210
233
  const simulcastCodecInfo = this.simulcastCodecs.get(codec.codec as VideoCodec);
211
234
  log.debug(`try setPublishingCodec for ${codec.codec}`, simulcastCodecInfo);
212
235
  if (!simulcastCodecInfo || !simulcastCodecInfo.sender) {
213
- return;
214
- }
215
-
216
- if (simulcastCodecInfo.encodings) {
236
+ for (const q of codec.qualities) {
237
+ if (q.enabled) {
238
+ newCodecs.push(codec.codec as VideoCodec);
239
+ break;
240
+ }
241
+ }
242
+ } else if (simulcastCodecInfo.encodings) {
217
243
  log.debug(`try setPublishingLayersForSender ${codec.codec}`);
218
244
  await setPublishingLayersForSender(
219
245
  simulcastCodecInfo.sender,
@@ -223,6 +249,7 @@ export default class LocalVideoTrack extends LocalTrack {
223
249
  }
224
250
  }
225
251
  }
252
+ return newCodecs;
226
253
  }
227
254
 
228
255
  /**
@@ -1,11 +1,11 @@
1
1
  import { debounce } from 'ts-debounce';
2
+ import log from '../../logger';
2
3
  import { TrackEvent } from '../events';
3
4
  import { computeBitrate, monitorFrequency, VideoReceiverStats } from '../stats';
4
5
  import { getIntersectionObserver, getResizeObserver, ObservableMediaElement } from '../utils';
5
6
  import RemoteTrack from './RemoteTrack';
6
7
  import { attachToElement, detachTrack, Track } from './Track';
7
8
  import { AdaptiveStreamSettings } from './types';
8
- import log from '../../logger';
9
9
 
10
10
  const REACTION_DELAY = 100;
11
11
 
@@ -110,6 +110,8 @@ export default class RemoteVideoTrack extends RemoteTrack {
110
110
  // the tab comes into focus for the first time.
111
111
  this.debouncedHandleResize();
112
112
  this.updateVisibility();
113
+ } else {
114
+ log.warn('visibility resize observer not triggered');
113
115
  }
114
116
  }
115
117
 
@@ -294,9 +296,9 @@ class HTMLElementInfo implements ElementInfo {
294
296
 
295
297
  handleVisibilityChanged?: () => void;
296
298
 
297
- constructor(element: HTMLMediaElement, visible: boolean = false) {
299
+ constructor(element: HTMLMediaElement, visible?: boolean) {
298
300
  this.element = element;
299
- this.visible = visible;
301
+ this.visible = visible ?? isElementInViewport(element);
300
302
  this.visibilityChangedAt = 0;
301
303
  }
302
304
 
@@ -305,7 +307,7 @@ class HTMLElementInfo implements ElementInfo {
305
307
  }
306
308
 
307
309
  height(): number {
308
- return this.element.clientWidth;
310
+ return this.element.clientHeight;
309
311
  }
310
312
 
311
313
  observe() {
@@ -332,3 +334,29 @@ class HTMLElementInfo implements ElementInfo {
332
334
  getResizeObserver()?.unobserve(this.element);
333
335
  }
334
336
  }
337
+
338
+ // does not account for occlusion by other elements
339
+ function isElementInViewport(el: HTMLElement) {
340
+ let top = el.offsetTop;
341
+ let left = el.offsetLeft;
342
+ const width = el.offsetWidth;
343
+ const height = el.offsetHeight;
344
+ const { hidden } = el;
345
+ const { opacity, display } = getComputedStyle(el);
346
+
347
+ while (el.offsetParent) {
348
+ el = el.offsetParent as HTMLElement;
349
+ top += el.offsetTop;
350
+ left += el.offsetLeft;
351
+ }
352
+
353
+ return (
354
+ top < window.pageYOffset + window.innerHeight &&
355
+ left < window.pageXOffset + window.innerWidth &&
356
+ top + height > window.pageYOffset &&
357
+ left + width > window.pageXOffset &&
358
+ !hidden &&
359
+ (opacity !== '' ? parseFloat(opacity) > 0 : true) &&
360
+ display !== 'none'
361
+ );
362
+ }
package/src/room/utils.ts CHANGED
@@ -81,8 +81,9 @@ let emptyVideoStreamTrack: MediaStreamTrack | undefined;
81
81
  export function getEmptyVideoStreamTrack() {
82
82
  if (!emptyVideoStreamTrack) {
83
83
  const canvas = document.createElement('canvas');
84
- canvas.width = 2;
85
- canvas.height = 2;
84
+ // the canvas size is set to 16, because electron apps seem to fail with smaller values
85
+ canvas.width = 16;
86
+ canvas.height = 16;
86
87
  canvas.getContext('2d')?.fillRect(0, 0, canvas.width, canvas.height);
87
88
  // @ts-ignore
88
89
  const emptyStream = canvas.captureStream();