mirror of
https://github.com/livekit/livekit.git
synced 2026-10-15 17:40:16 +00:00
* sfu/buffer: parse the dependency descriptor without allocating Every VP9 and AV1 SVC packet allocated six objects in the dependency descriptor parse: the descriptor, a clone of the frame template with its three slices, a resolution copy, and a frame entity that took the address of a parameter. The pooled ExtDependencyDescriptor now owns the storage for the descriptor, the template and the resolution, with fixed arrays for the template's slices. The reader copies the matching template into that storage. Delta frame parse: 6 allocs, 176 B, 174 ns -> 0 allocs, 0 B, 122 ns. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * sfu/dd: never copy a parsed frame into a structure template A caller can hand Unmarshal a descriptor whose FrameDependencies points at one of the structure's templates. Copying the packet's custom fields into it would change the template for every later packet, so the reader allocates a fresh template in that case. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
237 lines
8.0 KiB
Go
237 lines
8.0 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)
|
|
}
|
|
|
|
func (f *ddTestFeeder) feed(
|
|
frameNumber uint16,
|
|
template *dd.FrameDependencyTemplate,
|
|
attachedStructure *dd.FrameDependencyStructure,
|
|
) (*ExtDependencyDescriptor, error) {
|
|
f.t.Helper()
|
|
|
|
ddVal := &dd.DependencyDescriptor{
|
|
FirstPacketInFrame: true,
|
|
LastPacketInFrame: true,
|
|
FrameNumber: frameNumber,
|
|
FrameDependencies: template,
|
|
AttachedStructure: attachedStructure,
|
|
}
|
|
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)
|
|
}
|
|
|
|
// 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)
|
|
}
|