Files
livekit/pkg/sfu/buffer/dependencydescriptorparser_test.go
T
Raja SubramanianandClaude Opus 5.5 099e03fdf8 sfu: forward the dependency descriptor for VP8 simulcast (#4987)
* sfu: forward the dependency descriptor for VP8 simulcast

libwebrtc sends VP8 with a one byte payload descriptor when it sends the
dependency descriptor, so a VP8 subscriber gets neither picture IDs nor the
descriptor and finds frame references by sequence number alone. Under loss
with temporal filtering it stalls until a key frame.

DDForwarder rewrites the publisher's descriptor for each forwarded packet:
frame numbers continue across simulcast layer switches and stream restarts,
the structure goes out with the current layer's key frame, and the active
decode targets follow the forwarded temporal layer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* sfu: keep VP8 descriptor frame numbers from going back

Rebase frame numbers only when a new stream would go back past the last
number sent: on a layer switch, a stream restart (also when it starts with a
delta frame) and a new forwarder on the same downtrack, which now carries
over the last number sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* sfu: set VP8 descriptor decode targets from the packet's layer

The active decode targets use the temporal layer the packet is forwarded at,
not the selector's next layer on the last packet before a down switch. They
also go out on key frames, combined with the publisher's own mask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* sfu: carry the current active decode targets on every DD packet

The parser puts the stream's current mask on every packet, so a mask change
on a dropped packet still reaches the VP8 descriptor forwarder, which no
longer tracks the publisher's mask itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* sfu: take the first DD active decode targets mask at any sequence number

A publisher whose first packet has RTP sequence number 0 never had its key
frame's mask applied, so every packet reported no active decode targets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 12:08:49 +05:30

291 lines
9.9 KiB
Go

// Copyright 2026 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 buffer
import (
"testing"
"github.com/pion/rtp"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/logger"
dd "github.com/livekit/livekit-server/pkg/sfu/rtpextension/dependencydescriptor"
)
const ddTestExtID = uint8(1)
// single spatial layer, single temporal layer, one decode target, one chain
func newL1T1Structure(structureID int) *dd.FrameDependencyStructure {
return &dd.FrameDependencyStructure{
StructureId: structureID,
NumDecodeTargets: 1,
NumChains: 1,
DecodeTargetProtectedByChain: []int{0},
Templates: []*dd.FrameDependencyTemplate{
{ // key frame
DecodeTargetIndications: []dd.DecodeTargetIndication{dd.DecodeTargetSwitch},
ChainDiffs: []int{0},
},
{ // delta frame
DecodeTargetIndications: []dd.DecodeTargetIndication{dd.DecodeTargetRequired},
FrameDiffs: []int{1},
ChainDiffs: []int{1},
},
},
}
}
type ddTestFeeder struct {
t *testing.T
parser *DependencyDescriptorParser
// structure the writer marshals against, i. e. what the publisher last sent
structure *dd.FrameDependencyStructure
seq uint16
}
func newDDTestFeeder(t *testing.T) *ddTestFeeder {
return &ddTestFeeder{
t: t,
parser: NewDependencyDescriptorParser(ddTestExtID, logger.GetLogger(), func(int32, int32) {}, false),
}
}
// keyFrame sends a key frame carrying `structure`. Passing the same structure id as
// the previous key frame models a publisher that repeats an unchanged structure,
// which is what screen content with frequent key frames does.
func (f *ddTestFeeder) keyFrame(frameNumber uint16, structure *dd.FrameDependencyStructure) (*ExtDependencyDescriptor, error) {
f.structure = structure
return f.feed(frameNumber, structure.Templates[0], structure)
}
func (f *ddTestFeeder) deltaFrame(frameNumber uint16) (*ExtDependencyDescriptor, error) {
return f.feed(frameNumber, f.structure.Templates[1], nil)
}
// deltaFrameWithMask sends a delta frame that changes the active decode targets
func (f *ddTestFeeder) deltaFrameWithMask(frameNumber uint16, activeDecodeTargets uint32) (*ExtDependencyDescriptor, error) {
return f.feedWithMask(frameNumber, f.structure.Templates[1], nil, &activeDecodeTargets)
}
func (f *ddTestFeeder) feed(
frameNumber uint16,
template *dd.FrameDependencyTemplate,
attachedStructure *dd.FrameDependencyStructure,
) (*ExtDependencyDescriptor, error) {
return f.feedWithMask(frameNumber, template, attachedStructure, nil)
}
func (f *ddTestFeeder) feedWithMask(
frameNumber uint16,
template *dd.FrameDependencyTemplate,
attachedStructure *dd.FrameDependencyStructure,
activeDecodeTargets *uint32,
) (*ExtDependencyDescriptor, error) {
f.t.Helper()
ddVal := &dd.DependencyDescriptor{
FirstPacketInFrame: true,
LastPacketInFrame: true,
FrameNumber: frameNumber,
FrameDependencies: template,
AttachedStructure: attachedStructure,
ActiveDecodeTargetsBitmask: activeDecodeTargets,
}
buf, err := (&dd.DependencyDescriptorExtension{Descriptor: ddVal, Structure: f.structure}).Marshal()
require.NoError(f.t, err)
f.seq++
pkt := &rtp.Packet{Header: rtp.Header{SequenceNumber: f.seq}}
require.NoError(f.t, pkt.SetExtension(ddTestExtID, buf))
extDD, _, err := f.parser.Parse(pkt)
return extDD, err
}
// A key frame that repeats the current structure must not start dropping frames that
// precede it. Upstream advanced the drop threshold on every structure bearing key
// frame, so with frequent key frames (screen content) a late or retransmitted frame
// arriving after one was discarded as "earlier than current structure".
func TestDependencyDescriptorParserLateFrameAfterRepeatedStructure(t *testing.T) {
f := newDDTestFeeder(t)
structure := newL1T1Structure(0)
_, err := f.keyFrame(0, structure)
require.NoError(t, err)
_, err = f.deltaFrame(1)
require.NoError(t, err)
_, err = f.deltaFrame(3)
require.NoError(t, err)
// key frame repeating the same structure id
_, err = f.keyFrame(4, structure)
require.NoError(t, err)
// frame 2 finally shows up (reordered or retransmitted). The structure has not
// changed, so it is still decodable and must be forwarded.
extDD, err := f.deltaFrame(2)
require.NoError(t, err)
require.NotNil(t, extDD)
require.EqualValues(t, 2, extDD.ExtFrameNum)
}
// A key frame carrying a *different* structure does invalidate everything before it:
// earlier frames reference templates that no longer exist.
func TestDependencyDescriptorParserLateFrameAfterStructureChange(t *testing.T) {
f := newDDTestFeeder(t)
_, err := f.keyFrame(0, newL1T1Structure(0))
require.NoError(t, err)
_, err = f.deltaFrame(1)
require.NoError(t, err)
_, err = f.deltaFrame(3)
require.NoError(t, err)
_, err = f.keyFrame(4, newL1T1Structure(1))
require.NoError(t, err)
_, err = f.deltaFrame(2)
require.ErrorIs(t, err, ErrFrameEarlierThanKeyFrame)
}
// An out-of-order key frame must still be dropped: accepting it would regress
// structureExtFrameNum (ExtKeyFrameNum) and replay a stale structure update.
func TestDependencyDescriptorParserOutOfOrderKeyFrame(t *testing.T) {
f := newDDTestFeeder(t)
structure := newL1T1Structure(0)
_, err := f.keyFrame(0, structure)
require.NoError(t, err)
_, err = f.deltaFrame(1)
require.NoError(t, err)
_, err = f.keyFrame(4, structure)
require.NoError(t, err)
_, err = f.keyFrame(2, structure)
require.ErrorIs(t, err, ErrFrameEarlierThanKeyFrame)
}
// ExtKeyFrameNum keeps tracking every structure bearing key frame, unchanged by the
// split of the drop threshold into its own field.
func TestDependencyDescriptorParserExtKeyFrameNum(t *testing.T) {
f := newDDTestFeeder(t)
structure := newL1T1Structure(0)
extDD, err := f.keyFrame(0, structure)
require.NoError(t, err)
require.EqualValues(t, 0, extDD.ExtKeyFrameNum)
extDD, err = f.deltaFrame(1)
require.NoError(t, err)
require.EqualValues(t, 0, extDD.ExtKeyFrameNum)
// repeated structure still advances ExtKeyFrameNum
extDD, err = f.keyFrame(4, structure)
require.NoError(t, err)
require.EqualValues(t, 4, extDD.ExtKeyFrameNum)
extDD, err = f.deltaFrame(5)
require.NoError(t, err)
require.EqualValues(t, 4, extDD.ExtKeyFrameNum)
}
// every packet carries the stream's current active decode targets, also after the mask
// changed on a packet the forwarder dropped
func TestDependencyDescriptorParserActiveDecodeTargetsMask(t *testing.T) {
f := newDDTestFeeder(t)
structure := newL1T1Structure(0)
extDD, err := f.keyFrame(0, structure)
require.NoError(t, err)
require.EqualValues(t, 1, extDD.ActiveDecodeTargetsMask)
extDD, err = f.deltaFrameWithMask(1, 0)
require.NoError(t, err)
require.EqualValues(t, 0, extDD.ActiveDecodeTargetsMask)
extDD, err = f.deltaFrame(2)
require.NoError(t, err)
require.EqualValues(t, 0, extDD.ActiveDecodeTargetsMask)
extDD, err = f.keyFrame(3, structure)
require.NoError(t, err)
require.EqualValues(t, 1, extDD.ActiveDecodeTargetsMask)
}
// a first key frame at RTP sequence number 0 still makes its structure's decode targets
// active
func TestDependencyDescriptorParserActiveDecodeTargetsMaskAtSequenceZero(t *testing.T) {
f := newDDTestFeeder(t)
f.seq = 0xffff // the first packet goes out with sequence number 0
structure := newL1T1Structure(0)
extDD, err := f.keyFrame(0, structure)
require.NoError(t, err)
require.EqualValues(t, 1, extDD.ActiveDecodeTargetsMask)
extDD, err = f.deltaFrame(1)
require.NoError(t, err)
require.EqualValues(t, 1, extDD.ActiveDecodeTargetsMask)
}
// a pooled ExtDependencyDescriptor carries the storage for its descriptor, so parsing a delta
// frame must not allocate and a reused descriptor must not show the previous packet's values
func TestDependencyDescriptorParserNoAlloc(t *testing.T) {
f := newDDTestFeeder(t)
structure := newL1T1Structure(0)
structure.Resolutions = []dd.RenderResolution{{Width: 640, Height: 360}}
extDD, err := f.keyFrame(0, structure)
require.NoError(t, err)
require.Equal(t, 0, len(extDD.Descriptor.FrameDependencies.FrameDiffs))
require.Equal(t, dd.RenderResolution{Width: 640, Height: 360}, *extDD.Descriptor.Resolution)
ReleaseExtDependencyDescriptor(extDD)
extDD, err = f.deltaFrame(1)
require.NoError(t, err)
require.Equal(t, []int{1}, extDD.Descriptor.FrameDependencies.FrameDiffs)
require.Equal(t, []int{1}, extDD.Descriptor.FrameDependencies.ChainDiffs)
require.Nil(t, extDD.Descriptor.AttachedStructure)
ReleaseExtDependencyDescriptor(extDD)
// the parsed template is a copy, the structure's template is untouched
require.Equal(t, []int{1}, structure.Templates[1].FrameDiffs)
// marshal the packets first, only Parse runs under the allocation count
bufs := make([][]byte, 128)
for i := range bufs {
ddVal := &dd.DependencyDescriptor{
FirstPacketInFrame: true,
LastPacketInFrame: true,
FrameNumber: uint16(i + 2),
FrameDependencies: structure.Templates[1],
}
bufs[i], err = (&dd.DependencyDescriptorExtension{Descriptor: ddVal, Structure: structure}).Marshal()
require.NoError(t, err)
}
pkt := &rtp.Packet{Header: rtp.Header{SequenceNumber: f.seq}}
require.NoError(t, pkt.SetExtension(ddTestExtID, bufs[0]))
i := 0
allocs := testing.AllocsPerRun(100, func() {
pkt.SequenceNumber++
require.NoError(t, pkt.SetExtension(ddTestExtID, bufs[i]))
i++
extDD, _, err := f.parser.Parse(pkt)
require.NoError(t, err)
ReleaseExtDependencyDescriptor(extDD)
})
require.Zero(t, allocs)
}