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
13 changes: 13 additions & 0 deletions .changeset/improve-initial-video-quality.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
livekit: minor
livekit-ffi: minor
libwebrtc: minor
---

Improve initial video quality by setting `x-google-start-bitrate` SDP hint for all video codecs (VP8, VP9, AV1, H264, H265) and defaulting to `MaintainResolution` degradation preference.

This addresses the issue where video starts blurry for several seconds before improving, by:
1. Telling WebRTC's bandwidth estimator to start at 70% of target bitrate instead of ramping up from ~300kbps
2. Preferring frame drops over resolution reduction when bandwidth is constrained

The `DegradationPreference` option is now exposed via FFI for Python, C++, Unity, and Node SDKs.
44 changes: 44 additions & 0 deletions libwebrtc/src/rtp_parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ pub enum Priority {
High,
}

/// Controls how the encoder degrades quality when bandwidth is constrained.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
pub enum DegradationPreference {
/// Degrade framerate to maintain resolution.
MaintainFramerate,
/// Degrade resolution to maintain framerate.
MaintainResolution,
/// Balance between framerate and resolution degradation.
#[default]
Balanced,
/// Disable degradation preference (not recommended).
Disabled,
}

#[derive(Debug, Clone)]
pub struct RtpHeaderExtensionParameters {
pub uri: String,
Expand All @@ -43,6 +57,36 @@ pub struct RtpParameters {
pub(crate) degradation_preference: i32,
}

impl RtpParameters {
/// Sets the degradation preference for this RTP sender.
///
/// This controls how the encoder trades off between resolution and framerate
/// when bandwidth is constrained.
pub fn set_degradation_preference(&mut self, preference: DegradationPreference) {
self.has_degradation_preference = true;
self.degradation_preference = match preference {
DegradationPreference::Disabled => 0,
DegradationPreference::MaintainFramerate => 1,
DegradationPreference::MaintainResolution => 2,
DegradationPreference::Balanced => 3,
};
}

/// Gets the current degradation preference, if set.
pub fn degradation_preference(&self) -> Option<DegradationPreference> {
if !self.has_degradation_preference {
return None;
}
Some(match self.degradation_preference {
0 => DegradationPreference::Disabled,
1 => DegradationPreference::MaintainFramerate,
2 => DegradationPreference::MaintainResolution,
3 => DegradationPreference::Balanced,
_ => DegradationPreference::Balanced,
})
}
}

/// Mirrors webrtc_sys RtcpFeedback for round-trip fidelity.
#[derive(Debug, Clone, Default)]
pub(crate) struct CodecFeedback {
Expand Down
43 changes: 43 additions & 0 deletions livekit-ffi-node-bindings/proto/room_pb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,41 @@ export declare enum VideoEncoderBackend {
ENCODER_BACKEND_VIDEOTOOLBOX = 5,
}

/**
* Controls how the encoder degrades quality when bandwidth is constrained.
*
* @generated from enum livekit.proto.DegradationPreference
*/
export declare enum DegradationPreference {
/**
* Balance between framerate and resolution degradation.
*
* @generated from enum value: DEGRADATION_PREFERENCE_BALANCED = 0;
*/
BALANCED = 0,

/**
* Degrade framerate to maintain resolution.
*
* @generated from enum value: DEGRADATION_PREFERENCE_MAINTAIN_FRAMERATE = 1;
*/
MAINTAIN_FRAMERATE = 1,

/**
* Degrade resolution to maintain framerate (drop frames to keep clarity).
*
* @generated from enum value: DEGRADATION_PREFERENCE_MAINTAIN_RESOLUTION = 2;
*/
MAINTAIN_RESOLUTION = 2,

/**
* Disable degradation preference.
*
* @generated from enum value: DEGRADATION_PREFERENCE_DISABLED = 3;
*/
DISABLED = 3,
}

/**
* @generated from enum livekit.proto.IceTransportType
*/
Expand Down Expand Up @@ -1873,6 +1908,14 @@ export declare class TrackPublishOptions extends Message<TrackPublishOptions> {
*/
videoEncoder?: VideoEncoderBackend;

/**
* Controls how the encoder trades off between resolution and framerate
* when bandwidth is constrained. Default is MAINTAIN_RESOLUTION.
*
* @generated from field: optional livekit.proto.DegradationPreference degradation_preference = 13;
*/
degradationPreference?: DegradationPreference;

constructor(data?: PartialMessage<TrackPublishOptions>);

static readonly runtime: typeof proto2;
Expand Down
17 changes: 17 additions & 0 deletions livekit-ffi-node-bindings/proto/room_pb.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,21 @@ const VideoEncoderBackend = /*@__PURE__*/ proto2.makeEnum(
],
);

/**
* Controls how the encoder degrades quality when bandwidth is constrained.
*
* @generated from enum livekit.proto.DegradationPreference
*/
const DegradationPreference = /*@__PURE__*/ proto2.makeEnum(
"livekit.proto.DegradationPreference",
[
{no: 0, name: "DEGRADATION_PREFERENCE_BALANCED", localName: "BALANCED"},
{no: 1, name: "DEGRADATION_PREFERENCE_MAINTAIN_FRAMERATE", localName: "MAINTAIN_FRAMERATE"},
{no: 2, name: "DEGRADATION_PREFERENCE_MAINTAIN_RESOLUTION", localName: "MAINTAIN_RESOLUTION"},
{no: 3, name: "DEGRADATION_PREFERENCE_DISABLED", localName: "DISABLED"},
],
);

/**
* @generated from enum livekit.proto.IceTransportType
*/
Expand Down Expand Up @@ -733,6 +748,7 @@ const TrackPublishOptions = /*@__PURE__*/ proto2.makeMessageType(
{ no: 10, name: "frame_metadata_features", kind: "enum", T: proto2.getEnumType(FrameMetadataFeature), repeated: true },
{ no: 11, name: "scalability_mode", kind: "scalar", T: 9 /* ScalarType.STRING */, opt: true },
{ no: 12, name: "video_encoder", kind: "enum", T: proto2.getEnumType(VideoEncoderBackend), opt: true },
{ no: 13, name: "degradation_preference", kind: "enum", T: proto2.getEnumType(DegradationPreference), opt: true },
],
);

Expand Down Expand Up @@ -1635,6 +1651,7 @@ const DataTrackUnpublished = /*@__PURE__*/ proto2.makeMessageType(

exports.SimulateScenarioKind = SimulateScenarioKind;
exports.VideoEncoderBackend = VideoEncoderBackend;
exports.DegradationPreference = DegradationPreference;
exports.IceTransportType = IceTransportType;
exports.ContinualGatheringPolicy = ContinualGatheringPolicy;
exports.ConnectionQuality = ConnectionQuality;
Expand Down
15 changes: 15 additions & 0 deletions livekit-ffi/protocol/room.proto
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,9 @@ message TrackPublishOptions {
optional string scalability_mode = 11;
// Preferred encoder backend to use when publishing a video track.
optional VideoEncoderBackend video_encoder = 12;
// Controls how the encoder trades off between resolution and framerate
// when bandwidth is constrained. Default is MAINTAIN_RESOLUTION.
optional DegradationPreference degradation_preference = 13;
}

enum VideoEncoderBackend {
Expand All @@ -334,6 +337,18 @@ enum VideoEncoderBackend {
ENCODER_BACKEND_VIDEOTOOLBOX = 5;
}

// Controls how the encoder degrades quality when bandwidth is constrained.
enum DegradationPreference {
// Balance between framerate and resolution degradation.
DEGRADATION_PREFERENCE_BALANCED = 0;
// Degrade framerate to maintain resolution.
DEGRADATION_PREFERENCE_MAINTAIN_FRAMERATE = 1;
// Degrade resolution to maintain framerate (drop frames to keep clarity).
DEGRADATION_PREFERENCE_MAINTAIN_RESOLUTION = 2;
// Disable degradation preference.
DEGRADATION_PREFERENCE_DISABLED = 3;
}

enum IceTransportType {
TRANSPORT_RELAY = 0;
TRANSPORT_NOHOST = 1;
Expand Down
18 changes: 16 additions & 2 deletions livekit-ffi/src/conversion/room.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ use livekit::{
E2eeOptions, EncryptionType,
},
options::{
AudioEncoding, FrameMetadataFeatures, TrackPublishOptions, VideoEncoderBackend,
VideoEncoding,
AudioEncoding, DegradationPreference, FrameMetadataFeatures, TrackPublishOptions,
VideoEncoderBackend, VideoEncoding,
},
prelude::*,
webrtc::{
Expand Down Expand Up @@ -66,6 +66,19 @@ fn video_encoder_from_proto(backend: Option<i32>) -> Option<VideoEncoderBackend>
}
}

fn degradation_preference_from_proto(pref: Option<i32>) -> Option<DegradationPreference> {
match pref.and_then(|value| proto::DegradationPreference::try_from(value).ok())? {
proto::DegradationPreference::Balanced => Some(DegradationPreference::Balanced),
proto::DegradationPreference::MaintainFramerate => {
Some(DegradationPreference::MaintainFramerate)
}
proto::DegradationPreference::MaintainResolution => {
Some(DegradationPreference::MaintainResolution)
}
proto::DegradationPreference::Disabled => Some(DegradationPreference::Disabled),
}
}

impl From<EncryptionState> for proto::EncryptionState {
fn from(value: EncryptionState) -> Self {
match value {
Expand Down Expand Up @@ -338,6 +351,7 @@ impl From<proto::TrackPublishOptions> for TrackPublishOptions {
video_encoder: video_encoder_from_proto(opts.video_encoder)
.unwrap_or(default_publish_options.video_encoder),
scalability_mode: opts.scalability_mode,
degradation_preference: degradation_preference_from_proto(opts.degradation_preference),
}
}
}
Expand Down
110 changes: 109 additions & 1 deletion livekit/src/room/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ use livekit_protocol as proto;

use crate::prelude::*;

// Re-export DegradationPreference for users
pub use libwebrtc::rtp_parameters::DegradationPreference;

/// Preferred backend for video encoding when publishing a video track.
pub use libwebrtc::rtp_sender::VideoEncoderBackend;

Expand Down Expand Up @@ -136,6 +139,16 @@ pub struct TrackPublishOptions {
/// encoding is produced and that mode is forwarded to libwebrtc to
/// enable true SVC for VP9/AV1. Has no effect for VP8/H264.
pub scalability_mode: Option<String>,
/// Controls how the encoder trades off between resolution and framerate
/// when bandwidth is constrained.
///
/// - `MaintainResolution`: Prioritizes resolution, drops frames if needed
/// - `MaintainFramerate`: Prioritizes framerate, reduces resolution if needed
/// - `Balanced`: Balances between both
///
/// If not set, the SDK will use a smart default based on the track source
/// and resolution (MaintainResolution for screenshare or video >= 540p).
pub degradation_preference: Option<DegradationPreference>,
}

impl Default for TrackPublishOptions {
Expand All @@ -154,10 +167,36 @@ impl Default for TrackPublishOptions {
frame_metadata_features: FrameMetadataFeatures::default(),
video_encoder: VideoEncoderBackend::Auto,
scalability_mode: None,
degradation_preference: None,
}
}
}

/// Returns the appropriate degradation preference for a video track.
///
/// If the user explicitly set a preference in `TrackPublishOptions`, that is returned.
/// Otherwise, defaults to `MaintainResolution` for all video tracks.
///
/// `MaintainResolution` ensures video clarity is preserved during bandwidth constraints
/// by dropping frames rather than reducing resolution. This prevents the "blurry video"
/// issue that users commonly report during initial connection or network fluctuations.
///
/// Users who prefer smoother video over clarity can explicitly set `Balanced` or
/// `MaintainFramerate` in their `TrackPublishOptions`.
pub fn get_default_degradation_preference(
options: &TrackPublishOptions,
_height: u32,
) -> DegradationPreference {
// Return user's explicit choice if set
if let Some(pref) = options.degradation_preference {
return pref;
}

// Default to MaintainResolution for all video tracks to prevent blurry video
// during bandwidth ramp-up or network constraints
DegradationPreference::MaintainResolution
}

impl VideoPreset {
pub const fn new(width: u32, height: u32, max_bitrate: u64, max_framerate: f64) -> Self {
Self { width, height, encoding: VideoEncoding { max_bitrate, max_framerate } }
Expand Down Expand Up @@ -515,10 +554,79 @@ pub mod screenshare {

#[cfg(test)]
mod tests {
use super::{TrackPublishOptions, VideoEncoderBackend};
use super::{
get_default_degradation_preference, DegradationPreference, TrackPublishOptions,
VideoEncoderBackend,
};
use crate::prelude::TrackSource;

#[test]
fn track_publish_options_default_encoder_is_auto() {
assert_eq!(TrackPublishOptions::default().video_encoder, VideoEncoderBackend::Auto);
}

#[test]
fn degradation_preference_defaults_to_none() {
assert_eq!(TrackPublishOptions::default().degradation_preference, None);
}

#[test]
fn degradation_preference_defaults_to_maintain_resolution() {
// All sources should default to MaintainResolution
let camera_options =
TrackPublishOptions { source: TrackSource::Camera, ..Default::default() };
let screenshare_options =
TrackPublishOptions { source: TrackSource::Screenshare, ..Default::default() };
let default_options = TrackPublishOptions::default();

assert_eq!(
get_default_degradation_preference(&camera_options, 1080),
DegradationPreference::MaintainResolution
);
assert_eq!(
get_default_degradation_preference(&screenshare_options, 1080),
DegradationPreference::MaintainResolution
);
assert_eq!(
get_default_degradation_preference(&default_options, 720),
DegradationPreference::MaintainResolution
);
assert_eq!(
get_default_degradation_preference(&default_options, 360),
DegradationPreference::MaintainResolution
);
}

#[test]
fn degradation_preference_respects_explicit_user_choice() {
// User explicitly sets MaintainFramerate
let options = TrackPublishOptions {
degradation_preference: Some(DegradationPreference::MaintainFramerate),
..Default::default()
};
assert_eq!(
get_default_degradation_preference(&options, 1080),
DegradationPreference::MaintainFramerate
);

// User explicitly sets Balanced
let options = TrackPublishOptions {
degradation_preference: Some(DegradationPreference::Balanced),
..Default::default()
};
assert_eq!(
get_default_degradation_preference(&options, 1080),
DegradationPreference::Balanced
);

// User explicitly sets Disabled
let options = TrackPublishOptions {
degradation_preference: Some(DegradationPreference::Disabled),
..Default::default()
};
assert_eq!(
get_default_degradation_preference(&options, 1080),
DegradationPreference::Disabled
);
}
}
Loading
Loading