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.
- package/dist/livekit-client.esm.mjs +245 -94
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/proto/livekit_rtc.d.ts +7 -0
- package/dist/src/proto/livekit_rtc.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +1 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +5 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +2 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/proto/livekit_rtc.ts +12 -0
- package/src/room/Room.ts +40 -15
- package/src/room/participant/LocalParticipant.ts +128 -55
- package/src/room/participant/RemoteParticipant.ts +5 -11
- package/src/room/track/LocalVideoTrack.ts +34 -7
- package/src/room/track/RemoteVideoTrack.ts +32 -4
- package/src/room/utils.ts +3 -2
@@ -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
|
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
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
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
|
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
|
-
|
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
|
-
//
|
216
|
-
|
217
|
-
|
218
|
-
|
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',
|
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
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
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
|
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.
|
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
|
85
|
-
canvas.
|
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();
|