Files
livekit/pkg/sfu/codecmunger/ddtemporal_test.go
T
Raja SubramanianandClaude Opus 5.5 93296ad4f6 sfu: filter VP9 and AV1 simulcast temporal layers with the dependency descriptor (#4991)
* sfu: filter VP9 and AV1 simulcast temporal layers with the dependency descriptor

Every simulcast codec gets an explicit temporal layer selector. With the
dependency descriptor, VP8, VP9 and AV1 filter temporal layers by frame and
forward the descriptor. Without it, a pass-through selector forwards every
temporal layer and reports the target as reached, so a pending target cannot
block allocation. VP8DD becomes the codec-neutral DDTemporal.

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

* sfu: VP8-only padding header in DDTemporal, structure gate on presence

DDTemporal returns the one byte VP8 payload descriptor for padding only for
VP8. The descriptor forwarder takes a structure from any packet that carries
one, as the parser accepts it only on the first packet of a frame.

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

---------

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

109 lines
4.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 codecmunger
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/livekit/livekit-server/pkg/sfu/buffer"
"github.com/livekit/livekit-server/pkg/sfu/testutils"
"github.com/livekit/protocol/codecs/mime"
"github.com/livekit/protocol/logger"
)
func vp8DDPacket(t *testing.T, frameNum uint64, tid int) *buffer.ExtPacket {
extPkt, err := testutils.GetTestExtPacketVP8DD(
&testutils.TestExtPacketParams{SequenceNumber: uint16(frameNum), PayloadSize: 20},
&testutils.TestDDParams{
FirstPacketInFrame: true,
ExtFrameNum: frameNum,
TemporalID: tid,
DecodeTargets: testutils.TestVP8DecodeTargets,
},
)
require.NoError(t, err)
return extPkt
}
func TestDDTemporalUpdateAndGet(t *testing.T) {
t.Run("filters by temporal layer, header unchanged", func(t *testing.T) {
v := NewDDTemporal(mime.MimeTypeVP8, logger.GetLogger())
inputSize, _, n, err := v.UpdateAndGet(vp8DDPacket(t, 0, 0), false, false, 0)
require.NoError(t, err)
require.Zero(t, inputSize)
require.Zero(t, n)
_, _, _, err = v.UpdateAndGet(vp8DDPacket(t, 1, 2), false, false, 0)
require.ErrorIs(t, err, ErrFilteredTemporalLayer)
})
t.Run("a gap does not exempt a filtered layer", func(t *testing.T) {
v := NewDDTemporal(mime.MimeTypeVP8, logger.GetLogger())
_, _, _, err := v.UpdateAndGet(vp8DDPacket(t, 0, 0), false, false, 0)
require.NoError(t, err)
for frameNum := uint64(2); frameNum < 6; frameNum++ {
_, _, _, err = v.UpdateAndGet(vp8DDPacket(t, frameNum, 1), false, true, 0)
require.ErrorIs(t, err, ErrFilteredTemporalLayer)
}
})
t.Run("every packet of a frame gets the decision of its first packet", func(t *testing.T) {
v := NewDDTemporal(mime.MimeTypeVP8, logger.GetLogger())
_, _, _, err := v.UpdateAndGet(vp8DDPacket(t, 1, 2), false, false, 0)
require.ErrorIs(t, err, ErrFilteredTemporalLayer)
_, _, _, err = v.UpdateAndGet(vp8DDPacket(t, 2, 1), false, false, 1)
require.NoError(t, err)
// the layer changed in between, late packets keep the decision of their frame
_, _, _, err = v.UpdateAndGet(vp8DDPacket(t, 1, 2), true, false, 2)
require.ErrorIs(t, err, ErrFilteredOutOfOrder)
_, _, _, err = v.UpdateAndGet(vp8DDPacket(t, 2, 1), true, false, 0)
require.NoError(t, err)
})
t.Run("late packet of a frame older than the window", func(t *testing.T) {
v := NewDDTemporal(mime.MimeTypeVP8, logger.GetLogger())
_, _, _, err := v.UpdateAndGet(vp8DDPacket(t, 100, 0), false, false, 2)
require.NoError(t, err)
_, _, _, err = v.UpdateAndGet(vp8DDPacket(t, 100-ddTemporalFrameWindow, 0), true, false, 2)
require.ErrorIs(t, err, ErrFilteredOutOfOrder)
})
t.Run("layer switch forgets decisions", func(t *testing.T) {
v := NewDDTemporal(mime.MimeTypeVP8, logger.GetLogger())
_, _, _, err := v.UpdateAndGet(vp8DDPacket(t, 1, 2), false, false, 0)
require.ErrorIs(t, err, ErrFilteredTemporalLayer)
v.UpdateOffsets(vp8DDPacket(t, 1, 0))
_, _, _, err = v.UpdateAndGet(vp8DDPacket(t, 1, 2), false, false, 2)
require.NoError(t, err)
})
t.Run("padding", func(t *testing.T) {
v := NewDDTemporal(mime.MimeTypeVP8, logger.GetLogger())
hdr, err := v.UpdateAndGetPadding(true)
require.NoError(t, err)
require.Equal(t, []byte{0x10}, hdr)
// other codecs have no payload descriptor to prepend
for _, mimeType := range []mime.MimeType{mime.MimeTypeVP9, mime.MimeTypeAV1} {
hdr, err = NewDDTemporal(mimeType, logger.GetLogger()).UpdateAndGetPadding(true)
require.NoError(t, err)
require.Nil(t, hdr)
}
})
}