Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changes/default-degradation-preference-by-source
Original file line number Diff line number Diff line change
@@ -0,0 +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"
30 changes: 30 additions & 0 deletions lib/src/options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,29 @@ enum DegradationPreference {
maintainFramerateAndResolution,
}

/// Returns the degradation preference to use for a video track published under
/// [source], when the application did not set one explicitly.
///
/// - Camera: [DegradationPreference.maintainFramerate] (smoother video for
/// real-time communication)
/// - Screen share: [DegradationPreference.maintainResolution] (clarity is
/// critical for reading text/UI)
/// - Other/unknown: [DegradationPreference.balanced]
///
/// Any other source means the application declined to declare a
/// motion-vs-detail intent, so this falls back to balanced, the preference the
/// WebRTC spec mandates as the default.
DegradationPreference getDefaultDegradationPreference(TrackSource source) {
switch (source) {
case TrackSource.camera:
return DegradationPreference.maintainFramerate;
case TrackSource.screenShareVideo:
return DegradationPreference.maintainResolution;
default:
return DegradationPreference.balanced;
}
}

class BackupVideoCodec {
const BackupVideoCodec({
this.enabled = true,
Expand Down Expand Up @@ -415,6 +438,13 @@ class VideoPublishOptions extends PublishOptions {
/// Defaults to true.
final bool simulcast;

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

final List<VideoParameters> videoSimulcastLayers;
Expand Down
18 changes: 10 additions & 8 deletions lib/src/participant/local.dart
Original file line number Diff line number Diff line change
Expand Up @@ -390,10 +390,9 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
);
}

if ([TrackSource.camera, TrackSource.screenShareVideo].contains(track.source)) {
final degradationPreference = options.degradationPreference ?? DegradationPreference.maintainResolution;
await track.setDegradationPreference(degradationPreference);
}
await track.setDegradationPreference(
options.degradationPreference ?? getDefaultDegradationPreference(track.source),
);

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

if ([TrackSource.camera, TrackSource.screenShareVideo].contains(track.source)) {
final degradationPreference = publishOptions.degradationPreference ?? DegradationPreference.maintainResolution;
await track.setDegradationPreference(degradationPreference);
}
await track.setDegradationPreference(
publishOptions.degradationPreference ?? getDefaultDegradationPreference(track.source),
);

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

// the backup codec publishes over its own sender, so it needs the same
// degradation preference the primary sender resolved to.
await track.applyDegradationPreference(simulcastTrack.sender);

final cid = simulcastTrack.sender!.senderId;

final req = lk_rtc.AddTrackRequest(
Expand Down
28 changes: 25 additions & 3 deletions lib/src/track/local/video.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ class LocalVideoTrack extends LocalTrack with VideoTrack {
Map<String, SimulcastTrackInfo> simulcastCodecs = {};
Map<(String, int), rtc.RTCRtpEncoding> encodingBackups = {};

DegradationPreference? _degradationPreference;

List<lk_rtc.SubscribedCodec> subscribedCodecs = [];

@override
Expand Down Expand Up @@ -503,11 +505,31 @@ extension LocalVideoTrackExt on LocalVideoTrack {
}

Future<void> setDegradationPreference(DegradationPreference preference) async {
final params = sender?.parameters;
if (params == null) {
_degradationPreference = preference;
await applyDegradationPreference(sender);
for (final simulcastCodec in simulcastCodecs.values.toList()) {
await applyDegradationPreference(simulcastCodec.sender);
}
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

/// Applies the degradation preference resolved for this track to [sender].
///
/// Degradation preference is a property of the sender, not of the track, so
/// every sender publishing this track needs it applied separately. A backup
/// codec publishes over its own sender, which would otherwise resolve a
/// preference implicitly and diverge from the primary encoder.
@internal
Future<void> applyDegradationPreference(rtc.RTCRtpSender? sender) async {
final preference = _degradationPreference;
if (sender == null || preference == null) {
return;
}
final params = sender.parameters;
params.degradationPreference = preference.toRTCType();
await sender?.setParameters(params);
try {
await sender.setParameters(params);
} catch (e) {
logger.warning('Failed to set degradation preference on sender $e');
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
}
}
44 changes: 44 additions & 0 deletions test/options/degradation_preference_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2026 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import 'package:flutter_test/flutter_test.dart';

import 'package:livekit_client/src/options.dart';
import 'package:livekit_client/src/types/other.dart';

void main() {
group('getDefaultDegradationPreference', () {
test('camera prefers framerate', () {
expect(
getDefaultDegradationPreference(TrackSource.camera),
DegradationPreference.maintainFramerate,
);
});

test('screen share prefers resolution', () {
expect(
getDefaultDegradationPreference(TrackSource.screenShareVideo),
DegradationPreference.maintainResolution,
);
});

test('other sources fall back to balanced', () {
// the application declined to declare a motion-vs-detail intent
expect(
getDefaultDegradationPreference(TrackSource.unknown),
DegradationPreference.balanced,
);
});
});
}
Loading