livekit-client 1.9.0 → 1.9.2
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +6 -5
- package/dist/livekit-client.esm.mjs +200 -58
- 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/api/SignalClient.d.ts +1 -0
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +1 -0
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +4 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +7 -1
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +3 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/src/api/SignalClient.d.ts +1 -0
- package/dist/ts4.2/src/room/RTCEngine.d.ts +1 -0
- package/dist/ts4.2/src/room/Room.d.ts +4 -0
- package/dist/ts4.2/src/room/track/options.d.ts +7 -0
- package/dist/ts4.2/src/room/utils.d.ts +3 -0
- package/package.json +1 -1
- package/src/api/SignalClient.ts +15 -11
- package/src/room/PCTransport.ts +40 -1
- package/src/room/RTCEngine.ts +34 -1
- package/src/room/Room.ts +99 -42
- package/src/room/participant/LocalParticipant.ts +35 -4
- package/src/room/participant/publishUtils.ts +13 -5
- package/src/room/track/options.ts +14 -1
- package/src/room/utils.ts +18 -8
package/src/room/Room.ts
CHANGED
@@ -45,6 +45,7 @@ import LocalParticipant from './participant/LocalParticipant';
|
|
45
45
|
import type Participant from './participant/Participant';
|
46
46
|
import type { ConnectionQuality } from './participant/Participant';
|
47
47
|
import RemoteParticipant from './participant/RemoteParticipant';
|
48
|
+
import CriticalTimers from './timers';
|
48
49
|
import LocalAudioTrack from './track/LocalAudioTrack';
|
49
50
|
import LocalTrackPublication from './track/LocalTrackPublication';
|
50
51
|
import LocalVideoTrack from './track/LocalVideoTrack';
|
@@ -73,6 +74,8 @@ export enum ConnectionState {
|
|
73
74
|
Reconnecting = 'reconnecting',
|
74
75
|
}
|
75
76
|
|
77
|
+
const connectionReconcileFrequency = 2 * 1000;
|
78
|
+
|
76
79
|
/** @deprecated RoomState has been renamed to [[ConnectionState]] */
|
77
80
|
export const RoomState = ConnectionState;
|
78
81
|
|
@@ -124,6 +127,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
124
127
|
|
125
128
|
private disconnectLock: Mutex;
|
126
129
|
|
130
|
+
private cachedParticipantSids: Array<string>;
|
131
|
+
|
132
|
+
private connectionReconcileInterval?: ReturnType<typeof setInterval>;
|
133
|
+
|
127
134
|
/**
|
128
135
|
* Creates a new Room, the primary construct for a LiveKit session.
|
129
136
|
* @param options
|
@@ -132,6 +139,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
132
139
|
super();
|
133
140
|
this.setMaxListeners(100);
|
134
141
|
this.participants = new Map();
|
142
|
+
this.cachedParticipantSids = [];
|
135
143
|
this.identityToSid = new Map();
|
136
144
|
this.options = { ...roomOptionDefaults, ...options };
|
137
145
|
|
@@ -186,7 +194,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
186
194
|
}
|
187
195
|
|
188
196
|
private maybeCreateEngine() {
|
189
|
-
if (this.engine) {
|
197
|
+
if (this.engine && !this.engine.isClosed) {
|
190
198
|
return;
|
191
199
|
}
|
192
200
|
|
@@ -212,14 +220,24 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
212
220
|
.on(EngineEvent.ActiveSpeakersUpdate, this.handleActiveSpeakersUpdate)
|
213
221
|
.on(EngineEvent.DataPacketReceived, this.handleDataPacket)
|
214
222
|
.on(EngineEvent.Resuming, () => {
|
223
|
+
this.clearConnectionReconcile();
|
215
224
|
if (this.setAndEmitConnectionState(ConnectionState.Reconnecting)) {
|
216
225
|
this.emit(RoomEvent.Reconnecting);
|
217
226
|
}
|
227
|
+
this.cachedParticipantSids = Array.from(this.participants.keys());
|
218
228
|
})
|
219
229
|
.on(EngineEvent.Resumed, () => {
|
220
230
|
this.setAndEmitConnectionState(ConnectionState.Connected);
|
221
231
|
this.emit(RoomEvent.Reconnected);
|
232
|
+
this.registerConnectionReconcile();
|
222
233
|
this.updateSubscriptions();
|
234
|
+
|
235
|
+
// once reconnected, figure out if any participants connected during reconnect and emit events for it
|
236
|
+
const diffParticipants = Array.from(this.participants.values()).filter(
|
237
|
+
(p) => !this.cachedParticipantSids.includes(p.sid),
|
238
|
+
);
|
239
|
+
diffParticipants.forEach((p) => this.emit(RoomEvent.ParticipantConnected, p));
|
240
|
+
this.cachedParticipantSids = [];
|
223
241
|
})
|
224
242
|
.on(EngineEvent.SignalResumed, () => {
|
225
243
|
if (this.state === ConnectionState.Reconnecting) {
|
@@ -477,6 +495,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
477
495
|
}
|
478
496
|
this.setAndEmitConnectionState(ConnectionState.Connected);
|
479
497
|
this.emit(RoomEvent.Connected);
|
498
|
+
this.registerConnectionReconcile();
|
480
499
|
};
|
481
500
|
|
482
501
|
/**
|
@@ -818,6 +837,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
818
837
|
}
|
819
838
|
|
820
839
|
private handleRestarting = () => {
|
840
|
+
this.clearConnectionReconcile();
|
821
841
|
// also unwind existing participants & existing subscriptions
|
822
842
|
for (const p of this.participants.values()) {
|
823
843
|
this.handleParticipantDisconnected(p.sid, p);
|
@@ -833,6 +853,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
833
853
|
region: joinResponse.serverRegion,
|
834
854
|
});
|
835
855
|
|
856
|
+
this.cachedParticipantSids = [];
|
836
857
|
this.applyJoinResponse(joinResponse);
|
837
858
|
|
838
859
|
try {
|
@@ -882,6 +903,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
882
903
|
}
|
883
904
|
this.setAndEmitConnectionState(ConnectionState.Connected);
|
884
905
|
this.emit(RoomEvent.Reconnected);
|
906
|
+
this.registerConnectionReconcile();
|
885
907
|
|
886
908
|
// emit participant connected events after connection has been re-established
|
887
909
|
this.participants.forEach((participant) => {
|
@@ -890,57 +912,61 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
890
912
|
};
|
891
913
|
|
892
914
|
private handleDisconnect(shouldStopTracks = true, reason?: DisconnectReason) {
|
915
|
+
this.clearConnectionReconcile();
|
893
916
|
if (this.state === ConnectionState.Disconnected) {
|
894
917
|
return;
|
895
918
|
}
|
896
919
|
|
897
|
-
|
898
|
-
|
899
|
-
p.
|
920
|
+
try {
|
921
|
+
this.participants.forEach((p) => {
|
922
|
+
p.tracks.forEach((pub) => {
|
923
|
+
p.unpublishTrack(pub.trackSid);
|
924
|
+
});
|
900
925
|
});
|
901
|
-
});
|
902
926
|
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
|
927
|
+
this.localParticipant.tracks.forEach((pub) => {
|
928
|
+
if (pub.track) {
|
929
|
+
this.localParticipant.unpublishTrack(pub.track, shouldStopTracks);
|
930
|
+
}
|
931
|
+
if (shouldStopTracks) {
|
932
|
+
pub.track?.detach();
|
933
|
+
pub.track?.stop();
|
934
|
+
}
|
935
|
+
});
|
912
936
|
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
937
|
+
this.localParticipant
|
938
|
+
.off(ParticipantEvent.ParticipantMetadataChanged, this.onLocalParticipantMetadataChanged)
|
939
|
+
.off(ParticipantEvent.ParticipantNameChanged, this.onLocalParticipantNameChanged)
|
940
|
+
.off(ParticipantEvent.TrackMuted, this.onLocalTrackMuted)
|
941
|
+
.off(ParticipantEvent.TrackUnmuted, this.onLocalTrackUnmuted)
|
942
|
+
.off(ParticipantEvent.LocalTrackPublished, this.onLocalTrackPublished)
|
943
|
+
.off(ParticipantEvent.LocalTrackUnpublished, this.onLocalTrackUnpublished)
|
944
|
+
.off(ParticipantEvent.ConnectionQualityChanged, this.onLocalConnectionQualityChanged)
|
945
|
+
.off(ParticipantEvent.MediaDevicesError, this.onMediaDevicesError)
|
946
|
+
.off(
|
947
|
+
ParticipantEvent.ParticipantPermissionsChanged,
|
948
|
+
this.onLocalParticipantPermissionsChanged,
|
949
|
+
);
|
926
950
|
|
927
|
-
|
928
|
-
|
929
|
-
|
951
|
+
this.localParticipant.tracks.clear();
|
952
|
+
this.localParticipant.videoTracks.clear();
|
953
|
+
this.localParticipant.audioTracks.clear();
|
930
954
|
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
955
|
+
this.participants.clear();
|
956
|
+
this.activeSpeakers = [];
|
957
|
+
if (this.audioContext && typeof this.options.expWebAudioMix === 'boolean') {
|
958
|
+
this.audioContext.close();
|
959
|
+
this.audioContext = undefined;
|
960
|
+
}
|
961
|
+
if (isWeb()) {
|
962
|
+
window.removeEventListener('beforeunload', this.onPageLeave);
|
963
|
+
window.removeEventListener('pagehide', this.onPageLeave);
|
964
|
+
navigator.mediaDevices?.removeEventListener('devicechange', this.handleDeviceChange);
|
965
|
+
}
|
966
|
+
} finally {
|
967
|
+
this.setAndEmitConnectionState(ConnectionState.Disconnected);
|
968
|
+
this.emit(RoomEvent.Disconnected, reason);
|
941
969
|
}
|
942
|
-
this.setAndEmitConnectionState(ConnectionState.Disconnected);
|
943
|
-
this.emit(RoomEvent.Disconnected, reason);
|
944
970
|
}
|
945
971
|
|
946
972
|
private handleParticipantUpdates = (participantInfos: ParticipantInfo[]) => {
|
@@ -1329,6 +1355,37 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1329
1355
|
}
|
1330
1356
|
}
|
1331
1357
|
|
1358
|
+
private registerConnectionReconcile() {
|
1359
|
+
this.clearConnectionReconcile();
|
1360
|
+
let consecutiveFailures = 0;
|
1361
|
+
this.connectionReconcileInterval = CriticalTimers.setInterval(() => {
|
1362
|
+
if (
|
1363
|
+
// ensure we didn't tear it down
|
1364
|
+
!this.engine ||
|
1365
|
+
// engine detected close, but Room missed it
|
1366
|
+
this.engine.isClosed ||
|
1367
|
+
// transports failed without notifying engine
|
1368
|
+
!this.engine.verifyTransport()
|
1369
|
+
) {
|
1370
|
+
consecutiveFailures++;
|
1371
|
+
log.warn('detected connection state mismatch', { numFailures: consecutiveFailures });
|
1372
|
+
if (consecutiveFailures >= 3)
|
1373
|
+
this.handleDisconnect(
|
1374
|
+
this.options.stopLocalTrackOnUnpublish,
|
1375
|
+
DisconnectReason.UNKNOWN_REASON,
|
1376
|
+
);
|
1377
|
+
} else {
|
1378
|
+
consecutiveFailures = 0;
|
1379
|
+
}
|
1380
|
+
}, connectionReconcileFrequency);
|
1381
|
+
}
|
1382
|
+
|
1383
|
+
private clearConnectionReconcile() {
|
1384
|
+
if (this.connectionReconcileInterval) {
|
1385
|
+
CriticalTimers.clearInterval(this.connectionReconcileInterval);
|
1386
|
+
}
|
1387
|
+
}
|
1388
|
+
|
1332
1389
|
private setAndEmitConnectionState(state: ConnectionState): boolean {
|
1333
1390
|
if (state === this.state) {
|
1334
1391
|
// unchanged
|
@@ -27,10 +27,11 @@ import {
|
|
27
27
|
TrackPublishOptions,
|
28
28
|
VideoCaptureOptions,
|
29
29
|
isBackupCodec,
|
30
|
+
isCodecEqual,
|
30
31
|
} from '../track/options';
|
31
32
|
import { constraintsForOptions, mergeDefaultOptions } from '../track/utils';
|
32
33
|
import type { DataPublishOptions } from '../types';
|
33
|
-
import { Future, isFireFox, isSafari, isWeb, supportsAV1 } from '../utils';
|
34
|
+
import { Future, isFireFox, isSVCCodec, isSafari, isWeb, supportsAV1, supportsVP9 } from '../utils';
|
34
35
|
import Participant from './Participant';
|
35
36
|
import { ParticipantTrackPermission, trackPermissionToProto } from './ParticipantTrackPermission';
|
36
37
|
import RemoteParticipant from './RemoteParticipant';
|
@@ -570,10 +571,13 @@ export default class LocalParticipant extends Participant {
|
|
570
571
|
opts.simulcast = false;
|
571
572
|
}
|
572
573
|
|
573
|
-
// require full AV1 SVC support prior to using it
|
574
|
+
// require full AV1/VP9 SVC support prior to using it
|
574
575
|
if (opts.videoCodec === 'av1' && !supportsAV1()) {
|
575
576
|
opts.videoCodec = undefined;
|
576
577
|
}
|
578
|
+
if (opts.videoCodec === 'vp9' && !supportsVP9()) {
|
579
|
+
opts.videoCodec = undefined;
|
580
|
+
}
|
577
581
|
|
578
582
|
// handle track actions
|
579
583
|
track.on(TrackEvent.Muted, this.onTrackMuted);
|
@@ -614,7 +618,7 @@ export default class LocalParticipant extends Participant {
|
|
614
618
|
req.height = dims.height;
|
615
619
|
// for svc codecs, disable simulcast and use vp8 for backup codec
|
616
620
|
if (track instanceof LocalVideoTrack) {
|
617
|
-
if (opts
|
621
|
+
if (isSVCCodec(opts.videoCodec)) {
|
618
622
|
// set scalabilityMode to 'L3T3' by default
|
619
623
|
opts.scalabilityMode = opts.scalabilityMode ?? 'L3T3';
|
620
624
|
}
|
@@ -660,6 +664,33 @@ export default class LocalParticipant extends Participant {
|
|
660
664
|
}
|
661
665
|
|
662
666
|
const ti = await this.engine.addTrack(req);
|
667
|
+
let primaryCodecSupported = false;
|
668
|
+
let backupCodecSupported = false;
|
669
|
+
ti.codecs.forEach((c) => {
|
670
|
+
if (isCodecEqual(c.mimeType, opts.videoCodec)) {
|
671
|
+
primaryCodecSupported = true;
|
672
|
+
} else if (opts.backupCodec && isCodecEqual(c.mimeType, opts.backupCodec.codec)) {
|
673
|
+
backupCodecSupported = true;
|
674
|
+
}
|
675
|
+
});
|
676
|
+
|
677
|
+
if (req.simulcastCodecs.length > 0) {
|
678
|
+
if (!primaryCodecSupported && !backupCodecSupported) {
|
679
|
+
throw Error('cannot publish track, codec not supported by server');
|
680
|
+
}
|
681
|
+
|
682
|
+
if (!primaryCodecSupported && opts.backupCodec) {
|
683
|
+
const backupCodec = opts.backupCodec;
|
684
|
+
opts = { ...opts };
|
685
|
+
log.debug(
|
686
|
+
`primary codec ${opts.videoCodec} not supported, fallback to ${backupCodec.codec}`,
|
687
|
+
);
|
688
|
+
opts.videoCodec = backupCodec.codec;
|
689
|
+
opts.videoEncoding = backupCodec.encoding;
|
690
|
+
encodings = simEncodings;
|
691
|
+
}
|
692
|
+
}
|
693
|
+
|
663
694
|
const publication = new LocalTrackPublication(track.kind, ti, track);
|
664
695
|
// save options for when it needs to be republished again
|
665
696
|
publication.options = opts;
|
@@ -673,7 +704,7 @@ export default class LocalParticipant extends Participant {
|
|
673
704
|
// store RTPSender
|
674
705
|
track.sender = await this.engine.createSender(track, opts, encodings);
|
675
706
|
|
676
|
-
if (track.codec
|
707
|
+
if (track.codec && isSVCCodec(track.codec) && encodings && encodings[0]?.maxBitrate) {
|
677
708
|
this.engine.publisher.setTrackCodecBitrate(
|
678
709
|
req.cid,
|
679
710
|
track.codec,
|
@@ -13,6 +13,7 @@ import {
|
|
13
13
|
VideoPresets,
|
14
14
|
VideoPresets43,
|
15
15
|
} from '../track/options';
|
16
|
+
import { isSVCCodec } from '../utils';
|
16
17
|
|
17
18
|
/** @internal */
|
18
19
|
export function mediaTrackToLocalTrack(
|
@@ -121,7 +122,7 @@ export function computeVideoEncodings(
|
|
121
122
|
videoEncoding.maxFramerate,
|
122
123
|
);
|
123
124
|
|
124
|
-
if (scalabilityMode && videoCodec
|
125
|
+
if (scalabilityMode && isSVCCodec(videoCodec)) {
|
125
126
|
log.debug(`using svc with scalabilityMode ${scalabilityMode}`);
|
126
127
|
|
127
128
|
const encodings: RTCRtpEncodingParameters[] = [];
|
@@ -248,8 +249,13 @@ export function determineAppropriateEncoding(
|
|
248
249
|
if (codec) {
|
249
250
|
switch (codec) {
|
250
251
|
case 'av1':
|
252
|
+
encoding = { ...encoding };
|
251
253
|
encoding.maxBitrate = encoding.maxBitrate * 0.7;
|
252
254
|
break;
|
255
|
+
case 'vp9':
|
256
|
+
encoding = { ...encoding };
|
257
|
+
encoding.maxBitrate = encoding.maxBitrate * 0.85;
|
258
|
+
break;
|
253
259
|
default:
|
254
260
|
break;
|
255
261
|
}
|
@@ -303,13 +309,15 @@ function encodingsFromPresets(
|
|
303
309
|
}
|
304
310
|
const size = Math.min(width, height);
|
305
311
|
const rid = videoRids[idx];
|
306
|
-
|
312
|
+
const encoding: RTCRtpEncodingParameters = {
|
307
313
|
rid,
|
308
314
|
scaleResolutionDownBy: Math.max(1, size / Math.min(preset.width, preset.height)),
|
309
315
|
maxBitrate: preset.encoding.maxBitrate,
|
310
|
-
|
311
|
-
|
312
|
-
|
316
|
+
};
|
317
|
+
if (preset.encoding.maxFramerate) {
|
318
|
+
encoding.maxFramerate = preset.encoding.maxFramerate;
|
319
|
+
}
|
320
|
+
encodings.push(encoding);
|
313
321
|
});
|
314
322
|
return encodings;
|
315
323
|
}
|
@@ -145,6 +145,12 @@ export interface ScreenShareCaptureOptions {
|
|
145
145
|
|
146
146
|
/** specifies whether the browser should include the system audio among the possible audio sources offered to the user */
|
147
147
|
systemAudio?: 'include' | 'exclude';
|
148
|
+
|
149
|
+
/**
|
150
|
+
* Experimental option to control whether the audio playing in a tab will continue to be played out of a user's
|
151
|
+
* local speakers when the tab is captured.
|
152
|
+
*/
|
153
|
+
suppressLocalAudioPlayback?: boolean;
|
148
154
|
}
|
149
155
|
|
150
156
|
export interface AudioCaptureOptions {
|
@@ -241,7 +247,7 @@ export interface AudioPreset {
|
|
241
247
|
maxBitrate: number;
|
242
248
|
}
|
243
249
|
|
244
|
-
const codecs = ['vp8', 'h264', 'av1'] as const;
|
250
|
+
const codecs = ['vp8', 'h264', 'vp9', 'av1'] as const;
|
245
251
|
const backupCodecs = ['vp8', 'h264'] as const;
|
246
252
|
|
247
253
|
export type VideoCodec = (typeof codecs)[number];
|
@@ -252,6 +258,13 @@ export function isBackupCodec(codec: string): codec is BackupVideoCodec {
|
|
252
258
|
return !!backupCodecs.find((backup) => backup === codec);
|
253
259
|
}
|
254
260
|
|
261
|
+
export function isCodecEqual(c1: string | undefined, c2: string | undefined): boolean {
|
262
|
+
return (
|
263
|
+
c1?.toLowerCase().replace(/audio\/|video\//y, '') ===
|
264
|
+
c2?.toLowerCase().replace(/audio\/|video\//y, '')
|
265
|
+
);
|
266
|
+
}
|
267
|
+
|
255
268
|
/**
|
256
269
|
* scalability modes for svc, only supprot l3t3 now.
|
257
270
|
*/
|
package/src/room/utils.ts
CHANGED
@@ -7,6 +7,8 @@ import { getNewAudioContext } from './track/utils';
|
|
7
7
|
import type { LiveKitReactNativeInfo } from './types';
|
8
8
|
|
9
9
|
const separator = '|';
|
10
|
+
export const ddExtensionURI =
|
11
|
+
'https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension';
|
10
12
|
|
11
13
|
export function unpackStreamId(packed: string): string[] {
|
12
14
|
const parts = packed.split(separator);
|
@@ -41,7 +43,6 @@ export function supportsDynacast() {
|
|
41
43
|
export function supportsAV1(): boolean {
|
42
44
|
const capabilities = RTCRtpReceiver.getCapabilities('video');
|
43
45
|
let hasAV1 = false;
|
44
|
-
let hasDDExt = false;
|
45
46
|
if (capabilities) {
|
46
47
|
for (const codec of capabilities.codecs) {
|
47
48
|
if (codec.mimeType === 'video/AV1') {
|
@@ -49,17 +50,26 @@ export function supportsAV1(): boolean {
|
|
49
50
|
break;
|
50
51
|
}
|
51
52
|
}
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
53
|
+
}
|
54
|
+
return hasAV1;
|
55
|
+
}
|
56
|
+
|
57
|
+
export function supportsVP9(): boolean {
|
58
|
+
const capabilities = RTCRtpReceiver.getCapabilities('video');
|
59
|
+
let hasVP9 = false;
|
60
|
+
if (capabilities) {
|
61
|
+
for (const codec of capabilities.codecs) {
|
62
|
+
if (codec.mimeType === 'video/VP9') {
|
63
|
+
hasVP9 = true;
|
58
64
|
break;
|
59
65
|
}
|
60
66
|
}
|
61
67
|
}
|
62
|
-
return
|
68
|
+
return hasVP9;
|
69
|
+
}
|
70
|
+
|
71
|
+
export function isSVCCodec(codec?: string): boolean {
|
72
|
+
return codec === 'av1' || codec === 'vp9';
|
63
73
|
}
|
64
74
|
|
65
75
|
export function supportsSetSinkId(elm?: HTMLMediaElement): boolean {
|