Files
livekit/pkg/sfu/videolayerselector/simulcast.go
cfedcc71d0 feat: acquire requested video layer directly at HIGH quality by default (#4595)
* feat: acquire requested video layer directly at HIGH quality by default

Two changes that together remove the visible low->high quality ramp for a new
subscriber (both publisher-first and subscriber-first join orders):

1. Default a subscriber's initial video quality to HIGH on bind instead of LOW
   for adaptive stream, so the subscribed max layer is the top layer. Adaptive
   stream clients can still scale down afterwards based on viewport.

2. On initial layer acquisition the forwarder/selector latch directly onto the
   allocator's target (the requested top layer) instead of opportunistically
   latching onto the first lower key frame that arrives. A short
   initial-acquisition grace aims the target at the requested layer; if it does
   not show up in time, the target falls back to the highest layer seen so
   acquisition never stalls.

Always on - no configuration flag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat: gate start-at-desired-quality behind EnableStartAtDesiredQuality flag

Put the "acquire requested video layer directly at HIGH quality" behavior
behind a per-subscriber EnableStartAtDesiredQuality flag (default off, so
the original low->high ramp-up is restored unless enabled).

Plumbed from config.RTC.EnableStartAtDesiredQuality through ParticipantParams
-> SubscribedTrack/DownTrack -> Forwarder -> simulcast selector, gating all
three behavior changes: the HIGH default on bind, the forwarder's
initial-acquisition grace, and the selector's direct-latch-onto-target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* remove config.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-19 20:12:54 +08:00

162 lines
4.4 KiB
Go

// Copyright 2023 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.
package videolayerselector
import (
"github.com/livekit/livekit-server/pkg/sfu/buffer"
"github.com/livekit/protocol/logger"
)
type Simulcast struct {
*Base
}
func NewSimulcast(logger logger.Logger) *Simulcast {
return &Simulcast{
Base: NewBase(logger),
}
}
func NewSimulcastFromOther(vls VideoLayerSelector) *Simulcast {
switch vls := vls.(type) {
case *Null:
return &Simulcast{
Base: vls.Base,
}
case *Simulcast:
return &Simulcast{
Base: vls.Base,
}
case *DependencyDescriptor:
return &Simulcast{
Base: vls.Base,
}
case *VP9:
return &Simulcast{
Base: vls.Base,
}
default:
return nil
}
}
func (s *Simulcast) IsOvershootOkay() bool {
return true
}
func (s *Simulcast) Select(extPkt *buffer.ExtPacket, layer int32) (result VideoLayerSelectorResult) {
populateSwitches := func(isActive bool, reason string) {
result.IsSwitching = true
if !isActive {
result.IsResuming = true
}
if reason != "" {
s.logger.Debugw(
reason,
"previous", s.previousLayer,
"current", s.currentLayer,
"previousTarget", s.previousTargetLayer,
"target", s.targetLayer,
"max", s.maxLayer,
"layer", layer,
"req", s.requestSpatial,
"maxSeen", s.maxSeenLayer,
"feed", extPkt.Packet.SSRC,
)
}
}
if s.currentLayer.Spatial != s.targetLayer.Spatial {
currentLayer := s.currentLayer
// Two things to check when not locked to target
// 1. Opportunistic layer upgrade - needs a key frame
// 2. Need to downgrade - needs a key frame
isActive := s.currentLayer.IsValid()
found := false
reason := ""
if extPkt.IsKeyFrame {
if s.enableStartAtDesiredQuality && !isActive {
// Initial acquisition: latch directly onto the target layer instead of
// opportunistically latching onto the first key frame of any lower layer that
// happens to arrive first. This avoids a visible low-quality -> high-quality ramp
// (e.g. briefly decoding layer 0 before settling on a requested layer 2) for a
// subscriber that requested the higher layer.
//
// The target is chosen by the allocator: during the initial-acquisition grace it
// points at the requested layer (so we wait for it); if that layer never shows
// up the grace expires and the allocator drops the target to the highest layer
// actually seen, so we always end up latching onto a layer that is flowing.
if layer == s.targetLayer.Spatial {
reason = "acquiring target layer"
found = true
}
} else {
// default: opportunistically latch on to / step towards the target layer
if layer > s.currentLayer.Spatial && layer <= s.targetLayer.Spatial {
reason = "upgrading layer"
found = true
}
if layer < s.currentLayer.Spatial && layer >= s.targetLayer.Spatial {
reason = "downgrading layer"
found = true
}
}
if found {
currentLayer.Spatial = layer
currentLayer.Temporal = extPkt.VideoLayer.Temporal
}
}
if found {
s.previousLayer = s.currentLayer
s.currentLayer = currentLayer
s.previousTargetLayer = s.targetLayer
if s.currentLayer.Spatial >= s.maxLayer.Spatial || s.currentLayer.Spatial == s.maxSeenLayer.Spatial {
s.targetLayer.Spatial = s.currentLayer.Spatial
}
populateSwitches(isActive, reason)
}
}
// if locked to higher than max layer due to overshoot, check if it can be dialed back
if s.currentLayer.Spatial > s.maxLayer.Spatial && layer <= s.maxLayer.Spatial && extPkt.IsKeyFrame {
s.previousLayer = s.currentLayer
s.currentLayer.Spatial = layer
s.previousTargetLayer = s.targetLayer
if s.currentLayer.Spatial >= s.maxLayer.Spatial || s.currentLayer.Spatial == s.maxSeenLayer.Spatial {
s.targetLayer.Spatial = layer
}
populateSwitches(true, "adjusting overshoot")
}
result.RTPMarker = extPkt.Packet.Marker
result.IsSelected = layer == s.currentLayer.Spatial
result.IsRelevant = false
return
}