Skip to content

Commit 74a4b58

Browse files
authored
Fix svc mode for chrome v113, change default svc mode to L3T3_KEY (livekit#720)
* Fix svc mode for chrome v113. Fix svc mode for chrome v113. Add closable spatial layer for svc dynacast but disable it as two blur/frozen issues with current server/client: 1. chrome 113: when switching to up layer with scalability Mode change, it will generate a low resolution frame and recover very quickly, but noticable 2. livekit sfu: additional pli request cause video frozen for a few frames, also noticable
1 parent b9cd211 commit 74a4b58

File tree

5 files changed

+145
-48
lines changed

5 files changed

+145
-48
lines changed

.changeset/spicy-cats-shop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'livekit-client': patch
3+
---
4+
5+
Fix svc mode for chrome v113

src/room/participant/LocalParticipant.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -625,8 +625,8 @@ export default class LocalParticipant extends Participant {
625625
// for svc codecs, disable simulcast and use vp8 for backup codec
626626
if (track instanceof LocalVideoTrack) {
627627
if (isSVCCodec(opts.videoCodec)) {
628-
// set scalabilityMode to 'L3T3' by default
629-
opts.scalabilityMode = opts.scalabilityMode ?? 'L3T3';
628+
// set scalabilityMode to 'L3T3_KEY' by default
629+
opts.scalabilityMode = opts.scalabilityMode ?? 'L3T3_KEY';
630630
}
631631

632632
// set up backup
@@ -656,7 +656,7 @@ export default class LocalParticipant extends Participant {
656656
dims.height,
657657
opts,
658658
);
659-
req.layers = videoLayersFromEncodings(req.width, req.height, simEncodings ?? encodings);
659+
req.layers = videoLayersFromEncodings(req.width, req.height, encodings);
660660
} else if (track.kind === Track.Kind.Audio) {
661661
encodings = [
662662
{

src/room/participant/publishUtils.ts

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -128,17 +128,15 @@ export function computeVideoEncodings(
128128
// svc use first encoding as the original, so we sort encoding from high to low
129129
switch (scalabilityMode) {
130130
case 'L3T3':
131-
for (let i = 0; i < 3; i += 1) {
132-
encodings.push({
133-
rid: videoRids[2 - i],
134-
scaleResolutionDownBy: 2 ** i,
135-
maxBitrate: videoEncoding.maxBitrate / 3 ** i,
136-
/* @ts-ignore */
137-
maxFramerate: original.encoding.maxFramerate,
138-
/* @ts-ignore */
139-
scalabilityMode: 'L3T3',
140-
});
141-
}
131+
case 'L3T3_KEY':
132+
encodings.push({
133+
rid: videoRids[2],
134+
maxBitrate: videoEncoding.maxBitrate,
135+
/* @ts-ignore */
136+
maxFramerate: original.encoding.maxFramerate,
137+
/* @ts-ignore */
138+
scalabilityMode: scalabilityMode,
139+
});
142140
log.debug('encodings', encodings);
143141
return encodings;
144142

@@ -368,3 +366,34 @@ export function sortPresets(presets: Array<VideoPreset> | undefined) {
368366
return 0;
369367
});
370368
}
369+
370+
/** @internal */
371+
export class ScalabilityMode {
372+
spatial: number;
373+
374+
temporal: number;
375+
376+
suffix: undefined | 'h' | '_KEY' | '_KEY_SHIFT';
377+
378+
constructor(scalabilityMode: string) {
379+
const results = scalabilityMode.match(/^L(\d)T(\d)(h|_KEY|_KEY_SHIFT){0,1}$/);
380+
if (!results) {
381+
throw new Error('invalid scalability mode');
382+
}
383+
384+
this.spatial = parseInt(results[1]);
385+
this.temporal = parseInt(results[2]);
386+
if (results.length > 3) {
387+
switch (results[3]) {
388+
case 'h':
389+
case '_KEY':
390+
case '_KEY_SHIFT':
391+
this.suffix = results[3];
392+
}
393+
}
394+
}
395+
396+
toString(): string {
397+
return `L${this.spatial}T${this.temporal}${this.suffix ?? ''}`;
398+
}
399+
}

src/room/track/LocalVideoTrack.ts

Lines changed: 96 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { SignalClient } from '../../api/SignalClient';
22
import log from '../../logger';
33
import { VideoLayer, VideoQuality } from '../../proto/livekit_models';
44
import type { SubscribedCodec, SubscribedQuality } from '../../proto/livekit_rtc';
5+
import { ScalabilityMode } from '../participant/publishUtils';
56
import { computeBitrate, monitorFrequency } from '../stats';
67
import type { VideoSenderStats } from '../stats';
78
import { Mutex, isFireFox, isMobile, isWeb } from '../utils';
@@ -349,45 +350,88 @@ async function setPublishingLayersForSender(
349350
}
350351

351352
let hasChanged = false;
352-
encodings.forEach((encoding, idx) => {
353-
let rid = encoding.rid ?? '';
354-
if (rid === '') {
355-
rid = 'q';
356-
}
357-
const quality = videoQualityForRid(rid);
358-
const subscribedQuality = qualities.find((q) => q.quality === quality);
359-
if (!subscribedQuality) {
360-
return;
361-
}
362-
if (encoding.active !== subscribedQuality.enabled) {
353+
354+
/* @ts-ignore */
355+
if (encodings.length === 1 && encodings[0].scalabilityMode) {
356+
// svc dynacast encodings
357+
const encoding = encodings[0];
358+
/* @ts-ignore */
359+
// const mode = new ScalabilityMode(encoding.scalabilityMode);
360+
let maxQuality = VideoQuality.OFF;
361+
qualities.forEach((q) => {
362+
if (q.enabled && (maxQuality === VideoQuality.OFF || q.quality > maxQuality)) {
363+
maxQuality = q.quality;
364+
}
365+
});
366+
367+
if (maxQuality === VideoQuality.OFF) {
368+
if (encoding.active) {
369+
encoding.active = false;
370+
hasChanged = true;
371+
}
372+
} else if (!encoding.active /* || mode.spatial !== maxQuality + 1*/) {
363373
hasChanged = true;
364-
encoding.active = subscribedQuality.enabled;
365-
log.debug(
366-
`setting layer ${subscribedQuality.quality} to ${
367-
encoding.active ? 'enabled' : 'disabled'
368-
}`,
369-
);
370-
371-
// FireFox does not support setting encoding.active to false, so we
372-
// have a workaround of lowering its bitrate and resolution to the min.
373-
if (isFireFox()) {
374-
if (subscribedQuality.enabled) {
375-
encoding.scaleResolutionDownBy = senderEncodings[idx].scaleResolutionDownBy;
376-
encoding.maxBitrate = senderEncodings[idx].maxBitrate;
377-
/* @ts-ignore */
378-
encoding.maxFrameRate = senderEncodings[idx].maxFrameRate;
379-
} else {
380-
encoding.scaleResolutionDownBy = 4;
381-
encoding.maxBitrate = 10;
382-
/* @ts-ignore */
383-
encoding.maxFrameRate = 2;
384-
}
374+
encoding.active = true;
375+
/* disable closable spatial layer as it has video blur/frozen issue with current server/client
376+
1. chrome 113: when switching to up layer with scalability Mode change, it will generate a
377+
low resolution frame and recover very quickly, but noticable
378+
2. livekit sfu: additional pli request cause video frozen for a few frames, also noticable
379+
@ts-ignore
380+
const originalMode = new ScalabilityMode(senderEncodings[0].scalabilityMode)
381+
mode.spatial = maxQuality + 1;
382+
mode.suffix = originalMode.suffix;
383+
if (mode.spatial === 1) {
384+
// no suffix for L1Tx
385+
mode.suffix = undefined;
385386
}
387+
@ts-ignore
388+
encoding.scalabilityMode = mode.toString();
389+
encoding.scaleResolutionDownBy = 2 ** (2 - maxQuality);
390+
*/
386391
}
387-
});
392+
} else {
393+
// simulcast dynacast encodings
394+
encodings.forEach((encoding, idx) => {
395+
let rid = encoding.rid ?? '';
396+
if (rid === '') {
397+
rid = 'q';
398+
}
399+
const quality = videoQualityForRid(rid);
400+
const subscribedQuality = qualities.find((q) => q.quality === quality);
401+
if (!subscribedQuality) {
402+
return;
403+
}
404+
if (encoding.active !== subscribedQuality.enabled) {
405+
hasChanged = true;
406+
encoding.active = subscribedQuality.enabled;
407+
log.debug(
408+
`setting layer ${subscribedQuality.quality} to ${
409+
encoding.active ? 'enabled' : 'disabled'
410+
}`,
411+
);
412+
413+
// FireFox does not support setting encoding.active to false, so we
414+
// have a workaround of lowering its bitrate and resolution to the min.
415+
if (isFireFox()) {
416+
if (subscribedQuality.enabled) {
417+
encoding.scaleResolutionDownBy = senderEncodings[idx].scaleResolutionDownBy;
418+
encoding.maxBitrate = senderEncodings[idx].maxBitrate;
419+
/* @ts-ignore */
420+
encoding.maxFrameRate = senderEncodings[idx].maxFrameRate;
421+
} else {
422+
encoding.scaleResolutionDownBy = 4;
423+
encoding.maxBitrate = 10;
424+
/* @ts-ignore */
425+
encoding.maxFrameRate = 2;
426+
}
427+
}
428+
}
429+
});
430+
}
388431

389432
if (hasChanged) {
390433
params.encodings = encodings;
434+
log.debug(`setting encodings`, params.encodings);
391435
await sender.setParameters(params);
392436
}
393437
} finally {
@@ -425,6 +469,25 @@ export function videoLayersFromEncodings(
425469
},
426470
];
427471
}
472+
473+
/* @ts-ignore */
474+
if (encodings.length === 1 && encodings[0].scalabilityMode) {
475+
// svc layers
476+
/* @ts-ignore */
477+
const sm = new ScalabilityMode(encodings[0].scalabilityMode);
478+
const layers = [];
479+
for (let i = 0; i < sm.spatial; i += 1) {
480+
layers.push({
481+
quality: VideoQuality.HIGH - i,
482+
width: width / 2 ** i,
483+
height: height / 2 ** i,
484+
bitrate: encodings[0].maxBitrate ? encodings[0].maxBitrate / 3 ** i : 0,
485+
ssrc: 0,
486+
});
487+
}
488+
return layers;
489+
}
490+
428491
return encodings.map((encoding) => {
429492
const scale = encoding.scaleResolutionDownBy ?? 1;
430493
let quality = videoQualityForRid(encoding.rid ?? '');

src/room/track/options.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ export function isCodecEqual(c1: string | undefined, c2: string | undefined): bo
284284
/**
285285
* scalability modes for svc, only supprot l3t3 now.
286286
*/
287-
export type ScalabilityMode = 'L3T3';
287+
export type ScalabilityMode = 'L3T3' | 'L3T3_KEY';
288288

289289
export namespace AudioPresets {
290290
export const telephone: AudioPreset = {

0 commit comments

Comments
 (0)