sfu: use mediatransportutil/pkg/red in RedPrimaryReceiver (#5014)

RED depacketizing now lives in mediatransportutil so the SFU and
replay export share one implementation. Drop the local copies; the
encoder round-trip tests now exercise the shared package.
This commit is contained in:
David Colburn
2026-10-14 16:57:01 -04:00
committed by GitHub
parent 18df6ad4b6
commit 708b8ea6ba
4 changed files with 15 additions and 197 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ require (
github.com/jellydator/ttlcache/v3 v3.4.1
github.com/jxskiss/base62 v1.1.0
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731
github.com/livekit/mediatransportutil v0.0.0-20260821083140-f234b534b095
github.com/livekit/mediatransportutil v0.0.0-20261014192331-dd8e31a0adc0
github.com/livekit/protocol v1.52.2-0.20261012065025-9bb708cdb819
github.com/livekit/psrpc v0.8.2
github.com/mackerelio/go-osstat v0.2.8
+2 -2
View File
@@ -164,8 +164,8 @@ github.com/livekit/ice/v4 v4.4.0-warp.3 h1:iSAtEiLeCi/ZKEKOsRrDlBg/BRcyUKpV/NuAX
github.com/livekit/ice/v4 v4.4.0-warp.3/go.mod h1:obAyD+J+Hzs7QA7Y8YXHp5uIn6gb7z87pKedXZkrcFU=
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731 h1:9x+U2HGLrSw5ATTo469PQPkqzdoU7be46ryiCDO3boc=
github.com/livekit/mageutil v0.0.0-20250511045019-0f1ff63f7731/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ=
github.com/livekit/mediatransportutil v0.0.0-20260821083140-f234b534b095 h1:BcliKAXoMhl/nWmzQweQ5kmh4Qqagxl4s3Z5pvM/7AY=
github.com/livekit/mediatransportutil v0.0.0-20260821083140-f234b534b095/go.mod h1:o8CFmAdrVwzJNOCsQCLUzXRjokkufNshnQHOe4fRaqU=
github.com/livekit/mediatransportutil v0.0.0-20261014192331-dd8e31a0adc0 h1:BSLyHr3Lsl71bHGHsfiYNmbohugR2yoaf9HjAZeFJnc=
github.com/livekit/mediatransportutil v0.0.0-20261014192331-dd8e31a0adc0/go.mod h1:o8CFmAdrVwzJNOCsQCLUzXRjokkufNshnQHOe4fRaqU=
github.com/livekit/protocol v1.52.2-0.20261012065025-9bb708cdb819 h1:oZ/dI5RG22t0GalRDU1BJWCMtNAEQVPU+odrRgycKDE=
github.com/livekit/protocol v1.52.2-0.20261012065025-9bb708cdb819/go.mod h1:k7v95/DNhMkslMeBxdQ+lQ6gKc8hMNGYB7t0+6sEGU4=
github.com/livekit/psrpc v0.8.2 h1:7tbkhBJoYlK5Csfkdv1XYbZjyAQeaEZgupvIXNCv7Dc=
+4 -187
View File
@@ -15,9 +15,6 @@
package sfu
import (
"encoding/binary"
"errors"
"go.uber.org/atomic"
"github.com/pion/rtp"
@@ -25,17 +22,13 @@ import (
"github.com/livekit/livekit-server/pkg/sfu/buffer"
"github.com/livekit/livekit-server/pkg/sfu/utils"
"github.com/livekit/mediatransportutil/pkg/red"
"github.com/livekit/protocol/livekit"
"github.com/livekit/protocol/logger"
)
var _ REDTransformer = (*RedPrimaryReceiver)(nil)
var (
ErrIncompleteRedHeader = errors.New("incomplete red block header")
ErrIncompleteRedBlock = errors.New("incomplete red block payload")
)
type RedPrimaryReceiver struct {
TrackReceiver
downTrackSpreader *utils.DownTrackSpreader[TrackSender]
@@ -43,14 +36,10 @@ type RedPrimaryReceiver struct {
closed atomic.Bool
redPT uint8
firstPktReceived bool
lastSeq uint16
depacketizer red.Depacketizer
highestForwardedExtSequenceNumber uint64
highestForwardedExtTimestamp uint64
// bitset for upstream packet receive history [lastSeq-8, lastSeq-1], bit 1 represents packet received
pktHistory byte
// forwarded packet, reused since ForwardRTP runs on one goroutine and
// down tracks do not keep the packet past WriteRTP
sendExtPkt buffer.ExtPacket
@@ -80,7 +69,7 @@ func (r *RedPrimaryReceiver) ForwardRTP(pkt *buffer.ExtPacket, spatialLayer int3
return utils.BroadcastRTP(r.downTrackSpreader, pkt, spatialLayer)
}
pkts, err := r.getSendPktsFromRed(pkt.Packet)
pkts, err := r.depacketizer.Depacketize(pkt.Packet)
if err != nil {
r.logger.Errorw("get encoding for red failed", err, "payloadtype", pkt.Packet.PayloadType)
return 0
@@ -115,10 +104,8 @@ func (r *RedPrimaryReceiver) ForwardRTP(pkt *buffer.ExtPacket, spatialLayer int3
"packetESN", pPkt.ExtSequenceNumber,
"packetExtractedTS", pkts[i].Timestamp,
"packetETS", pPkt.ExtTimestamp,
"pktHistory", r.pktHistory,
"redHeader", pkt.Packet.Payload[:min(len(pkt.Packet.Payload), 10)],
"payloadSize", len(pkt.Packet.Payload),
"lastSeq", r.lastSeq,
"highestFowardedExtSequenceNumber", r.highestForwardedExtSequenceNumber,
"highestFowardedExtTimestamp", r.highestForwardedExtTimestamp,
)
@@ -207,7 +194,7 @@ func (r *RedPrimaryReceiver) ReadRTP(buf []byte, layer uint8, esn uint64) (int,
var pkt rtp.Packet
pkt.Unmarshal(buf[:n])
payload, err := extractPrimaryEncodingForRED(pkt.Payload)
payload, err := red.ExtractPrimaryEncoding(pkt.Payload)
if err != nil {
return 0, err
}
@@ -215,173 +202,3 @@ func (r *RedPrimaryReceiver) ReadRTP(buf []byte, layer uint8, esn uint64) (int,
return pkt.MarshalTo(buf)
}
func (r *RedPrimaryReceiver) getSendPktsFromRed(rtp *rtp.Packet) ([]*rtp.Packet, error) {
var needRecover bool
if !r.firstPktReceived {
r.lastSeq = rtp.SequenceNumber
r.pktHistory = 0
r.firstPktReceived = true
} else {
diff := rtp.SequenceNumber - r.lastSeq
switch {
case diff == 0: // duplicate
break
case diff > 0x8000: // unorder
// in history
if 65535-diff < 8 {
r.pktHistory |= 1 << (65535 - diff)
needRecover = true
}
case diff > 8: // long jump
r.lastSeq = rtp.SequenceNumber
r.pktHistory = 0
needRecover = true
default:
r.lastSeq = rtp.SequenceNumber
r.pktHistory = (r.pktHistory << byte(diff)) | 1<<(diff-1)
needRecover = true
}
}
var recoverBits byte
if needRecover {
bitIndex := r.lastSeq - rtp.SequenceNumber
for i := range maxRedCount {
if bitIndex > 7 {
break
}
if r.pktHistory&byte(1<<bitIndex) == 0 {
recoverBits |= byte(1 << i)
}
bitIndex++
}
}
return extractPktsFromRed(rtp, recoverBits)
}
// ---------------------------------------
type block struct {
tsOffset uint32
length int
pt uint8
primary bool
}
func extractPktsFromRed(redPkt *rtp.Packet, recoverBits byte) ([]*rtp.Packet, error) {
payload := redPkt.Payload
var blocks []block
var blockLength int
for {
if len(payload) < 1 {
// illegal data, need at least one byte for primary encoding
return nil, ErrIncompleteRedHeader
}
if payload[0]&0x80 == 0 {
// last block is primary encoding data
pt := uint8(payload[0] & 0x7F)
blocks = append(blocks, block{pt: pt, primary: true})
payload = payload[1:]
break
} else {
if len(payload) < 4 {
// illegal data
return nil, ErrIncompleteRedHeader
}
blockHead := binary.BigEndian.Uint32(payload[0:])
length := int(blockHead & 0x03FF)
blockHead >>= 10
tsOffset := blockHead & 0x3FFF
blockHead >>= 14
pt := uint8(blockHead & 0x7F)
blocks = append(blocks, block{pt: pt, length: length, tsOffset: tsOffset})
blockLength += length
payload = payload[4:]
}
}
if len(payload) < blockLength {
return nil, ErrIncompleteRedBlock
}
pkts := make([]*rtp.Packet, 0, len(blocks))
for i, b := range blocks {
if b.primary {
header := redPkt.Header
header.PayloadType = b.pt
pkts = append(pkts, &rtp.Packet{Header: header, Payload: payload})
break
}
recoverIndex := len(blocks) - i - 1
if recoverIndex < 1 || recoverBits&(1<<(recoverIndex-1)) == 0 {
// skip past packet/block that does not need recovery
payload = payload[b.length:]
continue
}
// recover missing packet
header := redPkt.Header
header.SequenceNumber -= uint16(recoverIndex)
header.Timestamp -= b.tsOffset
header.PayloadType = b.pt
pkts = append(pkts, &rtp.Packet{Header: header, Payload: payload[:b.length]})
payload = payload[b.length:]
}
return pkts, nil
}
func extractPrimaryEncodingForRED(payload []byte) ([]byte, error) {
/* RED payload https://datatracker.ietf.org/doc/html/rfc2198#section-3
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F| block PT | timestamp offset | block length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
F: 1 bit First bit in header indicates whether another header block
follows. If 1 further header blocks follow, if 0 this is the
last header block.
*/
var blockLength int
for {
if len(payload) < 1 {
// illegal data, need at least one byte for primary encoding
return nil, ErrIncompleteRedHeader
}
if payload[0]&0x80 == 0 {
// last block is primary encoding data
payload = payload[1:]
break
} else {
if len(payload) < 4 {
// illegal data
return nil, ErrIncompleteRedHeader
}
blockLength += int(binary.BigEndian.Uint16(payload[2:]) & 0x03FF)
payload = payload[4:]
}
}
if len(payload) < blockLength {
return nil, ErrIncompleteRedBlock
}
return payload[blockLength:], nil
}
+8 -7
View File
@@ -24,6 +24,7 @@ import (
"github.com/livekit/livekit-server/pkg/sfu/buffer"
"github.com/livekit/mediatransportutil/pkg/bucket"
"github.com/livekit/mediatransportutil/pkg/red"
"github.com/livekit/protocol/logger"
)
@@ -253,9 +254,9 @@ func TestRedReceiverReusedReadBuffer(t *testing.T) {
},
},
}
red := w.GetRedReceiver().(*RedReceiver)
rr := w.GetRedReceiver().(*RedReceiver)
dt := &dummyDowntrack{TrackSender: &DownTrack{}}
require.NoError(t, red.AddDownTrack(dt))
require.NoError(t, rr.AddDownTrack(dt))
var payloads [][]byte
pktBuf := make([]byte, bucket.RTPMaxPktSize)
@@ -272,12 +273,12 @@ func TestRedReceiverReusedReadBuffer(t *testing.T) {
require.NoError(t, err)
extPkt, err := buff.ReadExtended(pktBuf)
require.NoError(t, err)
red.ForwardRTP(extPkt, 0)
rr.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)
blocks, err := red.ExtractPackets(dt.lastReceivedPkt, 0xFF)
require.NoError(t, err)
require.Len(t, blocks, 3)
for i, block := range blocks {
@@ -285,14 +286,14 @@ func TestRedReceiverReusedReadBuffer(t *testing.T) {
}
}
func verifyRedEncodings(t *testing.T, red *rtp.Packet, redPkts []*rtp.Packet) {
func verifyRedEncodings(t *testing.T, redPkt *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)
pktsFromRed, err := red.ExtractPackets(redPkt, 0xFF)
require.NoError(t, err)
require.Len(t, pktsFromRed, len(solidPkts))
for i, pkt := range pktsFromRed {
@@ -493,7 +494,7 @@ func TestExtractPrimaryEncodingForRED(t *testing.T) {
primaryPkts := make([]*rtp.Packet, 0, len(redPkts))
for _, redPkt := range redPkts {
payload, err := extractPrimaryEncodingForRED(redPkt.Payload)
payload, err := red.ExtractPrimaryEncoding(redPkt.Payload)
require.NoError(t, err)
primaryHeader := redPkt.Header
primaryHeader.PayloadType = 111