@@ -28,6 +28,7 @@ import '../../proto/livekit_rtc.pb.dart' as lk_rtc;
2828import '../../stats/stats.dart' ;
2929import '../../support/platform.dart' ;
3030import '../../types/other.dart' ;
31+ import '../../utils.dart' show isSVCCodec;
3132import '../options.dart' ;
3233import 'audio.dart' ;
3334import '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