Skip to content

Commit 30a88de

Browse files
xianshijing-lkclaudehiroshihorie
authored
Default video degradation preference by track source, including the backup codec (#1155)
Aligns Flutter with the behavior landed in client-sdk-android (livekit/client-sdk-android#991). Two related changes. ## Source-based defaults Previously every video track fell back to `maintainResolution`, and the preference was only applied to camera and screen share tracks at all: ```dart if ([TrackSource.camera, TrackSource.screenShareVideo].contains(track.source)) { final degradationPreference = options.degradationPreference ?? DegradationPreference.maintainResolution; await track.setDegradationPreference(degradationPreference); } ``` Now `getDefaultDegradationPreference(source)` resolves camera → `maintainFramerate` (smoother video for real-time communication), screen share → `maintainResolution` (clarity matters for text/UI), other → `balanced`, and it is applied to every video sender. Custom sources previously got whatever WebRTC derived implicitly from the native source; `balanced` is the preference the WebRTC spec mandates as the default and is the honest choice when the application declined to declare a motion-vs-detail intent. An explicitly set `degradationPreference` still wins in all cases — the default only fills a null. ## Backup codec sender Degradation preference is a property of the **sender**, not of the track — a top-level field on `RtpParameters`, not per-encoding. `publishAdditionalCodecForPublication` adds a second transceiver and therefore a second sender, which was never configured, so the backup encoder resolved a preference implicitly and could adapt along a different axis than the primary. Both senders sink from the same video source, so a diverging backup does not just degrade itself — its restriction is merged onto the shared source and affects the primary too. `setDegradationPreference` now stores the resolved preference and fans out to every sender, and `publishAdditionalCodecForPublication` applies it to the backup sender once created. Using the track's stored resolved value means the two encoders cannot disagree. Note simulcast is unaffected — all simulcast encodings live under one sender and already share its preference. Only the backup codec is a separate sender. ## Tests `test/options/degradation_preference_test.dart` covers the three source mappings. Full suite passes (379 tests), `flutter analyze lib/ test/` clean, `dart format` clean at the repo's 120-column width. ## Cross-SDK client-sdk-js gets the backup-sender half in livekit/client-sdk-js#2040 (its source-based defaults already matched). The Rust SDK already resolves the same defaults and has no backup-codec publish path. Swift follows separately. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Hiroshi Horie <548776+hiroshihorie@users.noreply.github.com>
1 parent 3f67c74 commit 30a88de

5 files changed

Lines changed: 110 additions & 11 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
patch type="changed" "Default video degradation preference is now based on the track source (camera maintains framerate, screen share maintains resolution, others balanced) and is applied to the backup codec's sender as well"

lib/src/options.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,29 @@ enum DegradationPreference {
348348
maintainFramerateAndResolution,
349349
}
350350

351+
/// Returns the degradation preference to use for a video track published under
352+
/// [source], when the application did not set one explicitly.
353+
///
354+
/// - Camera: [DegradationPreference.maintainFramerate] (smoother video for
355+
/// real-time communication)
356+
/// - Screen share: [DegradationPreference.maintainResolution] (clarity is
357+
/// critical for reading text/UI)
358+
/// - Other/unknown: [DegradationPreference.balanced]
359+
///
360+
/// Any other source means the application declined to declare a
361+
/// motion-vs-detail intent, so this falls back to balanced, the preference the
362+
/// WebRTC spec mandates as the default.
363+
DegradationPreference getDefaultDegradationPreference(TrackSource source) {
364+
switch (source) {
365+
case TrackSource.camera:
366+
return DegradationPreference.maintainFramerate;
367+
case TrackSource.screenShareVideo:
368+
return DegradationPreference.maintainResolution;
369+
default:
370+
return DegradationPreference.balanced;
371+
}
372+
}
373+
351374
class BackupVideoCodec {
352375
const BackupVideoCodec({
353376
this.enabled = true,
@@ -415,6 +438,13 @@ class VideoPublishOptions extends PublishOptions {
415438
/// Defaults to true.
416439
final bool simulcast;
417440

441+
/// Controls how the encoder trades off between resolution and framerate when
442+
/// bandwidth is constrained.
443+
///
444+
/// When null, the SDK picks a default based on the track's source, see
445+
/// [getDefaultDegradationPreference]. A preference is always applied to video
446+
/// senders, so leaving this null selects that default rather than deferring to
447+
/// WebRTC's own implicit choice.
418448
final DegradationPreference? degradationPreference;
419449

420450
final List<VideoParameters> videoSimulcastLayers;

lib/src/participant/local.dart

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -390,10 +390,9 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
390390
);
391391
}
392392

393-
if ([TrackSource.camera, TrackSource.screenShareVideo].contains(track.source)) {
394-
final degradationPreference = options.degradationPreference ?? DegradationPreference.maintainResolution;
395-
await track.setDegradationPreference(degradationPreference);
396-
}
393+
await track.setDegradationPreference(
394+
options.degradationPreference ?? getDefaultDegradationPreference(track.source),
395+
);
397396

398397
if (kIsWeb && lkBrowser() == BrowserType.firefox && track.kind == TrackType.AUDIO) {
399398
//TOOD:
@@ -489,10 +488,9 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
489488
);
490489
}
491490

492-
if ([TrackSource.camera, TrackSource.screenShareVideo].contains(track.source)) {
493-
final degradationPreference = publishOptions.degradationPreference ?? DegradationPreference.maintainResolution;
494-
await track.setDegradationPreference(degradationPreference);
495-
}
491+
await track.setDegradationPreference(
492+
publishOptions.degradationPreference ?? getDefaultDegradationPreference(track.source),
493+
);
496494

497495
if (kIsWeb && lkBrowser() == BrowserType.firefox && track.kind == TrackType.AUDIO) {
498496
//TOOD:
@@ -944,6 +942,10 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
944942
backupCodec,
945943
);
946944

945+
// the backup codec publishes over its own sender, so it needs the same
946+
// degradation preference the primary sender resolved to.
947+
await track.applyDegradationPreference(simulcastTrack.sender);
948+
947949
final cid = simulcastTrack.sender!.senderId;
948950

949951
final req = lk_rtc.AddTrackRequest(

lib/src/track/local/video.dart

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ class LocalVideoTrack extends LocalTrack with VideoTrack {
6868
Map<String, SimulcastTrackInfo> simulcastCodecs = {};
6969
Map<(String, int), rtc.RTCRtpEncoding> encodingBackups = {};
7070

71+
DegradationPreference? _degradationPreference;
72+
7173
List<lk_rtc.SubscribedCodec> subscribedCodecs = [];
7274

7375
@override
@@ -503,11 +505,31 @@ extension LocalVideoTrackExt on LocalVideoTrack {
503505
}
504506

505507
Future<void> setDegradationPreference(DegradationPreference preference) async {
506-
final params = sender?.parameters;
507-
if (params == null) {
508+
_degradationPreference = preference;
509+
await applyDegradationPreference(sender);
510+
for (final simulcastCodec in simulcastCodecs.values.toList()) {
511+
await applyDegradationPreference(simulcastCodec.sender);
512+
}
513+
}
514+
515+
/// Applies the degradation preference resolved for this track to [sender].
516+
///
517+
/// Degradation preference is a property of the sender, not of the track, so
518+
/// every sender publishing this track needs it applied separately. A backup
519+
/// codec publishes over its own sender, which would otherwise resolve a
520+
/// preference implicitly and diverge from the primary encoder.
521+
@internal
522+
Future<void> applyDegradationPreference(rtc.RTCRtpSender? sender) async {
523+
final preference = _degradationPreference;
524+
if (sender == null || preference == null) {
508525
return;
509526
}
527+
final params = sender.parameters;
510528
params.degradationPreference = preference.toRTCType();
511-
await sender?.setParameters(params);
529+
try {
530+
await sender.setParameters(params);
531+
} catch (e) {
532+
logger.warning('Failed to set degradation preference on sender $e');
533+
}
512534
}
513535
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2026 LiveKit, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'package:flutter_test/flutter_test.dart';
16+
17+
import 'package:livekit_client/src/options.dart';
18+
import 'package:livekit_client/src/types/other.dart';
19+
20+
void main() {
21+
group('getDefaultDegradationPreference', () {
22+
test('camera prefers framerate', () {
23+
expect(
24+
getDefaultDegradationPreference(TrackSource.camera),
25+
DegradationPreference.maintainFramerate,
26+
);
27+
});
28+
29+
test('screen share prefers resolution', () {
30+
expect(
31+
getDefaultDegradationPreference(TrackSource.screenShareVideo),
32+
DegradationPreference.maintainResolution,
33+
);
34+
});
35+
36+
test('other sources fall back to balanced', () {
37+
// the application declined to declare a motion-vs-detail intent
38+
expect(
39+
getDefaultDegradationPreference(TrackSource.unknown),
40+
DegradationPreference.balanced,
41+
);
42+
});
43+
});
44+
}

0 commit comments

Comments
 (0)