From e6d6a94c89757a7cf6214bdf7e076b04a2895f2a Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sat, 21 Mar 2026 06:31:30 +0900 Subject: [PATCH 1/4] fix1 --- lib/src/track/local/video.dart | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/lib/src/track/local/video.dart b/lib/src/track/local/video.dart index 4ddcc681f..0343db524 100644 --- a/lib/src/track/local/video.dart +++ b/lib/src/track/local/video.dart @@ -28,6 +28,7 @@ import '../../proto/livekit_rtc.pb.dart' as lk_rtc; import '../../stats/stats.dart'; import '../../support/platform.dart'; import '../../types/other.dart'; +import '../../utils.dart' show isSVCCodec; import '../options.dart'; import 'audio.dart'; import 'local.dart'; @@ -417,13 +418,11 @@ extension LocalVideoTrackExt on LocalVideoTrack { 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 */ + const closableSpatial = false; - /* @ts-ignore */ - if (encodings[0].scalabilityMode != null) { + if (closableSpatial && encodings[0].scalabilityMode != null) { // svc dynacast encodings final encoding = encodings[0]; - /* @ts-ignore */ - // const mode = new ScalabilityMode(encoding.scalabilityMode); var maxQuality = lk_models.VideoQuality.OFF; for (var q in layers) { if (q.enabled && (maxQuality == lk_models.VideoQuality.OFF || q.quality.value > maxQuality.value)) { @@ -436,22 +435,21 @@ extension LocalVideoTrackExt on LocalVideoTrack { encoding.active = false; hasChanged = true; } - } else if (!encoding.active /* || mode.spatial !== maxQuality + 1*/) { + } else if (!encoding.active) { hasChanged = true; encoding.active = true; - /* - var originalMode = new ScalabilityMode(senderEncodings[0].scalabilityMode) - mode.spatial = maxQuality + 1; - mode.suffix = originalMode.suffix; - if (mode.spatial === 1) { - // no suffix for L1Tx - mode.suffix = undefined; - } - encoding.scalabilityMode = mode.toString(); - encoding.scaleResolutionDownBy = 2 ** (2 - maxQuality); - */ } } else { + // For SVC codecs (VP9/AV1), all layers must be enabled together since + // the SFU handles layer selection. Only allow disabling the entire track. + if (isSVCCodec(codec ?? '')) { + final hasEnabledEncoding = layers.any((q) => q.enabled); + if (hasEnabledEncoding) { + for (var q in layers) { + q.enabled = true; + } + } + } // simulcast dynacast encodings var idx = 0; for (var encoding in encodings) { From 6dfdd0b85d1260534b629aa82890cf8a9712f1d0 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sun, 22 Mar 2026 12:17:15 +0900 Subject: [PATCH 2/4] fixes --- lib/src/core/room.dart | 5 +- lib/src/track/local/video.dart | 133 ++++++++++++++------------------- 2 files changed, 58 insertions(+), 80 deletions(-) diff --git a/lib/src/core/room.dart b/lib/src/core/room.dart index a34043056..e68c9c062 100644 --- a/lib/src/core/room.dart +++ b/lib/src/core/room.dart @@ -51,7 +51,7 @@ import '../types/data_stream.dart'; import '../types/other.dart'; import '../types/rpc.dart'; import '../types/transcription_segment.dart'; -import '../utils.dart' show unpackStreamId; +import '../utils.dart' show isSVCCodec, unpackStreamId; import 'engine.dart'; import 'participant_collection.dart'; import 'pending_track_queue.dart'; @@ -363,7 +363,8 @@ class Room extends DisposableChangeNotifier with EventsEmittable { } } else if (event.subscribedQualities.isNotEmpty) { final videoTrack = publication.track as LocalVideoTrack; - await videoTrack.updatePublishingLayers(videoTrack, event.subscribedQualities); + await videoTrack.setPublishingLayers(videoTrack, event.subscribedQualities, + isSVC: isSVCCodec(videoTrack.codec ?? '')); } }) ..on((event) async { diff --git a/lib/src/track/local/video.dart b/lib/src/track/local/video.dart index 0343db524..47030edb6 100644 --- a/lib/src/track/local/video.dart +++ b/lib/src/track/local/video.dart @@ -329,7 +329,7 @@ extension LocalVideoTrackExt on LocalVideoTrack { // only enable simulcast codec for preference codec setted if (codec == null && codecs.isNotEmpty) { - await updatePublishingLayers(track, codecs[0].qualities); + await setPublishingLayers(track, codecs[0].qualities, isSVC: isSVCCodec(codecs[0].codec)); return []; } @@ -339,7 +339,7 @@ extension LocalVideoTrackExt on LocalVideoTrack { for (var codec in codecs) { if (this.codec?.toLowerCase() == codec.codec.toLowerCase()) { - await updatePublishingLayers(track, codec.qualities); + await setPublishingLayers(track, codec.qualities, isSVC: isSVCCodec(codec.codec)); } else { final simulcastCodecInfo = simulcastCodecs[codec.codec]; logger.fine('setPublishingCodecs $codecs'); @@ -356,6 +356,7 @@ extension LocalVideoTrackExt on LocalVideoTrack { simulcastCodecInfo.sender!, simulcastCodecInfo.encodings!, codec.qualities, + isSVC: isSVCCodec(codec.codec), ); } } @@ -364,10 +365,11 @@ extension LocalVideoTrackExt on LocalVideoTrack { } @internal - Future updatePublishingLayers( + Future setPublishingLayers( LocalTrack? track, - List layers, - ) async { + List layers, { + bool isSVC = false, + }) async { logger.fine('Update publishing layers: $layers'); if (track?.sender == null) { @@ -387,7 +389,7 @@ extension LocalVideoTrackExt on LocalVideoTrack { return; } - return setPublishingLayersForSender(track!.sender!, encodings, layers); + return setPublishingLayersForSender(track!.sender!, encodings, layers, isSVC: isSVC); } lk_models.VideoQuality _videoQualityForRid(String rid) { @@ -406,93 +408,68 @@ extension LocalVideoTrackExt on LocalVideoTrack { Future setPublishingLayersForSender( rtc.RTCRtpSender sender, List encodings, - List layers, - ) async { + List layers, { + bool isSVC = false, + }) async { logger.fine('Update publishing layers: $layers'); final params = sender.parameters; var hasChanged = false; - /* disable closable spatial layer as it has video blur / frozen issue 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 */ - const closableSpatial = false; - - if (closableSpatial && encodings[0].scalabilityMode != null) { - // svc dynacast encodings - final encoding = encodings[0]; - var maxQuality = lk_models.VideoQuality.OFF; - for (var q in layers) { - if (q.enabled && (maxQuality == lk_models.VideoQuality.OFF || q.quality.value > maxQuality.value)) { - maxQuality = q.quality; + // NOTE: closable spatial layer is disabled due to video blur / frozen issues + // with Chrome 113+ and LiveKit SFU PLI handling. See JS SDK LocalVideoTrack.ts:529-568. + // For SVC codecs, all layers are kept enabled and the SFU handles layer selection. + if (isSVC) { + final hasEnabledEncoding = layers.any((q) => q.enabled); + if (hasEnabledEncoding) { + for (var q in layers) { + q.enabled = true; } } - - if (maxQuality == lk_models.VideoQuality.OFF) { - if (encoding.active) { - encoding.active = false; - hasChanged = true; - } - } else if (!encoding.active) { - hasChanged = true; - encoding.active = true; + } + // simulcast dynacast encodings + var idx = 0; + for (var encoding in encodings) { + var rid = encoding.rid ?? ''; + if (rid == '') { + rid = 'q'; } - } else { - // For SVC codecs (VP9/AV1), all layers must be enabled together since - // the SFU handles layer selection. Only allow disabling the entire track. - if (isSVCCodec(codec ?? '')) { - final hasEnabledEncoding = layers.any((q) => q.enabled); - if (hasEnabledEncoding) { - for (var q in layers) { - q.enabled = true; - } - } + final quality = _videoQualityForRid(rid); + final subscribedQuality = layers.firstWhereOrNull( + (q) => q.quality == quality, + ); + if (subscribedQuality == null) { + continue; } - // simulcast dynacast encodings - var idx = 0; - for (var encoding in encodings) { - var rid = encoding.rid ?? ''; - if (rid == '') { - rid = 'q'; - } - final quality = _videoQualityForRid(rid); - final subscribedQuality = layers.firstWhereOrNull( - (q) => q.quality == quality, + if (encoding.active != subscribedQuality.enabled) { + hasChanged = true; + encoding.active = subscribedQuality.enabled; + logger.fine( + 'setting layer ${subscribedQuality.quality} to ${encoding.active ? 'enabled' : 'disabled'}', ); - if (subscribedQuality == null) { - continue; - } - if (encoding.active != subscribedQuality.enabled) { - hasChanged = true; - encoding.active = subscribedQuality.enabled; - logger.fine( - 'setting layer ${subscribedQuality.quality} to ${encoding.active ? 'enabled' : 'disabled'}', - ); - // FireFox does not support setting encoding.active to false, so we - // have a workaround of lowering its bitrate and resolution to the min. - if (kIsWeb && lkBrowser() == BrowserType.firefox) { - if (subscribedQuality.enabled) { - final encodingBackup = encodingBackups[(sender.senderId, idx)] ?? encoding; - encoding.scaleResolutionDownBy = encodingBackup.scaleResolutionDownBy; - encoding.maxBitrate = encodingBackup.maxBitrate; - encoding.maxFramerate = encodingBackup.maxFramerate; - } else { - encodingBackups[(sender.senderId, idx)] = rtc.RTCRtpEncoding( - scaleResolutionDownBy: encoding.scaleResolutionDownBy, - maxBitrate: encoding.maxBitrate, - maxFramerate: encoding.maxFramerate, - ); - encoding.scaleResolutionDownBy = 4; - encoding.maxBitrate = 10; - encoding.maxFramerate = 2; - } + // FireFox does not support setting encoding.active to false, so we + // have a workaround of lowering its bitrate and resolution to the min. + if (kIsWeb && lkBrowser() == BrowserType.firefox) { + if (subscribedQuality.enabled) { + final encodingBackup = encodingBackups[(sender.senderId, idx)] ?? encoding; + encoding.scaleResolutionDownBy = encodingBackup.scaleResolutionDownBy; + encoding.maxBitrate = encodingBackup.maxBitrate; + encoding.maxFramerate = encodingBackup.maxFramerate; + } else { + encodingBackups[(sender.senderId, idx)] = rtc.RTCRtpEncoding( + scaleResolutionDownBy: encoding.scaleResolutionDownBy, + maxBitrate: encoding.maxBitrate, + maxFramerate: encoding.maxFramerate, + ); + encoding.scaleResolutionDownBy = 4; + encoding.maxBitrate = 10; + encoding.maxFramerate = 2; } } - idx++; } + idx++; } if (hasChanged) { From 30d6248644c3667b19bad6393577a30347089ccb Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Sun, 22 Mar 2026 21:18:08 +0900 Subject: [PATCH 3/4] fix --- lib/src/participant/local.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/participant/local.dart b/lib/src/participant/local.dart index ee187b90b..56544de00 100644 --- a/lib/src/participant/local.dart +++ b/lib/src/participant/local.dart @@ -376,13 +376,13 @@ class LocalParticipant extends Participant { Future negotiate() async { track.transceiver = await room.engine.createTransceiverRTCRtpSender(track, publishOptions!, encodings); + track.codec = publishOptions.videoCodec; if (lkBrowser() != BrowserType.firefox) { await room.engine.setPreferredCodec( track.transceiver!, 'video', publishOptions.videoCodec, ); - track.codec = publishOptions.videoCodec; } if ([TrackSource.camera, TrackSource.screenShareVideo].contains(track.source)) { @@ -477,13 +477,13 @@ class LocalParticipant extends Participant { init: transceiverInit, ); + track.codec = publishOptions.videoCodec; if (lkBrowser() != BrowserType.firefox) { await room.engine.setPreferredCodec( track.transceiver!, 'video', publishOptions.videoCodec, ); - track.codec = publishOptions.videoCodec; } if ([TrackSource.camera, TrackSource.screenShareVideo].contains(track.source)) { From a1e68c8d36bead8ec223ae0a489d9ed35c389ef0 Mon Sep 17 00:00:00 2001 From: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com> Date: Mon, 23 Mar 2026 17:21:22 +0900 Subject: [PATCH 4/4] changes --- .changes/fix-svc-dynacast | 1 + 1 file changed, 1 insertion(+) create mode 100644 .changes/fix-svc-dynacast diff --git a/.changes/fix-svc-dynacast b/.changes/fix-svc-dynacast new file mode 100644 index 000000000..fecac8c76 --- /dev/null +++ b/.changes/fix-svc-dynacast @@ -0,0 +1 @@ +patch type="fixed" "Fix VP9/SVC dynacast layer handling"