Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -437,13 +437,17 @@ fun ensureVideoDDExtensionForSVC(mediaDesc: MediaDescription) {
}
}

/* The svc codec (av1/vp9) would use a very low bitrate at the beginning and
increase slowly by the bandwidth estimator until it reach the target bitrate. The
process commonly cost more than 10 seconds cause subscriber will get blur video at
the first few seconds. So we use a 70% of target bitrate here as the start bitrate to
eliminate this issue.
*/
private const val startBitrateForSVC = 0.7
/*
* Video codecs use a very low bitrate at the beginning and increase slowly by
* the bandwidth estimator until they reach the target bitrate. The process commonly
* costs more than 10 seconds causing subscribers to get blurry video at the first
* few seconds. We use x-google-start-bitrate to hint the BWE to start higher.
*
* Why 90%: Gives ~10% headroom for bandwidth estimation while starting close to target.
* Why same for all codecs: Target bitrate already accounts for codec efficiency
* (e.g., users set lower targets for VP9/AV1 knowing they're more efficient).
*/
private const val startBitrateMultiplier = 0.9

/**
* @suppress
Expand Down Expand Up @@ -476,7 +480,7 @@ fun ensureCodecBitrates(
fmtpFound = true
var newFmtpConfig = fmtp.config
if (!fmtp.config.contains("x-google-start-bitrate")) {
newFmtpConfig = "$newFmtpConfig;x-google-start-bitrate=${(trackBr.maxBitrate * startBitrateForSVC).roundToLong()}"
newFmtpConfig = "$newFmtpConfig;x-google-start-bitrate=${(trackBr.maxBitrate * startBitrateMultiplier).roundToLong()}"
}
if (!fmtp.config.contains("x-google-max-bitrate")) {
newFmtpConfig = "$newFmtpConfig;x-google-max-bitrate=${trackBr.maxBitrate}"
Expand All @@ -492,7 +496,7 @@ fun ensureCodecBitrates(
media.addAttribute(
SdpFmtp(
payload = codecPayload,
config = "x-google-start-bitrate=${trackBr.maxBitrate * startBitrateForSVC};" +
config = "x-google-start-bitrate=${trackBr.maxBitrate * startBitrateMultiplier};" +
"x-google-max-bitrate=${trackBr.maxBitrate}",
).toAttributeField(),
)
Expand All @@ -506,6 +510,15 @@ internal fun isSVCCodec(codec: String?): Boolean {
"vp9".equals(codec, ignoreCase = true))
}

internal fun isVideoCodec(codec: String?): Boolean {
return codec != null &&
("vp8".equals(codec, ignoreCase = true) ||
"vp9".equals(codec, ignoreCase = true) ||
"av1".equals(codec, ignoreCase = true) ||
"h264".equals(codec, ignoreCase = true) ||
"h265".equals(codec, ignoreCase = true))
}

/**
* @suppress
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import io.livekit.android.room.Room
import io.livekit.android.room.TrackBitrateInfo
import io.livekit.android.room.datastream.outgoing.OutgoingDataStreamManager
import io.livekit.android.room.isSVCCodec
import io.livekit.android.room.isVideoCodec
import io.livekit.android.room.rpc.RpcClientManager
import io.livekit.android.room.rpc.RpcManager
import io.livekit.android.room.rpc.RpcServerManager
Expand Down Expand Up @@ -697,9 +698,9 @@ internal constructor(
track.statsGetter = engine.createStatsGetter(transceiver.sender)

val finalOptions = options
// Handle trackBitrates
// Handle trackBitrates - apply start bitrate for all video codecs to prevent initial blurriness
if (encodings.isNotEmpty()) {
if (finalOptions is VideoTrackPublishOptions && isSVCCodec(finalOptions.videoCodec) && encodings.firstOrNull()?.maxBitrateBps != null) {
if (finalOptions is VideoTrackPublishOptions && isVideoCodec(finalOptions.videoCodec) && encodings.firstOrNull()?.maxBitrateBps != null) {

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.

Switching this gate from isSVCCodec to isVideoCodec pulls simulcast codecs (VP8 by default, plus H264) into this path, but maxBitrate is still taken from encodings.first(). For simulcast, computeVideoEncodings adds encodings smallest-to-largest (the sortedByDescending { calculateScaleDown } ordering), so encodings.first() is the lowest layer. A default 16:9 publish starts at H180 = 160 kbps. registerTrackBitrateInfo feeds that into ensureCodecBitrates, which writes a codec-level x-google-start-bitrate/x-google-max-bitrate from the low-layer value, capping the full-resolution layer far below its target. SVC kept a single full-bitrate encoding, which is why this was SVC-only before. For simulcast you'd need the top layer's bitrate (or to skip the codec-level cap).

engine.registerTrackBitrateInfo(
cid = cid,
TrackBitrateInfo(
Expand Down Expand Up @@ -1481,7 +1482,8 @@ data class VideoTrackPublishDefaults(
override val videoCodec: String = VideoCodec.VP8.codecName,
override val scalabilityMode: String? = null,
override val backupCodec: BackupVideoCodec? = null,
override val degradationPreference: RtpParameters.DegradationPreference? = null,
// Default to MAINTAIN_RESOLUTION to prevent initial video blurriness
override val degradationPreference: RtpParameters.DegradationPreference? = RtpParameters.DegradationPreference.MAINTAIN_RESOLUTION,

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.

This flips the default for every video publisher, not just the slow-ramp case. MAINTAIN_RESOLUTION holds resolution by dropping framerate under congestion, which suits screen share but turns camera video into low/stuttering framerate on constrained networks; WebRTC's default for camera content is maintain-framerate. The same change is in VideoTrackPublishOptions below, and the KDoc on the abstract degradationPreference still reads "null value indicates default value (maintain framerate)", which now contradicts the default. Consider scoping MAINTAIN_RESOLUTION to screen share rather than making it the global default, and updating the KDoc.

override val simulcastLayers: List<VideoPreset>? = null,
) : BaseVideoTrackPublishOptions()

Expand All @@ -1494,7 +1496,8 @@ data class VideoTrackPublishOptions(
override val backupCodec: BackupVideoCodec? = null,
override val source: Track.Source? = null,
override val stream: String? = null,
override val degradationPreference: RtpParameters.DegradationPreference? = null,
// Default to MAINTAIN_RESOLUTION to prevent initial video blurriness
override val degradationPreference: RtpParameters.DegradationPreference? = RtpParameters.DegradationPreference.MAINTAIN_RESOLUTION,
override val simulcastLayers: List<VideoPreset>? = null,
) : BaseVideoTrackPublishOptions(), TrackPublishOptions {
constructor(
Expand Down
Loading
Loading