livekit-client 1.9.0 → 1.9.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.
- 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 {
|