Skip to content

Fix initial video quality blurriness for all codecs - #1987

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

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

Conversation

@xianshijing-lk

@xianshijing-lk xianshijing-lk commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Improve initial video quality by:

  1. Capping x-google-start-bitrate at 1 Mbps to prevent BWE from starting too aggressively on high bitrate tracks
  2. Setting source-based degradationPreference defaults for optimal quality per use case

Changes

PCTransport.ts

  • Added 1 Mbps cap on x-google-start-bitrate: min(target * 0.9, 1000 kbps)
  • Prevents bandwidth estimator from overwhelming the network on high bitrate tracks (e.g., 4K video)

publishUtils.ts

  • Updated getDefaultDegradationPreference() to return defaults based on track source:
    • Camera: maintain-framerate (smoother video for real-time communication)
    • Screen share: maintain-resolution (clarity is critical for text/UI)
    • Other/unknown: balanced

options.ts

  • Updated documentation for degradationPreference option

Behavior
│ Setting │ Before │ After │
│ x-google-start-bitrate │ 90% of target (unbounded) │ min(90% of target, 1 Mbps) │
│ Camera degradationPreference │ maintain-resolution │ maintain-framerate │
│ Screen share degradationPreference │ maintain-resolution │ maintain-resolution │
│ Other degradationPreference │ maintain-resolution │ balanced │

Related

Aligns with rust-sdks PRs:

See Rust PR: livekit/rust-sdks#1197

@changeset-bot

changeset-bot Bot commented Jun 29, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 7029ee9

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
dist/livekit-client.esm.mjs 101.49 KB (+0.01% 🔺)
dist/livekit-client.umd.js 110.44 KB (+0.07% 🔺)

Comment thread src/room/PCTransport.ts Outdated
devin-ai-integration[bot]

This comment was marked as resolved.

});
} else if (track.codec && isVideoCodec(track.codec)) {
// Apply start bitrate for all video codecs to prevent initial blurriness.
// Sum all encoding bitrates for simulcast (BWE needs to handle all layers combined).

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.

don't we need to differentiate between SVC and non SVC here? (i.e. keeping it with first encoding only for SVC and summing for the others?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Theoretically yes, technically no.

For modern SVC ((Chrome M113+), they are single encoding with scalability mode, so it is fine.
for legacy SVC (Safari/RN/older Chrome): they have multiple encodings describing SVC layers. Summing the value might go beyond the target max bitrate. But given

  1. 1 Mbps cap - We cap at 1 Mbps anyway, so overcounting is limited
  2. Higher is safer - Starting slightly high is better than too low (which causes blurriness)
  3. Legacy SVC is rare - Modern Chrome uses single encoding; mainly Safari/RN use legacy

I fixed the code anyway.

Comment thread src/room/PCTransport.ts
@xianshijing-lk
xianshijing-lk merged commit 5db17af into main Jul 18, 2026
9 checks passed
@xianshijing-lk
xianshijing-lk deleted the sxian/CLT-3068/fix-initial-video-quality-blurriness-by-setting-x-google-start-bitrate branch July 18, 2026 01:34
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.

2 participants