Skip to content

Commit 889ad25

Browse files
authored
Fix dynacast (#1033)
- For SVC codecs (VP9/AV1), keep all quality layers enabled and let the SFU handle spatial layer selection, matching JS SDK behavior - Pass per-codec `isSVC` through the layer publishing call chain instead of using the track's primary codec, fixing multi-codec scenarios - Set `track.codec` unconditionally (was gated by Firefox check) - Rename `updatePublishingLayers` → `setPublishingLayers` to match JS SDK
1 parent 867e706 commit 889ad25

4 files changed

Lines changed: 63 additions & 86 deletions

File tree

.changes/fix-svc-dynacast

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
patch type="fixed" "Fix VP9/SVC dynacast layer handling"

lib/src/core/room.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import '../types/data_stream.dart';
5151
import '../types/other.dart';
5252
import '../types/rpc.dart';
5353
import '../types/transcription_segment.dart';
54-
import '../utils.dart' show unpackStreamId;
54+
import '../utils.dart' show isSVCCodec, unpackStreamId;
5555
import 'engine.dart';
5656
import 'participant_collection.dart';
5757
import 'pending_track_queue.dart';
@@ -363,7 +363,8 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
363363
}
364364
} else if (event.subscribedQualities.isNotEmpty) {
365365
final videoTrack = publication.track as LocalVideoTrack;
366-
await videoTrack.updatePublishingLayers(videoTrack, event.subscribedQualities);
366+
await videoTrack.setPublishingLayers(videoTrack, event.subscribedQualities,
367+
isSVC: isSVCCodec(videoTrack.codec ?? ''));
367368
}
368369
})
369370
..on<SignalSubscriptionPermissionUpdateEvent>((event) async {

lib/src/participant/local.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,13 +376,13 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
376376
Future<lk_models.TrackInfo> negotiate() async {
377377
track.transceiver = await room.engine.createTransceiverRTCRtpSender(track, publishOptions!, encodings);
378378

379+
track.codec = publishOptions.videoCodec;
379380
if (lkBrowser() != BrowserType.firefox) {
380381
await room.engine.setPreferredCodec(
381382
track.transceiver!,
382383
'video',
383384
publishOptions.videoCodec,
384385
);
385-
track.codec = publishOptions.videoCodec;
386386
}
387387

388388
if ([TrackSource.camera, TrackSource.screenShareVideo].contains(track.source)) {
@@ -477,13 +477,13 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
477477
init: transceiverInit,
478478
);
479479

480+
track.codec = publishOptions.videoCodec;
480481
if (lkBrowser() != BrowserType.firefox) {
481482
await room.engine.setPreferredCodec(
482483
track.transceiver!,
483484
'video',
484485
publishOptions.videoCodec,
485486
);
486-
track.codec = publishOptions.videoCodec;
487487
}
488488

489489
if ([TrackSource.camera, TrackSource.screenShareVideo].contains(track.source)) {

lib/src/track/local/video.dart

Lines changed: 57 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import '../../proto/livekit_rtc.pb.dart' as lk_rtc;
2828
import '../../stats/stats.dart';
2929
import '../../support/platform.dart';
3030
import '../../types/other.dart';
31+
import '../../utils.dart' show isSVCCodec;
3132
import '../options.dart';
3233
import 'audio.dart';
3334
import 'local.dart';
@@ -328,7 +329,7 @@ extension LocalVideoTrackExt on LocalVideoTrack {
328329

329330
// only enable simulcast codec for preference codec setted
330331
if (codec == null && codecs.isNotEmpty) {
331-
await updatePublishingLayers(track, codecs[0].qualities);
332+
await setPublishingLayers(track, codecs[0].qualities, isSVC: isSVCCodec(codecs[0].codec));
332333
return [];
333334
}
334335

@@ -338,7 +339,7 @@ extension LocalVideoTrackExt on LocalVideoTrack {
338339

339340
for (var codec in codecs) {
340341
if (this.codec?.toLowerCase() == codec.codec.toLowerCase()) {
341-
await updatePublishingLayers(track, codec.qualities);
342+
await setPublishingLayers(track, codec.qualities, isSVC: isSVCCodec(codec.codec));
342343
} else {
343344
final simulcastCodecInfo = simulcastCodecs[codec.codec];
344345
logger.fine('setPublishingCodecs $codecs');
@@ -355,6 +356,7 @@ extension LocalVideoTrackExt on LocalVideoTrack {
355356
simulcastCodecInfo.sender!,
356357
simulcastCodecInfo.encodings!,
357358
codec.qualities,
359+
isSVC: isSVCCodec(codec.codec),
358360
);
359361
}
360362
}
@@ -363,10 +365,11 @@ extension LocalVideoTrackExt on LocalVideoTrack {
363365
}
364366

365367
@internal
366-
Future<void> updatePublishingLayers(
368+
Future<void> setPublishingLayers(
367369
LocalTrack? track,
368-
List<lk_rtc.SubscribedQuality> layers,
369-
) async {
370+
List<lk_rtc.SubscribedQuality> layers, {
371+
bool isSVC = false,
372+
}) async {
370373
logger.fine('Update publishing layers: $layers');
371374

372375
if (track?.sender == null) {
@@ -386,7 +389,7 @@ extension LocalVideoTrackExt on LocalVideoTrack {
386389
return;
387390
}
388391

389-
return setPublishingLayersForSender(track!.sender!, encodings, layers);
392+
return setPublishingLayersForSender(track!.sender!, encodings, layers, isSVC: isSVC);
390393
}
391394

392395
lk_models.VideoQuality _videoQualityForRid(String rid) {
@@ -405,96 +408,68 @@ extension LocalVideoTrackExt on LocalVideoTrack {
405408
Future<void> setPublishingLayersForSender(
406409
rtc.RTCRtpSender sender,
407410
List<rtc.RTCRtpEncoding> encodings,
408-
List<lk_rtc.SubscribedQuality> layers,
409-
) async {
411+
List<lk_rtc.SubscribedQuality> layers, {
412+
bool isSVC = false,
413+
}) async {
410414
logger.fine('Update publishing layers: $layers');
411415

412416
final params = sender.parameters;
413417

414418
var hasChanged = false;
415419

416-
/* disable closable spatial layer as it has video blur / frozen issue with current server / client
417-
1. chrome 113: when switching to up layer with scalability Mode change, it will generate a
418-
low resolution frame and recover very quickly, but noticable
419-
2. livekit sfu: additional pli request cause video frozen for a few frames, also noticable */
420-
421-
/* @ts-ignore */
422-
if (encodings[0].scalabilityMode != null) {
423-
// svc dynacast encodings
424-
final encoding = encodings[0];
425-
/* @ts-ignore */
426-
// const mode = new ScalabilityMode(encoding.scalabilityMode);
427-
var maxQuality = lk_models.VideoQuality.OFF;
428-
for (var q in layers) {
429-
if (q.enabled && (maxQuality == lk_models.VideoQuality.OFF || q.quality.value > maxQuality.value)) {
430-
maxQuality = q.quality;
420+
// NOTE: closable spatial layer is disabled due to video blur / frozen issues
421+
// with Chrome 113+ and LiveKit SFU PLI handling. See JS SDK LocalVideoTrack.ts:529-568.
422+
// For SVC codecs, all layers are kept enabled and the SFU handles layer selection.
423+
if (isSVC) {
424+
final hasEnabledEncoding = layers.any((q) => q.enabled);
425+
if (hasEnabledEncoding) {
426+
for (var q in layers) {
427+
q.enabled = true;
431428
}
432429
}
433-
434-
if (maxQuality == lk_models.VideoQuality.OFF) {
435-
if (encoding.active) {
436-
encoding.active = false;
437-
hasChanged = true;
438-
}
439-
} else if (!encoding.active /* || mode.spatial !== maxQuality + 1*/) {
440-
hasChanged = true;
441-
encoding.active = true;
442-
/*
443-
var originalMode = new ScalabilityMode(senderEncodings[0].scalabilityMode)
444-
mode.spatial = maxQuality + 1;
445-
mode.suffix = originalMode.suffix;
446-
if (mode.spatial === 1) {
447-
// no suffix for L1Tx
448-
mode.suffix = undefined;
449-
}
450-
encoding.scalabilityMode = mode.toString();
451-
encoding.scaleResolutionDownBy = 2 ** (2 - maxQuality);
452-
*/
430+
}
431+
// simulcast dynacast encodings
432+
var idx = 0;
433+
for (var encoding in encodings) {
434+
var rid = encoding.rid ?? '';
435+
if (rid == '') {
436+
rid = 'q';
453437
}
454-
} else {
455-
// simulcast dynacast encodings
456-
var idx = 0;
457-
for (var encoding in encodings) {
458-
var rid = encoding.rid ?? '';
459-
if (rid == '') {
460-
rid = 'q';
461-
}
462-
final quality = _videoQualityForRid(rid);
463-
final subscribedQuality = layers.firstWhereOrNull(
464-
(q) => q.quality == quality,
438+
final quality = _videoQualityForRid(rid);
439+
final subscribedQuality = layers.firstWhereOrNull(
440+
(q) => q.quality == quality,
441+
);
442+
if (subscribedQuality == null) {
443+
continue;
444+
}
445+
if (encoding.active != subscribedQuality.enabled) {
446+
hasChanged = true;
447+
encoding.active = subscribedQuality.enabled;
448+
logger.fine(
449+
'setting layer ${subscribedQuality.quality} to ${encoding.active ? 'enabled' : 'disabled'}',
465450
);
466-
if (subscribedQuality == null) {
467-
continue;
468-
}
469-
if (encoding.active != subscribedQuality.enabled) {
470-
hasChanged = true;
471-
encoding.active = subscribedQuality.enabled;
472-
logger.fine(
473-
'setting layer ${subscribedQuality.quality} to ${encoding.active ? 'enabled' : 'disabled'}',
474-
);
475451

476-
// FireFox does not support setting encoding.active to false, so we
477-
// have a workaround of lowering its bitrate and resolution to the min.
478-
if (kIsWeb && lkBrowser() == BrowserType.firefox) {
479-
if (subscribedQuality.enabled) {
480-
final encodingBackup = encodingBackups[(sender.senderId, idx)] ?? encoding;
481-
encoding.scaleResolutionDownBy = encodingBackup.scaleResolutionDownBy;
482-
encoding.maxBitrate = encodingBackup.maxBitrate;
483-
encoding.maxFramerate = encodingBackup.maxFramerate;
484-
} else {
485-
encodingBackups[(sender.senderId, idx)] = rtc.RTCRtpEncoding(
486-
scaleResolutionDownBy: encoding.scaleResolutionDownBy,
487-
maxBitrate: encoding.maxBitrate,
488-
maxFramerate: encoding.maxFramerate,
489-
);
490-
encoding.scaleResolutionDownBy = 4;
491-
encoding.maxBitrate = 10;
492-
encoding.maxFramerate = 2;
493-
}
452+
// FireFox does not support setting encoding.active to false, so we
453+
// have a workaround of lowering its bitrate and resolution to the min.
454+
if (kIsWeb && lkBrowser() == BrowserType.firefox) {
455+
if (subscribedQuality.enabled) {
456+
final encodingBackup = encodingBackups[(sender.senderId, idx)] ?? encoding;
457+
encoding.scaleResolutionDownBy = encodingBackup.scaleResolutionDownBy;
458+
encoding.maxBitrate = encodingBackup.maxBitrate;
459+
encoding.maxFramerate = encodingBackup.maxFramerate;
460+
} else {
461+
encodingBackups[(sender.senderId, idx)] = rtc.RTCRtpEncoding(
462+
scaleResolutionDownBy: encoding.scaleResolutionDownBy,
463+
maxBitrate: encoding.maxBitrate,
464+
maxFramerate: encoding.maxFramerate,
465+
);
466+
encoding.scaleResolutionDownBy = 4;
467+
encoding.maxBitrate = 10;
468+
encoding.maxFramerate = 2;
494469
}
495470
}
496-
idx++;
497471
}
472+
idx++;
498473
}
499474

500475
if (hasChanged) {

0 commit comments

Comments
 (0)