Skip to content
Merged
1 change: 1 addition & 0 deletions .changes/priority-control
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
patch type="added" "Bitrate priority control APIs"
2 changes: 2 additions & 0 deletions lib/livekit_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ export 'src/track/remote/video.dart';
export 'src/track/track.dart';
export 'src/json/agent_attributes.dart';
export 'src/types/data_stream.dart';
export 'src/types/audio_encoding.dart';
export 'src/types/other.dart';
export 'src/types/participant_permissions.dart';
export 'src/types/participant_state.dart';
export 'src/types/priority.dart';
export 'src/types/rpc.dart';
export 'src/types/transcription_segment.dart';
export 'src/types/video_dimensions.dart';
Expand Down
25 changes: 9 additions & 16 deletions lib/src/options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import 'e2ee/options.dart';
import 'track/local/audio.dart';
import 'track/local/video.dart';
import 'track/options.dart';
import 'types/audio_encoding.dart';
import 'types/other.dart';
import 'types/video_encoding.dart';
import 'types/video_parameters.dart';
Expand Down Expand Up @@ -301,19 +302,14 @@ class VideoPublishOptions extends PublishOptions {
String toString() => '${runtimeType}(videoEncoding: ${videoEncoding}, simulcast: ${simulcast})';
}

class AudioPreset {
static const telephone = 12000;
static const speech = 24000;
static const music = 48000;
static const musicStereo = 64000;
static const musicHighQuality = 96000;
static const musicHighQualityStereo = 128000;
}

