mirror of
https://github.com/livekit/livekit.git
synced 2026-10-15 15:30:14 +00:00
* 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>
291 lines
9.9 KiB
Go
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)
|
|
}
|