From f3e9b688541557ebc7ea86cdaaa7241d90e1c4c2 Mon Sep 17 00:00:00 2001 From: Raja Subramanian Date: Wed, 4 Feb 2026 12:19:41 +0530 Subject: [PATCH] Do not increase max expected layer on track info update. (#4285) * Do not increase max expected layer on track info update. When max expected layer increases, the corresponding trackers are reset so that first packets from those layers can trigger a layer detected change enabling quick detection of layer start. A track info update changing max to what is in track info could set the max expected to be higher without resetting the tracker. And that would cause dynacast induced max layer change to miss tracker reset too. Sequence - dynacast sets max expected to 0 - track info update sets it to 2 - dynacast sets it to 1 --> this should have reset tracker on layer 1, but because it is less than current max (2), it is skipped. * thank you CodeRabbit * force update on start --- pkg/rtc/dynacast/dynacastmanagervideo.go | 5 ++- pkg/sfu/streamtrackermanager.go | 46 +++++++++++++----------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/pkg/rtc/dynacast/dynacastmanagervideo.go b/pkg/rtc/dynacast/dynacastmanagervideo.go index e2c871213..8178b4627 100644 --- a/pkg/rtc/dynacast/dynacastmanagervideo.go +++ b/pkg/rtc/dynacast/dynacastmanagervideo.go @@ -15,6 +15,7 @@ package dynacast import ( + "maps" "time" "github.com/bep/debounce" @@ -216,9 +217,7 @@ func (d *dynacastManagerVideo) update(force bool) { // commit change d.committedMaxSubscribedQuality = make(map[mime.MimeType]livekit.VideoQuality, len(d.maxSubscribedQuality)) - for mime, quality := range d.maxSubscribedQuality { - d.committedMaxSubscribedQuality[mime] = quality - } + maps.Copy(d.committedMaxSubscribedQuality, d.maxSubscribedQuality) d.enqueueSubscribedQualityChange() d.lock.Unlock() diff --git a/pkg/sfu/streamtrackermanager.go b/pkg/sfu/streamtrackermanager.go index 2bc246a53..7e0347906 100644 --- a/pkg/sfu/streamtrackermanager.go +++ b/pkg/sfu/streamtrackermanager.go @@ -16,7 +16,6 @@ package sfu import ( "slices" - "sort" "sync" "time" @@ -162,7 +161,8 @@ func NewStreamTrackerManager( s.trackerConfig = config.Video } - s.maxExpectedLayerFromTrackInfo() + s.maxExpectedLayer = buffer.InvalidLayerSpatial + s.maxExpectedLayerFromTrackInfo(true) if trackInfo.Type == livekit.TrackType_VIDEO { go s.bitrateReporter() @@ -333,8 +333,12 @@ func (s *StreamTrackerManager) RemoveAllTrackers() { s.trackers[layer] = nil } s.availableLayers = make([]int32, 0) - s.maxExpectedLayerFromTrackInfoLocked() + + s.maxExpectedLayer = buffer.InvalidLayerSpatial + s.maxExpectedLayerFromTrackInfoLocked(true) + s.paused = false + ddTracker := s.ddTracker s.ddTracker = nil s.lock.Unlock() @@ -382,7 +386,7 @@ func (s *StreamTrackerManager) IsPaused() bool { func (s *StreamTrackerManager) UpdateTrackInfo(ti *livekit.TrackInfo) { s.trackInfo.Store(utils.CloneProto(ti)) - s.maxExpectedLayerFromTrackInfo() + s.maxExpectedLayerFromTrackInfo(false) } func (s *StreamTrackerManager) SetMaxExpectedSpatialLayer(layer int32) int32 { @@ -488,7 +492,7 @@ func (s *StreamTrackerManager) getLayeredBitrateLocked() ([]int32, Bitrates) { tls = tracker.BitrateTemporalCumulative() } - for j := 0; j < len(br[i]); j++ { + for j := range len(br[i]) { br[i][j] = tls[j] } } @@ -515,20 +519,13 @@ func (s *StreamTrackerManager) getLayeredBitrateLocked() ([]int32, Bitrates) { func (s *StreamTrackerManager) addAvailableLayer(layer int32) { s.lock.Lock() - hasLayer := false - for _, l := range s.availableLayers { - if l == layer { - hasLayer = true - break - } - } - if hasLayer { + if slices.Contains(s.availableLayers, layer) { s.lock.Unlock() return } s.availableLayers = append(s.availableLayers, layer) - sort.Slice(s.availableLayers, func(i, j int) bool { return s.availableLayers[i] < s.availableLayers[j] }) + slices.Sort(s.availableLayers) // check if new layer is the max layer isMaxLayerChange := s.availableLayers[len(s.availableLayers)-1] == layer @@ -562,7 +559,7 @@ func (s *StreamTrackerManager) removeAvailableLayer(layer int32) { newLayers = append(newLayers, l) } } - sort.Slice(newLayers, func(i, j int) bool { return newLayers[i] < newLayers[j] }) + slices.Sort(newLayers) s.availableLayers = newLayers s.logger.Debugw( @@ -588,23 +585,30 @@ func (s *StreamTrackerManager) removeAvailableLayer(layer int32) { } } -func (s *StreamTrackerManager) maxExpectedLayerFromTrackInfo() { +func (s *StreamTrackerManager) maxExpectedLayerFromTrackInfo(force bool) { s.lock.Lock() defer s.lock.Unlock() - s.maxExpectedLayerFromTrackInfoLocked() + s.maxExpectedLayerFromTrackInfoLocked(force) } -func (s *StreamTrackerManager) maxExpectedLayerFromTrackInfoLocked() { - s.maxExpectedLayer = buffer.InvalidLayerSpatial +func (s *StreamTrackerManager) maxExpectedLayerFromTrackInfoLocked(force bool) { + maxExpectedLayer := buffer.InvalidLayerSpatial ti := s.trackInfo.Load() if ti != nil { for _, layer := range buffer.GetVideoLayersForMimeType(s.mimeType, ti) { - if layer.SpatialLayer > s.maxExpectedLayer { - s.maxExpectedLayer = layer.SpatialLayer + if layer.SpatialLayer > maxExpectedLayer { + maxExpectedLayer = layer.SpatialLayer } } } + + // when max expected is higher than current max, trackers are reset + // which allows a layer start to be detected on initial packets from that higher layer, + // so update max only on track info max being lower than current max + if force || maxExpectedLayer < s.maxExpectedLayer { + s.maxExpectedLayer = maxExpectedLayer + } } func (s *StreamTrackerManager) GetMaxTemporalLayerSeen() int32 {