/// Options used when publishing audio.
class AudioPublishOptions extends PublishOptions {
static const defaultMicrophoneName = 'microphone';

/// Preferred encoding parameters.
/// Defaults to [AudioEncoding.presetMusic] when not set.
final AudioEncoding? encoding;

/// Whether to enable DTX (Discontinuous Transmission) or not.
/// https://en.wikipedia.org/wiki/Discontinuous_transmission
/// Defaults to true.
Expand All @@ -322,33 +318,30 @@ class AudioPublishOptions extends PublishOptions {
/// red (Redundant Audio Data)
final bool? red;

/// max audio bitrate
final int audioBitrate;

/// Mark this audio as originating from a pre-connect buffer.
/// Used to populate protobuf audioFeatures (TF_PRECONNECT_BUFFER).
final bool preConnect;

const AudioPublishOptions({
super.name,
super.stream,
this.encoding,
this.dtx = true,
this.red = true,
this.audioBitrate = AudioPreset.music,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you follow semver? Because this was a breaking change.

this.preConnect = false,
});

AudioPublishOptions copyWith({
AudioEncoding? encoding,
bool? dtx,
int? audioBitrate,
String? name,
String? stream,
bool? red,
bool? preConnect,
}) =>
AudioPublishOptions(
encoding: encoding ?? this.encoding,
dtx: dtx ?? this.dtx,
audioBitrate: audioBitrate ?? this.audioBitrate,
name: name ?? this.name,
stream: stream ?? this.stream,
red: red ?? this.red,
Expand All @@ -357,7 +350,7 @@ class AudioPublishOptions extends PublishOptions {

@override
String toString() =>
'${runtimeType}(dtx: ${dtx}, audioBitrate: ${audioBitrate}, red: ${red}, preConnect: ${preConnect})';
'${runtimeType}(encoding: ${encoding}, dtx: ${dtx}, red: ${red}, preConnect: ${preConnect})';
}

final backupCodecs = ['vp8', 'h264'];
Expand Down
14 changes: 12 additions & 2 deletions lib/src/participant/local.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@ import '../track/local/audio.dart';
import '../track/local/local.dart';
import '../track/local/video.dart';
import '../track/options.dart';
import '../types/audio_encoding.dart';
import '../types/data_stream.dart';
import '../types/other.dart';
import '../types/participant_permissions.dart';
import '../types/priority.dart';
import '../types/rpc.dart';
import '../types/video_dimensions.dart';
import '../utils.dart' show buildStreamId, mimeTypeToVideoCodecString, Utils, compareVersions, isSVCCodec;
Expand Down Expand Up @@ -123,9 +125,12 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
// Use defaultPublishOptions if options is null
publishOptions ??= track.lastPublishOptions ?? room.roomOptions.defaultAudioPublishOptions;

final audioEncoding = publishOptions.encoding ?? AudioEncoding.presetMusic;
final List<rtc.RTCRtpEncoding> encodings = [
rtc.RTCRtpEncoding(
maxBitrate: publishOptions.audioBitrate,
maxBitrate: audioEncoding.maxBitrate,
priority: (audioEncoding.bitratePriority ?? Priority.low).toRtcpPriorityType(),
networkPriority: audioEncoding.networkPriority?.toRtcpPriorityType(),
)
];

Expand Down Expand Up @@ -163,7 +168,12 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
final transceiverInit = rtc.RTCRtpTransceiverInit(
direction: rtc.TransceiverDirection.SendOnly,
sendEncodings: [
if (publishOptions.audioBitrate > 0) rtc.RTCRtpEncoding(maxBitrate: publishOptions.audioBitrate),
if (audioEncoding.maxBitrate > 0)
rtc.RTCRtpEncoding(
maxBitrate: audioEncoding.maxBitrate,
priority: (audioEncoding.bitratePriority ?? Priority.low).toRtcpPriorityType(),
networkPriority: audioEncoding.networkPriority?.toRtcpPriorityType(),
),
],
);
// addTransceiver cannot pass in a kind parameter due to a bug in flutter-webrtc (web)
Expand Down
3 changes: 1 addition & 2 deletions lib/src/track/web/_audio_analyser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import 'dart:js_interop';
import 'dart:js_interop_unsafe';
import 'dart:math' as math;

import 'package:dart_webrtc/dart_webrtc.dart' show MediaStreamTrackWeb;
import 'package:dart_webrtc/dart_webrtc.dart' show MediaStreamWeb;
import 'package:dart_webrtc/dart_webrtc.dart' show MediaStreamTrackWeb, MediaStreamWeb;
import 'package:web/web.dart' as web;

import '../../track/local/local.dart' show AudioTrack;
Expand Down
88 changes: 88 additions & 0 deletions lib/src/types/audio_encoding.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright 2024 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_webrtc/flutter_webrtc.dart' as rtc;
import 'package:meta/meta.dart';

import 'priority.dart';

/// A type that represents audio encoding information.
@immutable
class AudioEncoding {
/// Maximum bitrate for the audio track.
final int maxBitrate;

/// Priority for bandwidth allocation.
final Priority? bitratePriority;

/// Priority for DSCP marking. Requires `RTCConfiguration.enableDscp` to be true.
final Priority? networkPriority;

const AudioEncoding({
required this.maxBitrate,
this.bitratePriority,
this.networkPriority,
});

AudioEncoding copyWith({
int? maxBitrate,
Priority? bitratePriority,
Priority? networkPriority,
}) =>
AudioEncoding(
maxBitrate: maxBitrate ?? this.maxBitrate,
bitratePriority: bitratePriority ?? this.bitratePriority,
networkPriority: networkPriority ?? this.networkPriority,
);

@override
String toString() =>
'${runtimeType}(maxBitrate: ${maxBitrate}, bitratePriority: ${bitratePriority}, networkPriority: ${networkPriority})';

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AudioEncoding &&
maxBitrate == other.maxBitrate &&
bitratePriority == other.bitratePriority &&
networkPriority == other.networkPriority;

@override
int get hashCode => Object.hash(maxBitrate, bitratePriority, networkPriority);

static const presetTelephone = AudioEncoding(maxBitrate: 12000);
static const presetSpeech = AudioEncoding(maxBitrate: 24000);
static const presetMusic = AudioEncoding(maxBitrate: 48000);
static const presetMusicStereo = AudioEncoding(maxBitrate: 64000);
static const presetMusicHighQuality = AudioEncoding(maxBitrate: 96000);
static const presetMusicHighQualityStereo = AudioEncoding(maxBitrate: 128000);

static const presets = [
presetTelephone,
presetSpeech,
presetMusic,
presetMusicStereo,
presetMusicHighQuality,
presetMusicHighQualityStereo,
];
}

/// Convenience extension for [AudioEncoding].
extension AudioEncodingExt on AudioEncoding {
rtc.RTCRtpEncoding toRTCRtpEncoding() => rtc.RTCRtpEncoding(
maxBitrate: maxBitrate,
priority: (bitratePriority ?? Priority.low).toRtcpPriorityType(),
networkPriority: networkPriority?.toRtcpPriorityType(),
);
Comment on lines +83 to +87
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's verify the current state of audio_encoding.dart
cd lib/src/types && head -100 audio_encoding.dart | tail -30

Repository: livekit/client-sdk-flutter

Length of output: 1148


🏁 Script executed:

# Search for video_encoding to compare similar concerns
fd -t f "video_encoding.dart" lib/

Repository: livekit/client-sdk-flutter

Length of output: 103


🏁 Script executed:

# Look for RTCRtpEncoding usage and understand the context better
rg "RTCRtpEncoding" -A 5 -B 2 lib/src/types/

Repository: livekit/client-sdk-flutter

Length of output: 1562


🏁 Script executed:

# Check git history for this file to understand what changed
git log --oneline lib/src/types/audio_encoding.dart | head -20

Repository: livekit/client-sdk-flutter

Length of output: 95


🏁 Script executed:

# Look for bitratePriority usage and how it was handled before
rg "bitratePriority" -B 3 -A 3 lib/src/types/audio_encoding.dart

Repository: livekit/client-sdk-flutter

Length of output: 1681


🏁 Script executed:

# Check video_encoding.dart to see how it handles priority
cat lib/src/types/video_encoding.dart | grep -A 20 "extension VideoEncodingExt"

Repository: livekit/client-sdk-flutter

Length of output: 610


🏁 Script executed:

# Get more recent git history with diffs
git log -p --max-count=5 -- lib/src/types/audio_encoding.dart

Repository: livekit/client-sdk-flutter

Length of output: 52


🏁 Script executed:

# Check the Priority enum definition
rg "enum Priority" -A 5 lib/

Repository: livekit/client-sdk-flutter

Length of output: 291


🏁 Script executed:

# Look for RTCRtpEncoding definition in flutter_webrtc or understand what happens when priority is null
rg "class RTCRtpEncoding" -A 10

Repository: livekit/client-sdk-flutter

Length of output: 52


🏁 Script executed:

# Check if there are any tests or usage of AudioEncoding that depend on the priority default
rg "AudioEncoding\(" -B 2 -A 2 --max-count=15 lib/src/

Repository: livekit/client-sdk-flutter

Length of output: 1576


Fix priority default to preserve null behavior when not configured.
Both AudioEncoding and VideoEncoding presets (presetTelephone, presetSpeech, etc.) are created without specifying bitratePriority, indicating priority should remain unset by default. The current code forces Priority.low when null, which changes this intended behavior. The networkPriority field already uses the correct pattern networkPriority?.toRtcpPriorityType() to preserve null values.

🛠️ Proposed adjustment (preserve default behavior)
-        priority: (bitratePriority ?? Priority.low).toRtcpPriorityType(),
+        priority: bitratePriority?.toRtcpPriorityType(),

This change should also be applied to VideoEncoding which has the identical issue.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
rtc.RTCRtpEncoding toRTCRtpEncoding() => rtc.RTCRtpEncoding(
maxBitrate: maxBitrate,
priority: (bitratePriority ?? Priority.low).toRtcpPriorityType(),
networkPriority: networkPriority?.toRtcpPriorityType(),
);
rtc.RTCRtpEncoding toRTCRtpEncoding() => rtc.RTCRtpEncoding(
maxBitrate: maxBitrate,
priority: bitratePriority?.toRtcpPriorityType(),
networkPriority: networkPriority?.toRtcpPriorityType(),
);
🤖 Prompt for AI Agents
In `@lib/src/types/audio_encoding.dart` around lines 83 - 87, The
toRTCRtpEncoding() methods in AudioEncoding and VideoEncoding currently force a
default Priority.low when bitratePriority is null; change the priority mapping
to preserve null by calling bitratePriority?.toRtcpPriorityType() instead of
(bitratePriority ?? Priority.low).toRtcpPriorityType(), leaving networkPriority
as-is (networkPriority?.toRtcpPriorityType()); update both
AudioEncoding.toRTCRtpEncoding and VideoEncoding.toRTCRtpEncoding accordingly so
presets that omit bitratePriority remain unset.

}
8 changes: 8 additions & 0 deletions lib/src/types/other.dart
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,16 @@ class RTCConfiguration {
final RTCIceTransportPolicy? iceTransportPolicy;
final bool? encodedInsertableStreams;

/// Allows DSCP codes to be set on outgoing packets.
/// No effect on web platforms.
final bool? enableDscp;

const RTCConfiguration({
this.iceCandidatePoolSize,
this.iceServers,
this.iceTransportPolicy,
this.encodedInsertableStreams,
this.enableDscp,
});

Map<String, dynamic> toMap() {
Expand All @@ -158,6 +163,7 @@ class RTCConfiguration {
// only supports unified plan
'sdpSemantics': 'unified-plan',
if (encodedInsertableStreams != null) 'encodedInsertableStreams': encodedInsertableStreams,
if (enableDscp != null) 'enableDscp': enableDscp,
if (iceServersMap.isNotEmpty) 'iceServers': iceServersMap,
if (iceCandidatePoolSize != null) 'iceCandidatePoolSize': iceCandidatePoolSize,
if (iceTransportPolicy != null) 'iceTransportPolicy': iceTransportPolicy!.toStringValue(),
Expand All @@ -170,12 +176,14 @@ class RTCConfiguration {
List<RTCIceServer>? iceServers,
RTCIceTransportPolicy? iceTransportPolicy,
bool? encodedInsertableStreams,
bool? enableDscp,
}) =>
RTCConfiguration(
iceCandidatePoolSize: iceCandidatePoolSize ?? this.iceCandidatePoolSize,
iceServers: iceServers ?? this.iceServers,
iceTransportPolicy: iceTransportPolicy ?? this.iceTransportPolicy,
encodedInsertableStreams: encodedInsertableStreams ?? this.encodedInsertableStreams,
enableDscp: enableDscp ?? this.enableDscp,
);
}

Expand Down
41 changes: 41 additions & 0 deletions lib/src/types/priority.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2024 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_webrtc/flutter_webrtc.dart' as rtc;

/// Priority levels for RTP encoding parameters.
///
/// `bitratePriority` controls WebRTC internal bandwidth allocation between streams.
/// `networkPriority` controls DSCP marking for network-level QoS.
enum Priority {
veryLow,
low,
medium,
high,
}

extension PriorityExt on Priority {
rtc.RTCPriorityType toRtcpPriorityType() {
switch (this) {
case Priority.veryLow:
return rtc.RTCPriorityType.veryLow;
case Priority.low:
return rtc.RTCPriorityType.low;
case Priority.medium:
return rtc.RTCPriorityType.medium;
case Priority.high:
return rtc.RTCPriorityType.high;
}
}
}
30 changes: 27 additions & 3 deletions lib/src/types/video_encoding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,39 +15,61 @@
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:meta/meta.dart';

import 'priority.dart';

/// A type that represents video encoding information.
@immutable
class VideoEncoding implements Comparable<VideoEncoding> {
/// Maximum framerate for the video track.
final int maxFramerate;

/// Maximum bitrate for the video track.
final int maxBitrate;

/// Priority for bandwidth allocation.
final Priority? bitratePriority;

/// Priority for DSCP marking. Requires `RTCConfiguration.enableDscp` to be true.
final Priority? networkPriority;

const VideoEncoding({
required this.maxFramerate,
required this.maxBitrate,
this.bitratePriority,
this.networkPriority,
});

VideoEncoding copyWith({
int? maxFramerate,
int? maxBitrate,
Priority? bitratePriority,
Priority? networkPriority,
}) =>
VideoEncoding(
maxFramerate: maxFramerate ?? this.maxFramerate,
maxBitrate: maxBitrate ?? this.maxBitrate,
bitratePriority: bitratePriority ?? this.bitratePriority,
networkPriority: networkPriority ?? this.networkPriority,
);
Comment on lines 42 to 53
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Allow copyWith to explicitly clear priorities.
As written, passing null keeps the existing value, so callers can’t reset a priority back to “unset.” Consider a sentinel-based pattern.

Suggested fix
 class VideoEncoding implements Comparable<VideoEncoding> {
+  static const Object _unset = Object();
   ...
   VideoEncoding copyWith({
     int? maxFramerate,
     int? maxBitrate,
-    Priority? bitratePriority,
-    Priority? networkPriority,
+    Object? bitratePriority = _unset,
+    Object? networkPriority = _unset,
   }) =>
       VideoEncoding(
         maxFramerate: maxFramerate ?? this.maxFramerate,
         maxBitrate: maxBitrate ?? this.maxBitrate,
-        bitratePriority: bitratePriority ?? this.bitratePriority,
-        networkPriority: networkPriority ?? this.networkPriority,
+        bitratePriority: identical(bitratePriority, _unset)
+            ? this.bitratePriority
+            : bitratePriority as Priority?,
+        networkPriority: identical(networkPriority, _unset)
+            ? this.networkPriority
+            : networkPriority as Priority?,
       );
🤖 Prompt for AI Agents
In `@lib/src/types/video_encoding.dart` around lines 42 - 53, The copyWith
implementation on VideoEncoding prevents callers from clearing priorities
because null means "use existing"; update VideoEncoding.copyWith to accept
explicit clearing flags (e.g., add bool clearBitratePriority = false and bool
clearNetworkPriority = false) and change the logic in VideoEncoding.copyWith so
that if clearBitratePriority is true it sets bitratePriority to null, else if
bitratePriority param is non-null it uses that, otherwise it keeps
this.bitratePriority; do the analogous change for networkPriority. This keeps
backward-compatible behavior for callers who omit the flags while letting
callers explicitly unset priorities.


@override
String toString() => '${runtimeType}(maxFramerate: ${maxFramerate}, maxBitrate: ${maxBitrate})';
String toString() =>
'${runtimeType}(maxFramerate: ${maxFramerate}, maxBitrate: ${maxBitrate}, bitratePriority: ${bitratePriority}, networkPriority: ${networkPriority})';

// ----------------------------------------------------------------------
// equality

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is VideoEncoding && maxFramerate == other.maxFramerate && maxBitrate == other.maxBitrate;
other is VideoEncoding &&
maxFramerate == other.maxFramerate &&
maxBitrate == other.maxBitrate &&
bitratePriority == other.bitratePriority &&
networkPriority == other.networkPriority;

@override
int get hashCode => Object.hash(maxFramerate, maxBitrate);
int get hashCode => Object.hash(maxFramerate, maxBitrate, bitratePriority, networkPriority);

// ----------------------------------------------------------------------
// Comparable
Expand Down Expand Up @@ -78,5 +100,7 @@ extension VideoEncodingExt on VideoEncoding {
maxFramerate: maxFramerate,
maxBitrate: maxBitrate,
numTemporalLayers: numTemporalLayers,
priority: (bitratePriority ?? Priority.low).toRtcpPriorityType(),
networkPriority: networkPriority?.toRtcpPriorityType(),
);
}
Loading
Loading