Skip to content

Fix initial video quality blurriness for all codecs - #1197

Merged
xianshijing-lk merged 6 commits into
mainfrom
sxian/CLT-3068/fix-initial-video-quality-blurriness-by-setting-x-google-start-bitrate
Jun 30, 2026
Merged

xianshijing-lk merged 6 commits into
mainfrom
sxian/CLT-3068/fix-initial-video-quality-blurriness-by-setting-x-google-start-bitrate

Conversation

@xianshijing-lk

Copy link
Copy Markdown
Contributor

Summary

Fixes initial video quality blurriness by:

  1. Applying x-google-start-bitrate SDP hint for all video codecs (VP8, VP9, AV1, H264, H265)
  2. Defaulting DegradationPreference to MaintainResolution for all video tracks

Problem

Users report video starting blurry for 5-15 seconds before reaching full quality (https://www.loom.com/share/9a34ae565f6949b891a155a436b3d573). This happens because:

  1. Slow bandwidth ramp-up: x-google-start-bitrate was only set for VP9/AV1, so VP8/H264 streams start at WebRTC's conservative default (~300 kbps) and ramp up slowly.
  2. Resolution degradation: Default DegradationPreference was Balanced, causing the encoder to reduce resolution during the ramp-up period.

Changes

livekit/src/rtc_engine/peer_transport.rs

  • Extended x-google-start-bitrate SDP munging to all video codecs (VP8, VP9, AV1, H264, H265)
  • Updated tests

libwebrtc/src/rtp_parameters.rs

  • Added DegradationPreference enum
  • Added set_degradation_preference() and degradation_preference() methods to RtpParameters

livekit/src/room/options.rs

  • Added degradation_preference field to TrackPublishOptions
  • Added get_default_degradation_preference() that defaults to MaintainResolution
  • Re-exported DegradationPreference for users

livekit/src/room/participant/local_participant.rs

  • Apply degradation preference to video tracks after creating the transceiver

Behavior
│ Setting │ Before │ After │
│ x-google-start-bitrate │ VP9/AV1 only │ All video codecs │
│ DegradationPreference │ Not set (WebRTC default) │ MaintainResolution │
Users can override degradation_preference in TrackPublishOptions if they prefer Balanced or MaintainFramerate.

Testing

  • Added unit tests for DegradationPreference defaults
  • Updated x-google-start-bitrate tests for all codecs
  • Manual testing with local_video example

@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Changeset

The following package versions will be affected by this PR:

Package Bump
libwebrtc minor
livekit minor
livekit-ffi minor

@boks1971 boks1971 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM!

Comment thread livekit/src/rtc_engine/peer_transport.rs Outdated
@xianshijing-lk
xianshijing-lk force-pushed the sxian/CLT-3068/fix-initial-video-quality-blurriness-by-setting-x-google-start-bitrate branch from bf4b429 to ac2f359 Compare June 29, 2026 10:09
@xianshijing-lk
xianshijing-lk merged commit d697b47 into main Jun 30, 2026
23 checks passed
@xianshijing-lk
xianshijing-lk deleted the sxian/CLT-3068/fix-initial-video-quality-blurriness-by-setting-x-google-start-bitrate branch June 30, 2026 02:27
sionzee added a commit to evobug-com/rust-sdks that referenced this pull request Jul 5, 2026
- drop 2 Mbps screenshare min_bitrate floor: exceeds preset
  max_bitrate (INVALID_RANGE, publish fails); upstream livekit#1197
  covers the localhost blurriness it worked around
- revert H265 Main10/HDR VUI to Main profile: bitstream broke
  Main-only decoders and mismatched SDP-negotiated profile
- update frameRateNum before VBV sizing in SetRates (h264+h265)
- delay-load nvcuda.dll/nvcuvid.dll so non-NVIDIA Windows hosts
  fall back to software codecs instead of failing at load
- fix vcvarsall import: raw_arg quoting, verify vars parsed
- warn when CUDA present but nvenc-linux feature disabled
- clamp min/max_bitrate u64->i32 casts to i32::MAX
- sync-upstream.yml: add contents:write, pre-push sanity gate,
  note cron requires file on default branch
- remove dead nvidia_factory_bridge.{cpp,h}
sergeyphi added a commit to sergeyphi/client-sdk-swift that referenced this pull request Sep 12, 2026
…-bitrate

libwebrtc starts every new video send stream at roughly 300 kbps and ramps
up from there regardless of the encodings' maxBitrate, so the first seconds
of a published track are visibly blurry (measured: QP 39-42 for ~12 s, the
2.3 Mbps ceiling reached only after ~30 s on a healthy network).

Match what client-sdk-js (livekit/client-sdk-js#1987) and rust-sdks
(livekit/rust-sdks#1197, #1226) already do: when a video sender is added,
derive a start bitrate from its encodings — the sum of the active layers'
maxBitrate, times 0.9, capped at 1 Mbps unless the track is a screen share,
and no hint under 300 kbps — and declare `x-google-start-bitrate=<kbps>` on
every video codec's fmtp (VP8/VP9/AV1/H264/H265) of that sender's section in
the publisher's offer, matched through the `a=msid` track id. Not opt-in and
no public API, as in the sibling SDKs.

- `SDPMediaSection.setFmtpParameter(_:value:forPayload:)`: replaces an
  existing value, appends to an existing fmtp line, or inserts one for
  payloads that have none (VP8), keeping every other parameter verbatim.
- `Transport.startBitrateKbps(targetBps:isScreenShare:)` /
  `startBitrateKbps(for:isScreenShare:)`: the shared formula.
- `Transport.mungeVideoStartBitrate(_:kbpsBySenderId:)`, appended as the
  last (optional) munge of `set(localDescription:munging:)` (livekit#1068) in both
  single- and dual-PC negotiation, on the SDP module from livekit#1078.
- `Transport.addTransceiver(with:transceiverInit:startBitrateKbps:)` records
  the hint against the new sender before returning, so the offer libwebrtc
  requests in response cannot be created without it; `remove(track:)`
  clears it. Both `LocalParticipant` video publish paths (primary and
  backup codec) pass the derived value.

Tests: the formula, the encodings sum, the per-sender munge (codec case,
replace/append/insert, rtx/audio/unmapped sections untouched, no-op
identity), `setFmtpParameter`, and — against the shipped libwebrtc — that a
send-only video section's msid carries the sender id and that the munged
offer is accepted by `setLocalDescription` rather than rejected as
disallowed munging.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
sergeyphi added a commit to sergeyphi/client-sdk-swift that referenced this pull request Sep 12, 2026
…-bitrate

libwebrtc starts every new video send stream at roughly 300 kbps and ramps
up from there regardless of the encodings' maxBitrate, so the first seconds
of a published track are visibly blurry (measured: QP 39-42 for ~12 s, the
2.3 Mbps ceiling reached only after ~30 s on a healthy network).

Match what client-sdk-js (livekit/client-sdk-js#1987) and rust-sdks
(livekit/rust-sdks#1197, #1226) already do: when a video sender is added,
derive a start bitrate from its encodings — the sum of the active layers'
maxBitrate, times 0.9, capped at 1 Mbps unless the track is a screen share,
and no hint under 300 kbps — and declare `x-google-start-bitrate=<kbps>` on
every video codec's fmtp (VP8/VP9/AV1/H264/H265) of that sender's section in
the publisher's offer, matched through the `a=msid` track id. Not opt-in and
no public API, as in the sibling SDKs.

- `SDPMediaSection.setFmtpParameter(_:value:forPayload:)`: replaces an
  existing value, appends to an existing fmtp line, or inserts one for
  payloads that have none (VP8), keeping every other parameter verbatim.
- `Transport.startBitrateKbps(targetBps:isScreenShare:)` /
  `startBitrateKbps(for:isScreenShare:)`: the shared formula.
- `Transport.mungeVideoStartBitrate(_:kbpsBySenderId:)`, appended as the
  last (optional) munge of `set(localDescription:munging:)` (livekit#1068) in both
  single- and dual-PC negotiation, on the SDP module from livekit#1078.
- `Transport.addTransceiver(with:transceiverInit:startBitrateKbps:)` records
  the hint against the new sender before returning, so the offer libwebrtc
  requests in response cannot be created without it; `remove(track:)`
  clears it. Both `LocalParticipant` video publish paths (primary and
  backup codec) pass the derived value.
- The backup-codec publish path now computes its encodings with
  `isScreenShare`, as the primary path does; it had defaulted to the camera
  encoding since livekit#275, which the screen-share branch of the new formula
  made visible.

Tests: the formula, the encodings sum, the per-sender munge (codec case,
replace/append/insert, rtx/audio/unmapped sections untouched, no-op
identity), `setFmtpParameter`, the backup codec's screen-share encodings,
and — against the shipped libwebrtc — that a send-only video section's msid
carries the sender id and that the munged offer is accepted by
`setLocalDescription` rather than rejected as disallowed munging.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants