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
26 changes: 9 additions & 17 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,42 +318,38 @@ 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
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,
preConnect: preConnect ?? this.preConnect,
);

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

final backupCodecs = ['vp8', 'h264'];
Expand Down
12 changes: 4 additions & 8 deletions lib/src/participant/local.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ 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';
Expand Down Expand Up @@ -123,11 +124,8 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
// Use defaultPublishOptions if options is null
publishOptions ??= track.lastPublishOptions ?? room.roomOptions.defaultAudioPublishOptions;

final List<rtc.RTCRtpEncoding> encodings = [
rtc.RTCRtpEncoding(
maxBitrate: publishOptions.audioBitrate,
)
];
final audioEncoding = publishOptions.encoding ?? AudioEncoding.presetMusic;
final List<rtc.RTCRtpEncoding> encodings = [audioEncoding.toRTCRtpEncoding()];

final req = lk_rtc.AddTrackRequest(
cid: track.getCid(),
Expand Down Expand Up @@ -162,9 +160,7 @@ class LocalParticipant extends Participant<LocalTrackPublication> {

final transceiverInit = rtc.RTCRtpTransceiverInit(
direction: rtc.TransceiverDirection.SendOnly,
sendEncodings: [
if (publishOptions.audioBitrate > 0) rtc.RTCRtpEncoding(maxBitrate: publishOptions.audioBitrate),
],
sendEncodings: encodings,
);
// addTransceiver cannot pass in a kind parameter due to a bug in flutter-webrtc (web)
track.transceiver = await room.engine.publisher?.pc.addTransceiver(
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.isDscpEnabled` 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?.toRtcpPriorityType() ?? rtc.RTCPriorityType.low,
networkPriority: networkPriority?.toRtcpPriorityType(),
);
Comment on lines 83 to 87
Copy link

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.

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

/// Allows DSCP (Differentiated Services Code Point) codes to be set on
/// outgoing packets for network level QoS.
///
/// This is a best effort hint and network routers may ignore DSCP markings.
/// Required for `networkPriority` to take effect.
///
/// Ignored on web platforms.
final bool? isDscpEnabled;

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

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

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;
}
}
}
Loading