Files
livekit/pkg/sfu/redreceiver_test.go
T
Raja SubramanianandClaude Opus 5.5 5111420e91 fix: copy RED history packets instead of keeping pointers (#4941)
RedReceiver kept pointers to the last forwarded packets for its
redundant blocks, but their payload is in the read buffer that
forwardRTP reuses for every packet, so the blocks carried the current
packet's bytes. Copy the history packets into slots RedReceiver owns.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 21:38:01 +05:30

508 lines
15 KiB
Go

// Copyright 2023 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 sfu
import (
"bytes"
"testing"
"github.com/pion/rtp"
"github.com/pion/webrtc/v4"
"github.com/stretchr/testify/require"
"github.com/livekit/livekit-server/pkg/sfu/buffer"
"github.com/livekit/mediatransportutil/pkg/bucket"
"github.com/livekit/protocol/logger"
)
const (
tsStep = uint32(48000 / 1000 * 10)
)
type dummyDowntrack struct {
TrackSender
lastReceivedPkt *rtp.Packet
receivedPkts []*rtp.Packet
}
func (dt *dummyDowntrack) WriteRTP(p *buffer.ExtPacket, _ int32) int32 {
dt.lastReceivedPkt = p.Packet
dt.receivedPkts = append(dt.receivedPkts, p.Packet)
return 1
}
func TestRedReceiver(t *testing.T) {
dt := &dummyDowntrack{TrackSender: &DownTrack{}}
t.Run("normal", func(t *testing.T) {
w := &WebRTCReceiver{
ReceiverBase: &ReceiverBase{
params: ReceiverBaseParams{
Kind: webrtc.RTPCodecTypeAudio,
Logger: logger.GetLogger(),
},
isRED: true,
},
}
require.Equal(t, w.GetRedReceiver(), w.ReceiverBase)
w.isRED = false
red := w.GetRedReceiver().(*RedReceiver)
require.NotNil(t, red)
require.NoError(t, red.AddDownTrack(dt))
header := rtp.Header{SequenceNumber: 65534, Timestamp: (uint32(1) << 31) - 2*tsStep, PayloadType: 111}
expectPkt := make([]*rtp.Packet, 0, maxRedCount+1)
for _, pkt := range generatePkts(header, 10, tsStep) {
expectPkt = append(expectPkt, pkt)
if len(expectPkt) > maxRedCount+1 {
expectPkt = expectPkt[1:]
}
red.ForwardRTP(&buffer.ExtPacket{
Packet: pkt,
}, 0)
verifyRedEncodings(t, dt.lastReceivedPkt, expectPkt)
}
})
t.Run("packet lost and jump", func(t *testing.T) {
w := &WebRTCReceiver{
ReceiverBase: &ReceiverBase{
params: ReceiverBaseParams{
Kind: webrtc.RTPCodecTypeAudio,
Logger: logger.GetLogger(),
},
},
}
red := w.GetRedReceiver().(*RedReceiver)
require.NoError(t, red.AddDownTrack(dt))
header := rtp.Header{SequenceNumber: 65534, Timestamp: (uint32(1) << 31) - 2*tsStep, PayloadType: 111}
expectPkt := make([]*rtp.Packet, 0, maxRedCount+1)
for i := range 10 {
if i%2 == 0 {
header.SequenceNumber++
header.Timestamp += tsStep
expectPkt = append(expectPkt, nil)
continue
}
hbuf, _ := header.Marshal()
pkt1 := &rtp.Packet{
Header: header,
Payload: hbuf,
}
expectPkt = append(expectPkt, pkt1)
if len(expectPkt) > maxRedCount+1 {
expectPkt = expectPkt[len(expectPkt)-maxRedCount-1:]
}
red.ForwardRTP(&buffer.ExtPacket{
Packet: pkt1,
}, 0)
verifyRedEncodings(t, dt.lastReceivedPkt, expectPkt)
header.SequenceNumber++
header.Timestamp += tsStep
}
// jump
header.SequenceNumber += 10
header.Timestamp += 10 * tsStep
expectPkt = expectPkt[:0]
for _, pkt := range generatePkts(header, 3, tsStep) {
expectPkt = append(expectPkt, pkt)
if len(expectPkt) > maxRedCount+1 {
expectPkt = expectPkt[len(expectPkt)-maxRedCount-1:]
}
red.ForwardRTP(&buffer.ExtPacket{
Packet: pkt,
}, 0)
verifyRedEncodings(t, dt.lastReceivedPkt, expectPkt)
}
})
t.Run("unorder and repeat", func(t *testing.T) {
w := &WebRTCReceiver{
ReceiverBase: &ReceiverBase{
params: ReceiverBaseParams{
Kind: webrtc.RTPCodecTypeAudio,
Logger: logger.GetLogger(),
},
},
}
red := w.GetRedReceiver().(*RedReceiver)
require.NoError(t, red.AddDownTrack(dt))
header := rtp.Header{SequenceNumber: 65534, Timestamp: (uint32(1) << 31) - 2*tsStep, PayloadType: 111}
var prevPkts []*rtp.Packet
for _, pkt := range generatePkts(header, 10, tsStep) {
red.ForwardRTP(&buffer.ExtPacket{
Packet: pkt,
}, 0)
prevPkts = append(prevPkts, pkt)
}
// old unorder data don't have red records
expectPkt := prevPkts[len(prevPkts)-3 : len(prevPkts)-2]
red.ForwardRTP(&buffer.ExtPacket{
Packet: expectPkt[0],
}, 0)
verifyRedEncodings(t, dt.lastReceivedPkt, expectPkt)
// repeat packet only have 1 red records
expectPkt = prevPkts[len(prevPkts)-2:]
red.ForwardRTP(&buffer.ExtPacket{
Packet: expectPkt[1],
}, 0)
verifyRedEncodings(t, dt.lastReceivedPkt, expectPkt)
})
t.Run("encoding exceed space", func(t *testing.T) {
w := &WebRTCReceiver{
ReceiverBase: &ReceiverBase{
params: ReceiverBaseParams{
Kind: webrtc.RTPCodecTypeAudio,
Logger: logger.GetLogger(),
},
isRED: true,
},
}
require.Equal(t, w.GetRedReceiver(), w.ReceiverBase)
w.isRED = false
red := w.GetRedReceiver().(*RedReceiver)
require.NotNil(t, red)
require.NoError(t, red.AddDownTrack(dt))
header := rtp.Header{SequenceNumber: 65534, Timestamp: (uint32(1) << 31) - 2*tsStep, PayloadType: 111}
expectPkt := make([]*rtp.Packet, 0, maxRedCount+1)
for _, pkt := range generatePkts(header, 10, tsStep) {
// make sure red encodings don't have enough space to encoding redundant packet
pkt.Payload = make([]byte, 1000)
expectPkt = append(expectPkt[:0], pkt)
red.ForwardRTP(&buffer.ExtPacket{
Packet: pkt,
}, 0)
verifyRedEncodings(t, dt.lastReceivedPkt, expectPkt)
}
})
t.Run("large timestamp gap", func(t *testing.T) {
w := &WebRTCReceiver{
ReceiverBase: &ReceiverBase{
params: ReceiverBaseParams{
Kind: webrtc.RTPCodecTypeAudio,
Logger: logger.GetLogger(),
},
isRED: true,
},
}
require.Equal(t, w.GetRedReceiver(), w.ReceiverBase)
w.isRED = false
red := w.GetRedReceiver().(*RedReceiver)
require.NotNil(t, red)
require.NoError(t, red.AddDownTrack(dt))
header := rtp.Header{SequenceNumber: 65534, Timestamp: (uint32(1) << 31) - 2*tsStep, PayloadType: 111}
// first few packets normal
expectPkt := make([]*rtp.Packet, 0, maxRedCount+1)
for _, pkt := range generatePkts(header, 4, tsStep) {
expectPkt = append(expectPkt, pkt)
if len(expectPkt) > maxRedCount+1 {
expectPkt = expectPkt[1:]
}
red.ForwardRTP(&buffer.ExtPacket{
Packet: pkt,
}, 0)
verifyRedEncodings(t, dt.lastReceivedPkt, expectPkt)
}
// and then a few packets with a large timestamp jump, should contain only primary
for _, pkt := range generatePkts(header, 4, 40*tsStep) {
red.ForwardRTP(&buffer.ExtPacket{
Packet: pkt,
}, 0)
verifyRedEncodings(t, dt.lastReceivedPkt, []*rtp.Packet{pkt})
}
})
}
// reads every packet into one reused buffer, the way ReceiverBase.forwardRTP does
func TestRedReceiverReusedReadBuffer(t *testing.T) {
opus := webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 2},
PayloadType: 111,
}
buff := buffer.NewBuffer(0x5678, 500, 200)
require.NoError(t, buff.Bind(webrtc.RTPParameters{Codecs: []webrtc.RTPCodecParameters{opus}}, opus.RTPCodecCapability, 0))
w := &WebRTCReceiver{
ReceiverBase: &ReceiverBase{
params: ReceiverBaseParams{
Kind: webrtc.RTPCodecTypeAudio,
Logger: logger.GetLogger(),
},
},
}
red := w.GetRedReceiver().(*RedReceiver)
dt := &dummyDowntrack{TrackSender: &DownTrack{}}
require.NoError(t, red.AddDownTrack(dt))
var payloads [][]byte
pktBuf := make([]byte, bucket.RTPMaxPktSize)
for i := range 6 {
payload := bytes.Repeat([]byte{byte(0x10 * (i + 1))}, 60)
payloads = append(payloads, payload)
pkt := rtp.Packet{
Header: rtp.Header{Version: 2, PayloadType: 111, SequenceNumber: uint16(100 + i), Timestamp: uint32(960 * i), SSRC: 0x5678},
Payload: payload,
}
raw, err := pkt.Marshal()
require.NoError(t, err)
_, err = buff.Write(raw)
require.NoError(t, err)
extPkt, err := buff.ReadExtended(pktBuf)
require.NoError(t, err)
red.ForwardRTP(extPkt, 0)
buffer.ReleaseExtPacket(extPkt)
}
// the last RED packet carries the two packets before it as redundant blocks
blocks, err := extractPktsFromRed(dt.lastReceivedPkt, 0xFF)
require.NoError(t, err)
require.Len(t, blocks, 3)
for i, block := range blocks {
require.Equal(t, payloads[3+i], block.Payload)
}
}
func verifyRedEncodings(t *testing.T, red *rtp.Packet, redPkts []*rtp.Packet) {
solidPkts := make([]*rtp.Packet, 0, len(redPkts))
for _, pkt := range redPkts {
if pkt != nil {
solidPkts = append(solidPkts, pkt)
}
}
pktsFromRed, err := extractPktsFromRed(red, 0xFF)
require.NoError(t, err)
require.Len(t, pktsFromRed, len(solidPkts))
for i, pkt := range pktsFromRed {
verifyEncodingEqual(t, pkt, solidPkts[i])
}
}
func verifyPktsEqual(t *testing.T, p1s, p2s []*rtp.Packet) {
require.Len(t, p1s, len(p2s))
for i, pkt := range p1s {
verifyEncodingEqual(t, pkt, p2s[i])
}
}
func verifyEncodingEqual(t *testing.T, p1, p2 *rtp.Packet) {
require.Equal(t, p1.Header.Timestamp, p2.Header.Timestamp)
require.Equal(t, p1.PayloadType, p2.PayloadType)
require.EqualValues(t, p1.Payload, p2.Payload, "seq1 %s", p1.SequenceNumber)
}
func generatePkts(header rtp.Header, count int, tsStep uint32) []*rtp.Packet {
pkts := make([]*rtp.Packet, 0, count)
for range count {
hbuf, _ := header.Marshal()
pkts = append(pkts, &rtp.Packet{
Header: header,
Payload: hbuf,
})
header.SequenceNumber++
header.Timestamp += tsStep
}
return pkts
}
func generateRedPkts(t *testing.T, pkts []*rtp.Packet, redCount int) []*rtp.Packet {
redPkts := make([]*rtp.Packet, 0, len(pkts))
for i, pkt := range pkts {
encodingPkts := make([]*rtp.Packet, 0, redCount)
for j := i - redCount; j < i; j++ {
if j < 0 {
continue
}
encodingPkts = append(encodingPkts, pkts[j])
}
buf := make([]byte, mtuSize)
redPkt := *pkt
redPkt.PayloadType = opusRedPT
encoded, err := encodeRedForPrimary(encodingPkts, pkt, buf)
require.NoError(t, err)
redPkt.Payload = buf[:encoded]
redPkts = append(redPkts, &redPkt)
}
return redPkts
}
func testRedRedPrimaryReceiver(t *testing.T, maxPktCount, redCount int, sendPktIdx, expectPktIdx []int) {
dt := &dummyDowntrack{TrackSender: &DownTrack{}}
w := &WebRTCReceiver{
ReceiverBase: &ReceiverBase{
params: ReceiverBaseParams{
Kind: webrtc.RTPCodecTypeAudio,
Logger: logger.GetLogger(),
Codec: webrtc.RTPCodecParameters{PayloadType: opusRedPT, RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/red"}},
},
},
}
require.Equal(t, w.GetPrimaryReceiverForRed(), w.ReceiverBase)
w.isRED = true
red := w.GetPrimaryReceiverForRed().(*RedPrimaryReceiver)
require.NotNil(t, red)
require.NoError(t, red.AddDownTrack(dt))
header := rtp.Header{SequenceNumber: 65530, Timestamp: (uint32(1) << 31) - 2*tsStep, PayloadType: 111}
primaryPkts := generatePkts(header, maxPktCount, tsStep)
redPkts := generateRedPkts(t, primaryPkts, redCount)
for _, i := range sendPktIdx {
red.ForwardRTP(&buffer.ExtPacket{
Packet: redPkts[i],
}, 0)
}
expectPkts := make([]*rtp.Packet, 0, len(expectPktIdx))
for _, i := range expectPktIdx {
expectPkts = append(expectPkts, primaryPkts[i])
}
verifyPktsEqual(t, expectPkts, dt.receivedPkts)
}
func TestRedPrimaryReceiver(t *testing.T) {
w := &WebRTCReceiver{
ReceiverBase: &ReceiverBase{
params: ReceiverBaseParams{
Kind: webrtc.RTPCodecTypeAudio,
Logger: logger.GetLogger(),
},
},
}
require.Equal(t, w.GetPrimaryReceiverForRed(), w.ReceiverBase)
w.isRED = true
red := w.GetPrimaryReceiverForRed().(*RedPrimaryReceiver)
require.NotNil(t, red)
t.Run("packet should send only once", func(t *testing.T) {
maxPktCount := 19
var sendPktIndex []int
for i := range maxPktCount {
sendPktIndex = append(sendPktIndex, i)
}
testRedRedPrimaryReceiver(t, maxPktCount, maxRedCount, sendPktIndex, sendPktIndex)
})
t.Run("packet duplicate and unorder", func(t *testing.T) {
maxPktCount := 19
var sendPktIndex []int
for i := range maxPktCount {
sendPktIndex = append(sendPktIndex, i)
if i > 0 {
sendPktIndex = append(sendPktIndex, i-1)
}
sendPktIndex = append(sendPktIndex, i)
}
testRedRedPrimaryReceiver(t, maxPktCount, maxRedCount, sendPktIndex, sendPktIndex)
})
t.Run("full recover", func(t *testing.T) {
maxPktCount := 19
var sendPktIndex, recvPktIndex []int
for i := range maxPktCount {
recvPktIndex = append(recvPktIndex, i)
// drop packets covered by red encoding
if i%(maxRedCount+1) != 0 {
continue
}
sendPktIndex = append(sendPktIndex, i)
}
testRedRedPrimaryReceiver(t, maxPktCount, maxRedCount, sendPktIndex, recvPktIndex)
})
t.Run("lost 2 but red recover 1", func(t *testing.T) {
maxPktCount := 19
sendPktIndex := []int{0, 3, 6, 9, 12}
recvPktIndex := []int{0, 2, 3, 5, 6, 8, 9, 11, 12}
testRedRedPrimaryReceiver(t, maxPktCount, 1, sendPktIndex, recvPktIndex)
})
t.Run("part recover and long jump", func(t *testing.T) {
maxPktCount := 50
sendPktIndex := []int{0, 5, 12, 21 /*long jump*/, 24, 27}
recvPktIndex := []int{0, 3, 4, 5, 10, 11, 12, 19, 20, 21, 22, 23, 24, 25, 26, 27}
testRedRedPrimaryReceiver(t, maxPktCount, maxRedCount, sendPktIndex, recvPktIndex)
})
t.Run("unorder", func(t *testing.T) {
maxPktCount := 50
sendPktIndex := []int{20, 10 /*unorder can't recover*/, 25, 23, 34}
recvPktIndex := []int{20, 10, 23, 24, 25, 21, 22, 23, 32, 33, 34}
testRedRedPrimaryReceiver(t, maxPktCount, maxRedCount, sendPktIndex, recvPktIndex)
})
t.Run("mixed primary codec", func(t *testing.T) {
dt := &dummyDowntrack{TrackSender: &DownTrack{}}
w := &WebRTCReceiver{
ReceiverBase: &ReceiverBase{
params: ReceiverBaseParams{
Kind: webrtc.RTPCodecTypeAudio,
Logger: logger.GetLogger(),
Codec: webrtc.RTPCodecParameters{PayloadType: opusRedPT, RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "audio/red"}},
},
},
}
require.Equal(t, w.GetPrimaryReceiverForRed(), w.ReceiverBase)
w.isRED = true
red := w.GetPrimaryReceiverForRed().(*RedPrimaryReceiver)
require.NotNil(t, red)
require.NoError(t, red.AddDownTrack(dt))
primaryPkt := &rtp.Packet{
Header: rtp.Header{SequenceNumber: 65530, Timestamp: (uint32(1) << 31) - 2*tsStep, PayloadType: 111},
Payload: []byte{1, 3, 5, 7, 9},
}
red.ForwardRTP(&buffer.ExtPacket{
Packet: primaryPkt,
}, 0)
verifyPktsEqual(t, []*rtp.Packet{primaryPkt}, dt.receivedPkts)
})
}
func TestExtractPrimaryEncodingForRED(t *testing.T) {
header := rtp.Header{SequenceNumber: 65530, Timestamp: (uint32(1) << 31) - 2*tsStep, PayloadType: 111}
pkts := generatePkts(header, 10, tsStep)
redPkts := generateRedPkts(t, pkts, 2)
primaryPkts := make([]*rtp.Packet, 0, len(redPkts))
for _, redPkt := range redPkts {
payload, err := extractPrimaryEncodingForRED(redPkt.Payload)
require.NoError(t, err)
primaryHeader := redPkt.Header
primaryHeader.PayloadType = 111
primaryPkts = append(primaryPkts, &rtp.Packet{
Header: primaryHeader,
Payload: payload,
})
}
verifyPktsEqual(t, pkts, primaryPkts)
}