livekit-client 1.14.1 → 1.14.3
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +25 -44
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +555 -306
- 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/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/utils.d.ts +0 -1
- package/dist/src/e2ee/utils.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
- package/dist/src/proto/livekit_models_pb.d.ts +87 -11
- package/dist/src/proto/livekit_models_pb.d.ts.map +1 -1
- package/dist/src/proto/livekit_rtc_pb.d.ts +0 -4
- package/dist/src/proto/livekit_rtc_pb.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +20 -1
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +1 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/defaults.d.ts +1 -0
- package/dist/src/room/defaults.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +3 -1
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -0
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/timers.d.ts +1 -1
- package/dist/src/room/timers.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +3 -3
- package/dist/src/room/track/LocalTrack.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/Track.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +0 -1
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts +2 -1
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/utils/cloneDeep.d.ts +2 -0
- package/dist/src/utils/cloneDeep.d.ts.map +1 -0
- package/dist/ts4.2/src/e2ee/utils.d.ts +0 -1
- package/dist/ts4.2/src/proto/livekit_models_pb.d.ts +87 -11
- package/dist/ts4.2/src/proto/livekit_rtc_pb.d.ts +0 -4
- package/dist/ts4.2/src/room/PCTransport.d.ts +20 -1
- package/dist/ts4.2/src/room/RTCEngine.d.ts +1 -1
- package/dist/ts4.2/src/room/Room.d.ts +1 -1
- package/dist/ts4.2/src/room/defaults.d.ts +1 -0
- package/dist/ts4.2/src/room/events.d.ts +3 -1
- package/dist/ts4.2/src/room/participant/Participant.d.ts +1 -0
- package/dist/ts4.2/src/room/timers.d.ts +1 -1
- package/dist/ts4.2/src/room/track/LocalAudioTrack.d.ts +1 -1
- package/dist/ts4.2/src/room/track/LocalTrack.d.ts +3 -3
- package/dist/ts4.2/src/room/track/LocalVideoTrack.d.ts +2 -1
- package/dist/ts4.2/src/room/track/options.d.ts +0 -1
- package/dist/ts4.2/src/room/track/utils.d.ts +1 -0
- package/dist/ts4.2/src/utils/cloneDeep.d.ts +2 -0
- package/package.json +15 -15
- package/src/connectionHelper/checks/webrtc.ts +1 -1
- package/src/e2ee/E2eeManager.ts +2 -1
- package/src/e2ee/utils.ts +0 -10
- package/src/e2ee/worker/FrameCryptor.ts +13 -14
- package/src/e2ee/worker/ParticipantKeyHandler.ts +4 -5
- package/src/e2ee/worker/e2ee.worker.ts +3 -1
- package/src/proto/livekit_models_pb.ts +140 -15
- package/src/proto/livekit_rtc_pb.ts +1 -7
- package/src/room/PCTransport.ts +122 -5
- package/src/room/RTCEngine.ts +56 -92
- package/src/room/Room.ts +14 -11
- package/src/room/defaults.ts +4 -2
- package/src/room/events.ts +5 -1
- package/src/room/participant/LocalParticipant.ts +47 -68
- package/src/room/participant/Participant.ts +1 -0
- package/src/room/track/LocalAudioTrack.ts +1 -1
- package/src/room/track/LocalTrack.ts +8 -5
- package/src/room/track/LocalVideoTrack.ts +2 -1
- package/src/room/track/Track.ts +6 -1
- package/src/room/track/options.ts +0 -7
- package/src/room/track/utils.ts +17 -8
- package/src/utils/cloneDeep.test.ts +54 -0
- package/src/utils/cloneDeep.ts +11 -0
@@ -18,6 +18,7 @@ import {
|
|
18
18
|
TrackUnpublishedResponse,
|
19
19
|
} from '../../proto/livekit_rtc_pb';
|
20
20
|
import type RTCEngine from '../RTCEngine';
|
21
|
+
import { defaultVideoCodec } from '../defaults';
|
21
22
|
import { DeviceUnsupportedError, TrackInvalidError, UnexpectedConnectionState } from '../errors';
|
22
23
|
import { EngineEvent, ParticipantEvent, TrackEvent } from '../events';
|
23
24
|
import LocalAudioTrack from '../track/LocalAudioTrack';
|
@@ -33,10 +34,11 @@ import type {
|
|
33
34
|
TrackPublishOptions,
|
34
35
|
VideoCaptureOptions,
|
35
36
|
} from '../track/options';
|
36
|
-
import { VideoPresets, isBackupCodec
|
37
|
+
import { VideoPresets, isBackupCodec } from '../track/options';
|
37
38
|
import {
|
38
39
|
constraintsForOptions,
|
39
40
|
mergeDefaultOptions,
|
41
|
+
mimeTypeToVideoCodecString,
|
40
42
|
screenCaptureToDisplayMediaStreamOptions,
|
41
43
|
} from '../track/utils';
|
42
44
|
import type { DataPublishOptions } from '../types';
|
@@ -408,6 +410,7 @@ export default class LocalParticipant extends Participant {
|
|
408
410
|
|
409
411
|
if (constraints.audio) {
|
410
412
|
this.microphoneError = undefined;
|
413
|
+
this.emit(ParticipantEvent.AudioStreamAcquired);
|
411
414
|
}
|
412
415
|
if (constraints.video) {
|
413
416
|
this.cameraError = undefined;
|
@@ -460,6 +463,7 @@ export default class LocalParticipant extends Participant {
|
|
460
463
|
screenVideo.source = Track.Source.ScreenShare;
|
461
464
|
const localTracks: Array<LocalTrack> = [screenVideo];
|
462
465
|
if (stream.getAudioTracks().length > 0) {
|
466
|
+
this.emit(ParticipantEvent.AudioStreamAcquired);
|
463
467
|
const screenAudio = new LocalAudioTrack(
|
464
468
|
stream.getAudioTracks()[0],
|
465
469
|
undefined,
|
@@ -599,18 +603,7 @@ export default class LocalParticipant extends Participant {
|
|
599
603
|
(publishedTrack) => track instanceof LocalTrack && publishedTrack.source === track.source,
|
600
604
|
);
|
601
605
|
if (existingTrackOfSource && track.source !== Track.Source.Unknown) {
|
602
|
-
|
603
|
-
// throw an Error in order to capture the stack trace
|
604
|
-
throw Error(`publishing a second track with the same source: ${track.source}`);
|
605
|
-
} catch (e: unknown) {
|
606
|
-
if (e instanceof Error) {
|
607
|
-
log.warn(e.message, {
|
608
|
-
oldTrack: existingTrackOfSource,
|
609
|
-
newTrack: track,
|
610
|
-
trace: e.stack,
|
611
|
-
});
|
612
|
-
}
|
613
|
-
}
|
606
|
+
log.info(`publishing a second track with the same source: ${track.source}`);
|
614
607
|
}
|
615
608
|
if (opts.stopMicTrackOnMute && track instanceof LocalAudioTrack) {
|
616
609
|
track.stopOnMute = true;
|
@@ -629,6 +622,10 @@ export default class LocalParticipant extends Participant {
|
|
629
622
|
if (opts.videoCodec === 'vp9' && !supportsVP9()) {
|
630
623
|
opts.videoCodec = undefined;
|
631
624
|
}
|
625
|
+
if (opts.videoCodec === undefined) {
|
626
|
+
opts.videoCodec = defaultVideoCodec;
|
627
|
+
}
|
628
|
+
const videoCodec = opts.videoCodec;
|
632
629
|
|
633
630
|
// handle track actions
|
634
631
|
track.on(TrackEvent.Muted, this.onTrackMuted);
|
@@ -654,7 +651,6 @@ export default class LocalParticipant extends Participant {
|
|
654
651
|
|
655
652
|
// compute encodings and layers for video
|
656
653
|
let encodings: RTCRtpEncodingParameters[] | undefined;
|
657
|
-
let simEncodings: RTCRtpEncodingParameters[] | undefined;
|
658
654
|
if (track.kind === Track.Kind.Video) {
|
659
655
|
let dims: Track.Dimensions = {
|
660
656
|
width: 0,
|
@@ -679,53 +675,40 @@ export default class LocalParticipant extends Participant {
|
|
679
675
|
req.height = dims.height;
|
680
676
|
// for svc codecs, disable simulcast and use vp8 for backup codec
|
681
677
|
if (track instanceof LocalVideoTrack) {
|
682
|
-
if (isSVCCodec(
|
678
|
+
if (isSVCCodec(videoCodec)) {
|
683
679
|
// vp9 svc with screenshare has problem to encode, always use L1T3 here
|
684
|
-
if (track.source === Track.Source.ScreenShare &&
|
680
|
+
if (track.source === Track.Source.ScreenShare && videoCodec === 'vp9') {
|
685
681
|
opts.scalabilityMode = 'L1T3';
|
686
682
|
}
|
687
683
|
// set scalabilityMode to 'L3T3_KEY' by default
|
688
684
|
opts.scalabilityMode = opts.scalabilityMode ?? 'L3T3_KEY';
|
689
685
|
}
|
690
686
|
|
687
|
+
req.simulcastCodecs = [
|
688
|
+
new SimulcastCodec({
|
689
|
+
codec: videoCodec,
|
690
|
+
cid: track.mediaStreamTrack.id,
|
691
|
+
}),
|
692
|
+
];
|
693
|
+
|
691
694
|
// set up backup
|
692
|
-
if (opts.
|
695
|
+
if (opts.backupCodec && videoCodec !== opts.backupCodec.codec) {
|
693
696
|
if (!this.roomOptions.dynacast) {
|
694
697
|
this.roomOptions.dynacast = true;
|
695
698
|
}
|
696
|
-
|
697
|
-
simOpts.simulcast = true;
|
698
|
-
simEncodings = computeTrackBackupEncodings(track, opts.backupCodec.codec, simOpts);
|
699
|
-
|
700
|
-
req.simulcastCodecs = [
|
701
|
-
new SimulcastCodec({
|
702
|
-
codec: opts.videoCodec,
|
703
|
-
cid: track.mediaStreamTrack.id,
|
704
|
-
enableSimulcastLayers: true,
|
705
|
-
}),
|
699
|
+
req.simulcastCodecs.push(
|
706
700
|
new SimulcastCodec({
|
707
701
|
codec: opts.backupCodec.codec,
|
708
702
|
cid: '',
|
709
|
-
enableSimulcastLayers: true,
|
710
|
-
}),
|
711
|
-
];
|
712
|
-
} else if (opts.videoCodec) {
|
713
|
-
// pass codec info to sfu so it can prefer codec for the client which don't support
|
714
|
-
// setCodecPreferences
|
715
|
-
req.simulcastCodecs = [
|
716
|
-
new SimulcastCodec({
|
717
|
-
codec: opts.videoCodec,
|
718
|
-
cid: track.mediaStreamTrack.id,
|
719
|
-
enableSimulcastLayers: opts.simulcast ?? false,
|
720
703
|
}),
|
721
|
-
|
704
|
+
);
|
722
705
|
}
|
723
706
|
}
|
724
707
|
|
725
708
|
encodings = computeVideoEncodings(
|
726
709
|
track.source === Track.Source.ScreenShare,
|
727
|
-
|
728
|
-
|
710
|
+
req.width,
|
711
|
+
req.height,
|
729
712
|
opts,
|
730
713
|
);
|
731
714
|
req.layers = videoLayersFromEncodings(
|
@@ -749,30 +732,28 @@ export default class LocalParticipant extends Participant {
|
|
749
732
|
}
|
750
733
|
|
751
734
|
const ti = await this.engine.addTrack(req);
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
backupCodecSupported = true;
|
735
|
+
// server might not support the codec the client has requested, in that case, fallback
|
736
|
+
// to a supported codec
|
737
|
+
let primaryCodecMime: string | undefined;
|
738
|
+
ti.codecs.forEach((codec) => {
|
739
|
+
if (primaryCodecMime === undefined) {
|
740
|
+
primaryCodecMime = codec.mimeType;
|
759
741
|
}
|
760
742
|
});
|
761
|
-
|
762
|
-
|
763
|
-
if (
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
743
|
+
if (primaryCodecMime && track.kind === Track.Kind.Video) {
|
744
|
+
const updatedCodec = mimeTypeToVideoCodecString(primaryCodecMime);
|
745
|
+
if (updatedCodec !== videoCodec) {
|
746
|
+
log.debug('falling back to server selected codec', { codec: updatedCodec });
|
747
|
+
/* @ts-ignore */
|
748
|
+
opts.videoCodec = updatedCodec;
|
749
|
+
|
750
|
+
// recompute encodings since bitrates/etc could have changed
|
751
|
+
encodings = computeVideoEncodings(
|
752
|
+
track.source === Track.Source.ScreenShare,
|
753
|
+
req.width,
|
754
|
+
req.height,
|
755
|
+
opts,
|
772
756
|
);
|
773
|
-
opts.videoCodec = backupCodec.codec;
|
774
|
-
opts.videoEncoding = backupCodec.encoding;
|
775
|
-
encodings = simEncodings;
|
776
757
|
}
|
777
758
|
}
|
778
759
|
|
@@ -786,20 +767,19 @@ export default class LocalParticipant extends Participant {
|
|
786
767
|
}
|
787
768
|
log.debug(`publishing ${track.kind} with encodings`, { encodings, trackInfo: ti });
|
788
769
|
|
789
|
-
// store RTPSender
|
790
770
|
track.sender = await this.engine.createSender(track, opts, encodings);
|
791
771
|
|
792
772
|
if (encodings) {
|
793
773
|
if (isFireFox() && track.kind === Track.Kind.Audio) {
|
794
774
|
/* Refer to RFC https://datatracker.ietf.org/doc/html/rfc7587#section-6.1,
|
795
|
-
livekit-server uses maxaveragebitrate=
|
775
|
+
livekit-server uses maxaveragebitrate=510000 in the answer sdp to permit client to
|
796
776
|
publish high quality audio track. But firefox always uses this value as the actual
|
797
777
|
bitrates, causing the audio bitrates to rise to 510Kbps in any stereo case unexpectedly.
|
798
778
|
So the client need to modify maxaverragebitrates in answer sdp to user provided value to
|
799
779
|
fix the issue.
|
800
780
|
*/
|
801
781
|
let trackTransceiver: RTCRtpTransceiver | undefined = undefined;
|
802
|
-
for (const transceiver of this.engine.publisher.
|
782
|
+
for (const transceiver of this.engine.publisher.getTransceivers()) {
|
803
783
|
if (transceiver.sender === track.sender) {
|
804
784
|
trackTransceiver = transceiver;
|
805
785
|
break;
|
@@ -889,7 +869,6 @@ export default class LocalParticipant extends Participant {
|
|
889
869
|
{
|
890
870
|
codec: opts.videoCodec,
|
891
871
|
cid: simulcastTrack.mediaStreamTrack.id,
|
892
|
-
enableSimulcastLayers: opts.simulcast,
|
893
872
|
},
|
894
873
|
],
|
895
874
|
});
|
@@ -947,11 +926,11 @@ export default class LocalParticipant extends Participant {
|
|
947
926
|
track.sender = undefined;
|
948
927
|
if (
|
949
928
|
this.engine.publisher &&
|
950
|
-
this.engine.publisher.
|
929
|
+
this.engine.publisher.getConnectionState() !== 'closed' &&
|
951
930
|
trackSender
|
952
931
|
) {
|
953
932
|
try {
|
954
|
-
for (const transceiver of this.engine.publisher.
|
933
|
+
for (const transceiver of this.engine.publisher.getTransceivers()) {
|
955
934
|
// if sender is not currently sending (after replaceTrack(null))
|
956
935
|
// removeTrack would have no effect.
|
957
936
|
// to ensure we end up successfully removing the track, manually set
|
@@ -309,6 +309,7 @@ export type ParticipantEventCallbacks = {
|
|
309
309
|
status: TrackPublication.PermissionStatus,
|
310
310
|
) => void;
|
311
311
|
mediaDevicesError: (error: Error) => void;
|
312
|
+
audioStreamAcquired: () => void;
|
312
313
|
participantPermissionsChanged: (prevPermissions?: ParticipantPermission) => void;
|
313
314
|
trackSubscriptionStatusChanged: (
|
314
315
|
publication: RemoteTrackPublication,
|
@@ -138,7 +138,7 @@ export default class LocalAudioTrack extends LocalTrack {
|
|
138
138
|
this.prevStats = stats;
|
139
139
|
};
|
140
140
|
|
141
|
-
async setProcessor(processor: TrackProcessor<
|
141
|
+
async setProcessor(processor: TrackProcessor<this['kind']>) {
|
142
142
|
const unlock = await this.processorLock.lock();
|
143
143
|
try {
|
144
144
|
if (!this.audioContext) {
|
@@ -34,7 +34,7 @@ export default abstract class LocalTrack extends Track {
|
|
34
34
|
|
35
35
|
protected processorElement?: HTMLMediaElement;
|
36
36
|
|
37
|
-
protected processor?: TrackProcessor<
|
37
|
+
protected processor?: TrackProcessor<this['kind']>;
|
38
38
|
|
39
39
|
protected processorLock: Mutex;
|
40
40
|
|
@@ -163,6 +163,12 @@ export default abstract class LocalTrack extends Track {
|
|
163
163
|
throw new Error('cannot get dimensions for audio tracks');
|
164
164
|
}
|
165
165
|
|
166
|
+
if (getBrowser()?.os === 'iOS') {
|
167
|
+
// browsers report wrong initial resolution on iOS.
|
168
|
+
// when slightly delaying the call to .getSettings(), the correct resolution is being reported
|
169
|
+
await sleep(10);
|
170
|
+
}
|
171
|
+
|
166
172
|
const started = Date.now();
|
167
173
|
while (Date.now() - started < timeout) {
|
168
174
|
const dims = this.dimensions;
|
@@ -396,10 +402,7 @@ export default abstract class LocalTrack extends Track {
|
|
396
402
|
* @param showProcessedStreamLocally
|
397
403
|
* @returns
|
398
404
|
*/
|
399
|
-
async setProcessor(
|
400
|
-
processor: TrackProcessor<typeof this.kind>,
|
401
|
-
showProcessedStreamLocally = true,
|
402
|
-
) {
|
405
|
+
async setProcessor(processor: TrackProcessor<this['kind']>, showProcessedStreamLocally = true) {
|
403
406
|
const unlock = await this.processorLock.lock();
|
404
407
|
try {
|
405
408
|
log.debug('setting up processor');
|
@@ -284,7 +284,8 @@ export default class LocalVideoTrack extends LocalTrack {
|
|
284
284
|
|
285
285
|
/**
|
286
286
|
* @internal
|
287
|
-
* Sets codecs that should be publishing
|
287
|
+
* Sets codecs that should be publishing, returns new codecs that have not yet
|
288
|
+
* been published
|
288
289
|
*/
|
289
290
|
async setPublishingCodecs(codecs: SubscribedCodec[]): Promise<VideoCodec[]> {
|
290
291
|
log.debug('setting publishing codecs', {
|
package/src/room/track/Track.ts
CHANGED
@@ -293,7 +293,12 @@ export function attachToElement(track: MediaStreamTrack, element: HTMLMediaEleme
|
|
293
293
|
mediaStream.addTrack(track);
|
294
294
|
}
|
295
295
|
|
296
|
-
element
|
296
|
+
if (!isSafari() || !(element instanceof HTMLVideoElement)) {
|
297
|
+
// when in low power mode (applies to both macOS and iOS), Safari will show a play/pause overlay
|
298
|
+
// when a video starts that has the `autoplay` attribute is set.
|
299
|
+
// we work around this by _not_ setting the autoplay attribute on safari and instead call `setTimeout(() => el.play(),0)` further down
|
300
|
+
element.autoplay = true;
|
301
|
+
}
|
297
302
|
// In case there are no audio tracks present on the mediastream, we set the element as muted to ensure autoplay works
|
298
303
|
element.muted = mediaStream.getAudioTracks().length === 0;
|
299
304
|
if (element instanceof HTMLVideoElement) {
|
@@ -301,13 +301,6 @@ export function isBackupCodec(codec: string): codec is BackupVideoCodec {
|
|
301
301
|
return !!backupCodecs.find((backup) => backup === codec);
|
302
302
|
}
|
303
303
|
|
304
|
-
export function isCodecEqual(c1: string | undefined, c2: string | undefined): boolean {
|
305
|
-
return (
|
306
|
-
c1?.toLowerCase().replace(/audio\/|video\//y, '') ===
|
307
|
-
c2?.toLowerCase().replace(/audio\/|video\//y, '')
|
308
|
-
);
|
309
|
-
}
|
310
|
-
|
311
304
|
/**
|
312
305
|
* scalability modes for svc.
|
313
306
|
*/
|
package/src/room/track/utils.ts
CHANGED
@@ -1,10 +1,13 @@
|
|
1
|
+
import { cloneDeep } from '../../utils/cloneDeep';
|
1
2
|
import { isSafari, sleep } from '../utils';
|
2
3
|
import { Track } from './Track';
|
3
|
-
import
|
4
|
-
AudioCaptureOptions,
|
5
|
-
CreateLocalTracksOptions,
|
6
|
-
ScreenShareCaptureOptions,
|
7
|
-
VideoCaptureOptions,
|
4
|
+
import {
|
5
|
+
type AudioCaptureOptions,
|
6
|
+
type CreateLocalTracksOptions,
|
7
|
+
type ScreenShareCaptureOptions,
|
8
|
+
type VideoCaptureOptions,
|
9
|
+
VideoCodec,
|
10
|
+
videoCodecs,
|
8
11
|
} from './options';
|
9
12
|
import type { AudioTrack } from './types';
|
10
13
|
|
@@ -13,9 +16,7 @@ export function mergeDefaultOptions(
|
|
13
16
|
audioDefaults?: AudioCaptureOptions,
|
14
17
|
videoDefaults?: VideoCaptureOptions,
|
15
18
|
): CreateLocalTracksOptions {
|
16
|
-
const opts: CreateLocalTracksOptions = {
|
17
|
-
...options,
|
18
|
-
};
|
19
|
+
const opts: CreateLocalTracksOptions = cloneDeep(options) ?? {};
|
19
20
|
if (opts.audio === true) opts.audio = {};
|
20
21
|
if (opts.video === true) opts.video = {};
|
21
22
|
|
@@ -181,3 +182,11 @@ export function screenCaptureToDisplayMediaStreamOptions(
|
|
181
182
|
systemAudio: options.systemAudio,
|
182
183
|
};
|
183
184
|
}
|
185
|
+
|
186
|
+
export function mimeTypeToVideoCodecString(mimeType: string) {
|
187
|
+
const codec = mimeType.split('/')[1].toLowerCase() as VideoCodec;
|
188
|
+
if (!videoCodecs.includes(codec)) {
|
189
|
+
throw Error(`Video codec not supported: ${codec}`);
|
190
|
+
}
|
191
|
+
return codec;
|
192
|
+
}
|
@@ -0,0 +1,54 @@
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
2
|
+
import { cloneDeep } from './cloneDeep';
|
3
|
+
|
4
|
+
describe('cloneDeep', () => {
|
5
|
+
beforeEach(() => {
|
6
|
+
global.structuredClone = vi.fn((val) => {
|
7
|
+
return JSON.parse(JSON.stringify(val));
|
8
|
+
});
|
9
|
+
});
|
10
|
+
|
11
|
+
afterEach(() => {
|
12
|
+
vi.restoreAllMocks();
|
13
|
+
});
|
14
|
+
|
15
|
+
it('should clone a simple object', () => {
|
16
|
+
const structuredCloneSpy = vi.spyOn(global, 'structuredClone');
|
17
|
+
|
18
|
+
const original = { name: 'John', age: 30 };
|
19
|
+
const cloned = cloneDeep(original);
|
20
|
+
|
21
|
+
expect(cloned).toEqual(original);
|
22
|
+
expect(cloned).not.toBe(original);
|
23
|
+
expect(structuredCloneSpy).toHaveBeenCalledTimes(1);
|
24
|
+
});
|
25
|
+
|
26
|
+
it('should clone an object with nested properties', () => {
|
27
|
+
const structuredCloneSpy = vi.spyOn(global, 'structuredClone');
|
28
|
+
|
29
|
+
const original = { name: 'John', age: 30, children: [{ name: 'Mark', age: 7 }] };
|
30
|
+
const cloned = cloneDeep(original);
|
31
|
+
|
32
|
+
expect(cloned).toEqual(original);
|
33
|
+
expect(cloned).not.toBe(original);
|
34
|
+
expect(cloned?.children).not.toBe(original.children);
|
35
|
+
expect(structuredCloneSpy).toHaveBeenCalledTimes(1);
|
36
|
+
});
|
37
|
+
|
38
|
+
it('should use JSON namespace as a fallback', () => {
|
39
|
+
const structuredCloneSpy = vi.spyOn(global, 'structuredClone');
|
40
|
+
const serializeSpy = vi.spyOn(JSON, 'stringify');
|
41
|
+
const deserializeSpy = vi.spyOn(JSON, 'parse');
|
42
|
+
|
43
|
+
global.structuredClone = undefined as any;
|
44
|
+
|
45
|
+
const original = { name: 'John', age: 30 };
|
46
|
+
const cloned = cloneDeep(original);
|
47
|
+
|
48
|
+
expect(cloned).toEqual(original);
|
49
|
+
expect(cloned).not.toBe(original);
|
50
|
+
expect(structuredCloneSpy).not.toHaveBeenCalled();
|
51
|
+
expect(serializeSpy).toHaveBeenCalledTimes(1);
|
52
|
+
expect(deserializeSpy).toHaveBeenCalledTimes(1);
|
53
|
+
});
|
54
|
+
});
|