livekit-client 1.0.4 → 1.1.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.
@@ -15,15 +15,14 @@ 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 {
20
+ AudioCaptureOptions,
23
21
  CreateLocalTracksOptions,
24
22
  ScreenShareCaptureOptions,
25
23
  ScreenSharePresets,
26
24
  TrackPublishOptions,
25
+ VideoCaptureOptions,
27
26
  VideoCodec,
28
27
  } from '../track/options';
29
28
  import { Track } from '../track/Track';
@@ -34,7 +33,7 @@ import { ParticipantTrackPermission, trackPermissionToProto } from './Participan
34
33
  import { computeVideoEncodings, mediaTrackToLocalTrack } from './publishUtils';
35
34
  import RemoteParticipant from './RemoteParticipant';
36
35
 
37
- const compatibleCodecForSVC = 'vp8';
36
+ const compatibleCodec = 'vp8';
38
37
  export default class LocalParticipant extends Participant {
39
38
  audioTracks: Map<string, LocalTrackPublication>;
40
39
 
@@ -117,8 +116,11 @@ export default class LocalParticipant extends Participant {
117
116
  * If a track has already published, it'll mute or unmute the track.
118
117
  * Resolves with a `LocalTrackPublication` instance if successful and `undefined` otherwise
119
118
  */
120
- setCameraEnabled(enabled: boolean): Promise<LocalTrackPublication | undefined> {
121
- return this.setTrackEnabled(Track.Source.Camera, enabled);
119
+ setCameraEnabled(
120
+ enabled: boolean,
121
+ options?: VideoCaptureOptions,
122
+ ): Promise<LocalTrackPublication | undefined> {
123
+ return this.setTrackEnabled(Track.Source.Camera, enabled, options);
122
124
  }
123
125
 
124
126
  /**
@@ -127,16 +129,22 @@ export default class LocalParticipant extends Participant {
127
129
  * If a track has already published, it'll mute or unmute the track.
128
130
  * Resolves with a `LocalTrackPublication` instance if successful and `undefined` otherwise
129
131
  */
130
- setMicrophoneEnabled(enabled: boolean): Promise<LocalTrackPublication | undefined> {
131
- return this.setTrackEnabled(Track.Source.Microphone, enabled);
132
+ setMicrophoneEnabled(
133
+ enabled: boolean,
134
+ options?: AudioCaptureOptions,
135
+ ): Promise<LocalTrackPublication | undefined> {
136
+ return this.setTrackEnabled(Track.Source.Microphone, enabled, options);
132
137
  }
133
138
 
134
139
  /**
135
140
  * Start or stop sharing a participant's screen
136
141
  * Resolves with a `LocalTrackPublication` instance if successful and `undefined` otherwise
137
142
  */
138
- setScreenShareEnabled(enabled: boolean): Promise<LocalTrackPublication | undefined> {
139
- return this.setTrackEnabled(Track.Source.ScreenShare, enabled);
143
+ setScreenShareEnabled(
144
+ enabled: boolean,
145
+ options?: ScreenShareCaptureOptions,
146
+ ): Promise<LocalTrackPublication | undefined> {
147
+ return this.setTrackEnabled(Track.Source.ScreenShare, enabled, options);
140
148
  }
141
149
 
142
150
  /** @internal */
@@ -155,16 +163,32 @@ export default class LocalParticipant extends Participant {
155
163
  * Resolves with LocalTrackPublication if successful and void otherwise
156
164
  */
157
165
  private async setTrackEnabled(
158
- source: Track.Source,
166
+ source: Extract<Track.Source, Track.Source.Camera>,
159
167
  enabled: boolean,
160
- ): Promise<LocalTrackPublication | undefined> {
168
+ options?: VideoCaptureOptions,
169
+ ): Promise<LocalTrackPublication | undefined>;
170
+ private async setTrackEnabled(
171
+ source: Extract<Track.Source, Track.Source.Microphone>,
172
+ enabled: boolean,
173
+ options?: AudioCaptureOptions,
174
+ ): Promise<LocalTrackPublication | undefined>;
175
+ private async setTrackEnabled(
176
+ source: Extract<Track.Source, Track.Source.ScreenShare>,
177
+ enabled: boolean,
178
+ options?: ScreenShareCaptureOptions,
179
+ ): Promise<LocalTrackPublication | undefined>;
180
+ private async setTrackEnabled(
181
+ source: Track.Source,
182
+ enabled: true,
183
+ options?: VideoCaptureOptions | AudioCaptureOptions | ScreenShareCaptureOptions,
184
+ ) {
161
185
  log.debug('setTrackEnabled', { source, enabled });
162
186
  let track = this.getTrack(source);
163
187
  if (enabled) {
164
188
  if (track) {
165
189
  await track.unmute();
166
190
  } else {
167
- let localTrack: LocalTrack | undefined;
191
+ let localTracks: Array<LocalTrack> | undefined;
168
192
  if (this.pendingPublishing.has(source)) {
169
193
  log.info('skipping duplicate published source', { source });
170
194
  // no-op it's already been requested
@@ -174,23 +198,32 @@ export default class LocalParticipant extends Participant {
174
198
  try {
175
199
  switch (source) {
176
200
  case Track.Source.Camera:
177
- [localTrack] = await this.createTracks({
178
- video: true,
201
+ localTracks = await this.createTracks({
202
+ video: (options as VideoCaptureOptions | undefined) ?? true,
179
203
  });
204
+
180
205
  break;
181
206
  case Track.Source.Microphone:
182
- [localTrack] = await this.createTracks({
183
- audio: true,
207
+ localTracks = await this.createTracks({
208
+ audio: (options as AudioCaptureOptions | undefined) ?? true,
184
209
  });
185
210
  break;
186
211
  case Track.Source.ScreenShare:
187
- [localTrack] = await this.createScreenTracks({ audio: false });
212
+ localTracks = await this.createScreenTracks({
213
+ ...(options as ScreenShareCaptureOptions | undefined),
214
+ });
188
215
  break;
189
216
  default:
190
217
  throw new TrackInvalidError(source);
191
218
  }
192
-
193
- track = await this.publishTrack(localTrack);
219
+ const publishPromises: Array<Promise<LocalTrackPublication>> = [];
220
+ for (const localTrack of localTracks) {
221
+ publishPromises.push(this.publishTrack(localTrack));
222
+ }
223
+ const publishedTracks = await Promise.all(publishPromises);
224
+ // for screen share publications including audio, this will only return the screen share publication, not the screen share audio one
225
+ // revisit if we want to return an array of tracks instead for v2
226
+ [track] = publishedTracks;
194
227
  } catch (e) {
195
228
  if (e instanceof Error && !(e instanceof TrackInvalidError)) {
196
229
  this.emit(ParticipantEvent.MediaDevicesError, e);
@@ -204,6 +237,10 @@ export default class LocalParticipant extends Participant {
204
237
  // screenshare cannot be muted, unpublish instead
205
238
  if (source === Track.Source.ScreenShare) {
206
239
  track = this.unpublishTrack(track.track);
240
+ const screenAudioTrack = this.getTrack(Track.Source.ScreenShareAudio);
241
+ if (screenAudioTrack && screenAudioTrack.track) {
242
+ this.unpublishTrack(screenAudioTrack.track);
243
+ }
207
244
  } else {
208
245
  await track.mute();
209
246
  }
@@ -415,7 +452,6 @@ export default class LocalParticipant extends Participant {
415
452
  // compute encodings and layers for video
416
453
  let encodings: RTCRtpEncodingParameters[] | undefined;
417
454
  let simEncodings: RTCRtpEncodingParameters[] | undefined;
418
- let simulcastTracks: SimulcastTrackInfo[] | undefined;
419
455
  if (track.kind === Track.Kind.Video) {
420
456
  // TODO: support react native, which doesn't expose getSettings
421
457
  const settings = track.mediaStreamTrack.getSettings();
@@ -425,37 +461,38 @@ export default class LocalParticipant extends Participant {
425
461
  req.width = width ?? 0;
426
462
  req.height = height ?? 0;
427
463
  // for svc codecs, disable simulcast and use vp8 for backup codec
428
- if (
429
- track instanceof LocalVideoTrack &&
430
- (opts?.videoCodec === 'vp9' || opts?.videoCodec === 'av1')
431
- ) {
432
- // set scalabilityMode to 'L3T3' by default
433
- opts.scalabilityMode = opts.scalabilityMode ?? 'L3T3';
434
-
435
- // add backup codec track
436
- const simOpts = { ...opts };
437
- simOpts.simulcast = true;
438
- simOpts.scalabilityMode = undefined;
439
- simEncodings = computeVideoEncodings(
440
- track.source === Track.Source.ScreenShare,
441
- width,
442
- height,
443
- simOpts,
444
- );
445
- const simulcastTrack = track.addSimulcastTrack(compatibleCodecForSVC, simEncodings);
446
- simulcastTracks = [simulcastTrack];
447
- req.simulcastCodecs = [
448
- {
449
- codec: opts.videoCodec,
450
- cid: track.mediaStreamTrack.id,
451
- enableSimulcastLayers: true,
452
- },
453
- {
454
- codec: simulcastTrack.codec,
455
- cid: simulcastTrack.mediaStreamTrack.id,
456
- enableSimulcastLayers: true,
457
- },
458
- ];
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
+ }
459
496
  }
460
497
 
461
498
  encodings = computeVideoEncodings(
@@ -501,22 +538,6 @@ export default class LocalParticipant extends Participant {
501
538
  track.codec = opts.videoCodec;
502
539
  }
503
540
 
504
- const localTrack = track as LocalVideoTrack;
505
- if (simulcastTracks) {
506
- for await (const simulcastTrack of simulcastTracks) {
507
- const simTransceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
508
- if (simulcastTrack.encodings) {
509
- simTransceiverInit.sendEncodings = simulcastTrack.encodings;
510
- }
511
- const simTransceiver = await this.engine.publisher!.pc.addTransceiver(
512
- simulcastTrack.mediaStreamTrack,
513
- simTransceiverInit,
514
- );
515
- this.setPreferredCodec(simTransceiver, localTrack.kind, simulcastTrack.codec);
516
- localTrack.setSimulcastTrackSender(simulcastTrack.codec, simTransceiver.sender);
517
- }
518
- }
519
-
520
541
  this.engine.negotiate();
521
542
 
522
543
  // store RTPSender
@@ -534,6 +555,91 @@ export default class LocalParticipant extends Participant {
534
555
  return publication;
535
556
  }
536
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
+
537
643
  unpublishTrack(
538
644
  track: LocalTrack | MediaStreamTrack,
539
645
  stopOnUnpublish?: boolean,
@@ -721,7 +827,7 @@ export default class LocalParticipant extends Participant {
721
827
  this.onTrackMuted(track, track.isMuted);
722
828
  };
723
829
 
724
- private handleSubscribedQualityUpdate = (update: SubscribedQualityUpdate) => {
830
+ private handleSubscribedQualityUpdate = async (update: SubscribedQualityUpdate) => {
725
831
  if (!this.roomOptions?.dynacast) {
726
832
  return;
727
833
  }
@@ -734,7 +840,14 @@ export default class LocalParticipant extends Participant {
734
840
  return;
735
841
  }
736
842
  if (update.subscribedCodecs.length > 0) {
737
- 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
+ }
738
851
  } else if (update.subscribedQualities.length > 0) {
739
852
  pub.videoTrack?.setPublishingLayers(update.subscribedQualities);
740
853
  }
@@ -794,35 +907,36 @@ export default class LocalParticipant extends Participant {
794
907
  const cap = RTCRtpSender.getCapabilities(kind);
795
908
  if (!cap) return;
796
909
  log.debug('get capabilities', cap);
797
- let selected: RTCRtpCodecCapability | undefined;
798
- const codecs: RTCRtpCodecCapability[] = [];
910
+ const matched: RTCRtpCodecCapability[] = [];
911
+ const partialMatched: RTCRtpCodecCapability[] = [];
912
+ const unmatched: RTCRtpCodecCapability[] = [];
799
913
  cap.codecs.forEach((c) => {
800
914
  const codec = c.mimeType.toLowerCase();
915
+ if (codec === 'audio/opus') {
916
+ matched.push(c);
917
+ return;
918
+ }
801
919
  const matchesVideoCodec = codec === `video/${videoCodec}`;
802
-
803
- if (selected !== undefined) {
804
- codecs.push(c);
920
+ if (!matchesVideoCodec) {
921
+ unmatched.push(c);
805
922
  return;
806
923
  }
807
924
  // for h264 codecs that have sdpFmtpLine available, use only if the
808
925
  // profile-level-id is 42e01f for cross-browser compatibility
809
- if (videoCodec === 'h264' && c.sdpFmtpLine) {
810
- if (matchesVideoCodec && c.sdpFmtpLine.includes('profile-level-id=42e01f')) {
811
- selected = c;
812
- return;
926
+ if (videoCodec === 'h264') {
927
+ if (c.sdpFmtpLine && c.sdpFmtpLine.includes('profile-level-id=42e01f')) {
928
+ matched.push(c);
929
+ } else {
930
+ partialMatched.push(c);
813
931
  }
814
- }
815
- if (matchesVideoCodec || codec === 'audio/opus') {
816
- selected = c;
817
932
  return;
818
933
  }
819
- codecs.push(c);
934
+
935
+ matched.push(c);
820
936
  });
821
937
 
822
- if (selected && 'setCodecPreferences' in transceiver) {
823
- // @ts-ignore
824
- codecs.unshift(selected);
825
- transceiver.setCodecPreferences(codecs);
938
+ if ('setCodecPreferences' in transceiver) {
939
+ transceiver.setCodecPreferences(matched.concat(partialMatched, unmatched));
826
940
  }
827
941
  }
828
942
 
@@ -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
 
@@ -305,7 +305,7 @@ class HTMLElementInfo implements ElementInfo {
305
305
  }
306
306
 
307
307
  height(): number {
308
- return this.element.clientWidth;
308
+ return this.element.clientHeight;
309
309
  }
310
310
 
311
311
  observe() {
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();