mirror of
https://github.com/livekit/livekit.git
synced 2026-11-12 22:58:01 +00:00
* sfu: ignore a nil publisher sender report in the forwarder A stream restart replaces the receiver stats between the sender report callback and its read, so the report can be nil. SetRefSenderReport now keeps the previous report instead of reading the nil one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * dynacast: skip a codec regression when the target codec has no quality HandleCodecRegression looked up the target codec after it released the lock. If the manager closed in between, or the target mime type was unknown, the lookup returned nil and RegressTo called Restart or Replace on it. The target is now looked up with the source, and a missing one stops the regression. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * service: create the internal record for a stored room that has none RedisStore.LoadRoom returns a nil RoomInternal without an error when the room exists but its internal field does not, for example after a delete races a store. CreateRoom then wrote egress, agent, playout delay or sync settings into the nil record. It now starts from an empty record, which the store call writes back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * sfu: drop a dependency descriptor whose chains do not match the structure The parser takes a new structure from any packet, but DDForwarder keeps the structure of the last packet it forwarded. When the publisher attached a structure with fewer chains to a packet this downtrack dropped, the next descriptor had fewer chain diffs than the forwarder's structure, and the writer indexed past them. The forwarder now drops such a packet, and the writer returns an error instead of indexing past the chain diffs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * deps: bump mediatransportutil for the empty VP9 payload fix A VP9 packet without a dependency descriptor and with a 1-byte payload such as 0x08 parses to an empty VP9 payload, and IsVP9KeyFrame read its first byte. mediatransportutil#106 returns false for an empty payload. This pins the transport-v4 commit that carries it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * sfu: keep the template test comment with its test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * dynacast: create the target quality only for a valid regression A target quality starts a timer that notifies the publisher. Look it up only after the source codec is found, so a rejected regression creates nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * service: restore the internal defaults for a room that lost them A stored room without an internal record now gets the configured playout delay and sync streams defaults. Its own settings, such as the empty timeout and codecs, stay as stored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
846 lines
22 KiB
Go
846 lines
22 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 buffer
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"math"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pion/rtcp"
|
|
"github.com/pion/rtp"
|
|
"github.com/pion/sdp/v3"
|
|
"github.com/pion/webrtc/v4"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/atomic"
|
|
|
|
"github.com/livekit/mediatransportutil/pkg/nack"
|
|
|
|
act "github.com/livekit/livekit-server/pkg/sfu/rtpextension/abscapturetime"
|
|
"github.com/livekit/livekit-server/pkg/sfu/utils"
|
|
)
|
|
|
|
var h265Codec = webrtc.RTPCodecParameters{
|
|
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
MimeType: "video/h265",
|
|
ClockRate: 90000,
|
|
RTCPFeedback: []webrtc.RTCPFeedback{{
|
|
Type: "nack",
|
|
}},
|
|
},
|
|
PayloadType: 116,
|
|
}
|
|
|
|
var vp8Codec = webrtc.RTPCodecParameters{
|
|
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
MimeType: "video/vp8",
|
|
ClockRate: 90000,
|
|
RTCPFeedback: []webrtc.RTCPFeedback{{
|
|
Type: "nack",
|
|
}},
|
|
},
|
|
PayloadType: 96,
|
|
}
|
|
|
|
var opusCodec = webrtc.RTPCodecParameters{
|
|
RTPCodecCapability: webrtc.RTPCodecCapability{
|
|
MimeType: "audio/opus",
|
|
ClockRate: 48000,
|
|
},
|
|
PayloadType: 111,
|
|
}
|
|
|
|
func TestNack(t *testing.T) {
|
|
t.Run("nack normal", func(t *testing.T) {
|
|
buff := NewBuffer(123, 1, 1)
|
|
buff.codecType = webrtc.RTPCodecTypeVideo
|
|
require.NotNil(t, buff)
|
|
var wg sync.WaitGroup
|
|
// 5 tries
|
|
wg.Add(5)
|
|
buff.OnRtcpFeedback(func(fb []rtcp.Packet) {
|
|
for _, pkt := range fb {
|
|
switch p := pkt.(type) {
|
|
case *rtcp.TransportLayerNack:
|
|
if p.Nacks[0].PacketList()[0] == 1 && p.MediaSSRC == 123 {
|
|
wg.Done()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
buff.Bind(webrtc.RTPParameters{
|
|
HeaderExtensions: nil,
|
|
Codecs: []webrtc.RTPCodecParameters{vp8Codec},
|
|
}, vp8Codec.RTPCodecCapability, 0)
|
|
rtt := uint32(20)
|
|
buff.nacker.SetRTT(rtt)
|
|
for i := range 15 {
|
|
if i == 1 {
|
|
continue
|
|
}
|
|
if i < 14 {
|
|
time.Sleep(time.Duration(float64(rtt)*math.Pow(nack.NackQueueParamsDefault.BackoffFactor, float64(i))+10) * time.Millisecond)
|
|
} else {
|
|
time.Sleep(500 * time.Millisecond) // even a long wait should not exceed max retries
|
|
}
|
|
pkt := rtp.Packet{
|
|
Header: rtp.Header{
|
|
Version: 2,
|
|
PayloadType: 96,
|
|
SequenceNumber: uint16(i),
|
|
Timestamp: uint32(i),
|
|
SSRC: 123,
|
|
},
|
|
Payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x9f, 0x94, 0x1},
|
|
}
|
|
b, err := pkt.Marshal()
|
|
require.NoError(t, err)
|
|
_, err = buff.Write(b)
|
|
require.NoError(t, err)
|
|
}
|
|
wg.Wait()
|
|
|
|
})
|
|
|
|
t.Run("nack with seq wrap", func(t *testing.T) {
|
|
buff := NewBuffer(123, 1, 1)
|
|
buff.codecType = webrtc.RTPCodecTypeVideo
|
|
require.NotNil(t, buff)
|
|
var wg sync.WaitGroup
|
|
expects := map[uint16]int{
|
|
65534: 0,
|
|
65535: 0,
|
|
0: 0,
|
|
1: 0,
|
|
}
|
|
wg.Add(5 * len(expects)) // retry 5 times
|
|
buff.OnRtcpFeedback(func(fb []rtcp.Packet) {
|
|
for _, pkt := range fb {
|
|
switch p := pkt.(type) {
|
|
case *rtcp.TransportLayerNack:
|
|
if p.MediaSSRC == 123 {
|
|
for _, v := range p.Nacks {
|
|
v.Range(func(seq uint16) bool {
|
|
if _, ok := expects[seq]; ok {
|
|
wg.Done()
|
|
} else {
|
|
require.Fail(t, "unexpected nack seq ", seq)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
buff.Bind(webrtc.RTPParameters{
|
|
HeaderExtensions: nil,
|
|
Codecs: []webrtc.RTPCodecParameters{vp8Codec},
|
|
}, vp8Codec.RTPCodecCapability, 0)
|
|
rtt := uint32(30)
|
|
buff.nacker.SetRTT(rtt)
|
|
for i := range 15 {
|
|
if i > 0 && i < 5 {
|
|
continue
|
|
}
|
|
if i < 14 {
|
|
time.Sleep(time.Duration(float64(rtt)*math.Pow(nack.NackQueueParamsDefault.BackoffFactor, float64(i))+10) * time.Millisecond)
|
|
} else {
|
|
time.Sleep(500 * time.Millisecond) // even a long wait should not exceed max retries
|
|
}
|
|
pkt := rtp.Packet{
|
|
Header: rtp.Header{
|
|
Version: 2,
|
|
PayloadType: 96,
|
|
SequenceNumber: uint16(i + 65533),
|
|
Timestamp: uint32(i),
|
|
SSRC: 123,
|
|
},
|
|
Payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x9f, 0x94, 0x1},
|
|
}
|
|
b, err := pkt.Marshal()
|
|
require.NoError(t, err)
|
|
_, err = buff.Write(b)
|
|
require.NoError(t, err)
|
|
}
|
|
wg.Wait()
|
|
|
|
})
|
|
}
|
|
|
|
func TestNackOnRTX(t *testing.T) {
|
|
buff := NewBuffer(123, 1, 1)
|
|
buff.codecType = webrtc.RTPCodecTypeVideo
|
|
var mu sync.Mutex
|
|
var nacked []uint16
|
|
buff.OnRtcpFeedback(func(fb []rtcp.Packet) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
for _, pkt := range fb {
|
|
if p, ok := pkt.(*rtcp.TransportLayerNack); ok && p.MediaSSRC == 123 {
|
|
for _, pair := range p.Nacks {
|
|
nacked = append(nacked, pair.PacketList()...)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
require.NoError(t, buff.Bind(webrtc.RTPParameters{
|
|
Codecs: []webrtc.RTPCodecParameters{vp8Codec},
|
|
}, vp8Codec.RTPCodecCapability, 0))
|
|
buff.rtxPayloadType = 97
|
|
rtt := uint32(20)
|
|
buff.nacker.SetRTT(rtt)
|
|
|
|
payload := []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x9f, 0x94, 0x1}
|
|
// sequence numbers 1 and 2 are lost
|
|
for _, sn := range []uint16{0, 3} {
|
|
pkt := rtp.Packet{
|
|
Header: rtp.Header{Version: 2, PayloadType: 96, SequenceNumber: sn, Timestamp: uint32(sn), SSRC: 123},
|
|
Payload: payload,
|
|
}
|
|
b, err := pkt.Marshal()
|
|
require.NoError(t, err)
|
|
_, err = buff.Write(b)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
time.Sleep(time.Duration(2*rtt) * time.Millisecond)
|
|
mu.Lock()
|
|
nacked = nil
|
|
mu.Unlock()
|
|
|
|
// RTX repairs sequence number 2, the NACK for 1 is due and must be sent
|
|
rtxPkt := rtp.Packet{
|
|
Header: rtp.Header{Version: 2, PayloadType: 97, SequenceNumber: 500, Timestamp: 2, SSRC: 456},
|
|
Payload: append([]byte{0x00, 0x02}, payload...),
|
|
}
|
|
buff.writeRTX(&rtxPkt, time.Now().UnixNano())
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
require.Equal(t, []uint16{1}, nacked)
|
|
}
|
|
|
|
func TestVP9EmptyFramePayload(t *testing.T) {
|
|
vp9Codec := webrtc.RTPCodecParameters{
|
|
RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "video/vp9", ClockRate: 90000},
|
|
PayloadType: 98,
|
|
}
|
|
buff := NewBuffer(123, 1, 1)
|
|
buff.codecType = webrtc.RTPCodecTypeVideo
|
|
require.NoError(t, buff.Bind(webrtc.RTPParameters{
|
|
Codecs: []webrtc.RTPCodecParameters{vp9Codec},
|
|
}, vp9Codec.RTPCodecCapability, 0))
|
|
|
|
// B bit set and no VP9 frame bytes, which parses to an empty VP9 payload
|
|
pkt := rtp.Packet{
|
|
Header: rtp.Header{Version: 2, PayloadType: 98, SequenceNumber: 1, Timestamp: 1, SSRC: 123},
|
|
Payload: []byte{0x08},
|
|
}
|
|
b, err := pkt.Marshal()
|
|
require.NoError(t, err)
|
|
require.NotPanics(t, func() { _, _ = buff.Write(b) })
|
|
}
|
|
|
|
func TestNewBuffer(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
}{
|
|
{
|
|
name: "Must not be nil and add packets in sequence",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var TestPackets = []*rtp.Packet{
|
|
{
|
|
Header: rtp.Header{
|
|
Version: 2,
|
|
PayloadType: 96,
|
|
SequenceNumber: 65533,
|
|
SSRC: 123,
|
|
},
|
|
},
|
|
{
|
|
Header: rtp.Header{
|
|
Version: 2,
|
|
PayloadType: 96,
|
|
SequenceNumber: 65534,
|
|
SSRC: 123,
|
|
},
|
|
Payload: []byte{1},
|
|
},
|
|
{
|
|
Header: rtp.Header{
|
|
Version: 2,
|
|
PayloadType: 96,
|
|
SequenceNumber: 2,
|
|
SSRC: 123,
|
|
},
|
|
},
|
|
{
|
|
Header: rtp.Header{
|
|
Version: 2,
|
|
PayloadType: 96,
|
|
SequenceNumber: 65535,
|
|
SSRC: 123,
|
|
},
|
|
},
|
|
}
|
|
buff := NewBuffer(123, 1, 1)
|
|
buff.codecType = webrtc.RTPCodecTypeVideo
|
|
require.NotNil(t, buff)
|
|
buff.OnRtcpFeedback(func(_ []rtcp.Packet) {})
|
|
buff.Bind(webrtc.RTPParameters{
|
|
HeaderExtensions: nil,
|
|
Codecs: []webrtc.RTPCodecParameters{vp8Codec},
|
|
}, vp8Codec.RTPCodecCapability, 0)
|
|
|
|
for _, p := range TestPackets {
|
|
buf, _ := p.Marshal()
|
|
_, _ = buff.Write(buf)
|
|
}
|
|
require.Equal(t, uint16(2), buff.rtpStats.HighestSequenceNumber())
|
|
require.Equal(t, uint64(65536+2), buff.rtpStats.ExtendedHighestSequenceNumber())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFractionLostReport(t *testing.T) {
|
|
buff := NewBuffer(123, 1, 1)
|
|
require.NotNil(t, buff)
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// with loss proxying
|
|
wg.Add(1)
|
|
buff.SetAudioLossProxying(true)
|
|
buff.SetLastFractionLostReport(55)
|
|
buff.OnRtcpFeedback(func(fb []rtcp.Packet) {
|
|
for _, pkt := range fb {
|
|
switch p := pkt.(type) {
|
|
case *rtcp.ReceiverReport:
|
|
for _, v := range p.Reports {
|
|
require.EqualValues(t, 55, v.FractionLost)
|
|
}
|
|
wg.Done()
|
|
}
|
|
}
|
|
})
|
|
buff.Bind(webrtc.RTPParameters{
|
|
HeaderExtensions: nil,
|
|
Codecs: []webrtc.RTPCodecParameters{opusCodec},
|
|
}, opusCodec.RTPCodecCapability, 0)
|
|
for i := range 15 {
|
|
pkt := rtp.Packet{
|
|
Header: rtp.Header{
|
|
Version: 2,
|
|
PayloadType: 111,
|
|
SequenceNumber: uint16(i),
|
|
Timestamp: uint32(i),
|
|
SSRC: 123,
|
|
},
|
|
Payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x9f, 0x94, 0x1},
|
|
}
|
|
b, err := pkt.Marshal()
|
|
require.NoError(t, err)
|
|
if i == 1 {
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
_, err = buff.Write(b)
|
|
require.NoError(t, err)
|
|
}
|
|
wg.Wait()
|
|
|
|
wg.Add(1)
|
|
buff.SetAudioLossProxying(false)
|
|
buff.OnRtcpFeedback(func(fb []rtcp.Packet) {
|
|
for _, pkt := range fb {
|
|
switch p := pkt.(type) {
|
|
case *rtcp.ReceiverReport:
|
|
for _, v := range p.Reports {
|
|
require.EqualValues(t, 0, v.FractionLost)
|
|
}
|
|
wg.Done()
|
|
}
|
|
}
|
|
})
|
|
buff.Bind(webrtc.RTPParameters{
|
|
HeaderExtensions: nil,
|
|
Codecs: []webrtc.RTPCodecParameters{opusCodec},
|
|
}, opusCodec.RTPCodecCapability, 0)
|
|
for i := range 15 {
|
|
pkt := rtp.Packet{
|
|
Header: rtp.Header{
|
|
Version: 2,
|
|
PayloadType: 111,
|
|
SequenceNumber: uint16(i),
|
|
Timestamp: uint32(i),
|
|
SSRC: 123,
|
|
},
|
|
Payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x9f, 0x94, 0x1},
|
|
}
|
|
b, err := pkt.Marshal()
|
|
require.NoError(t, err)
|
|
if i == 1 {
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
_, err = buff.Write(b)
|
|
require.NoError(t, err)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestCodecChange(t *testing.T) {
|
|
// codec change before bind
|
|
buff := NewBuffer(123, 1, 1)
|
|
require.NotNil(t, buff)
|
|
changedCodec := make(chan webrtc.RTPCodecParameters, 1)
|
|
buff.OnCodecChange(func(rp webrtc.RTPCodecParameters) {
|
|
select {
|
|
case changedCodec <- rp:
|
|
default:
|
|
t.Fatalf("codec change not consumed")
|
|
}
|
|
})
|
|
buff.OnStreamRestart(func(reason string) {
|
|
require.Equal(t, "codec-change", reason)
|
|
|
|
// read once to clear pending restart
|
|
var buf [1500]byte
|
|
extPkt, err := buff.ReadExtended(buf[:])
|
|
require.NoError(t, err)
|
|
require.Nil(t, extPkt)
|
|
})
|
|
|
|
h265Pkt := rtp.Packet{
|
|
Header: rtp.Header{
|
|
Version: 2,
|
|
PayloadType: 116,
|
|
SequenceNumber: 1,
|
|
Timestamp: 1,
|
|
SSRC: 123,
|
|
},
|
|
Payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x9f, 0x94, 0x1},
|
|
}
|
|
buf, err := h265Pkt.Marshal()
|
|
require.NoError(t, err)
|
|
_, err = buff.Write(buf)
|
|
require.NoError(t, err)
|
|
|
|
select {
|
|
case <-changedCodec:
|
|
t.Fatalf("unexpected codec change")
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
|
|
// Bind sets up VP8 as expected codec,
|
|
// packet written to the buffer above before Bind is H.265,
|
|
// that should trigger a codec change and a stream restart
|
|
// when the queued packets from Write before Bind get flushed
|
|
buff.Bind(
|
|
webrtc.RTPParameters{
|
|
HeaderExtensions: nil,
|
|
Codecs: []webrtc.RTPCodecParameters{vp8Codec, h265Codec},
|
|
},
|
|
vp8Codec.RTPCodecCapability,
|
|
0,
|
|
)
|
|
|
|
select {
|
|
case c := <-changedCodec:
|
|
require.Equal(t, h265Codec, c)
|
|
case <-time.After(1 * time.Second):
|
|
t.Fatalf("expected codec change")
|
|
}
|
|
|
|
// second codec change - writing VP8 packet after Bind should trigger another codec change
|
|
vp8Pkt := rtp.Packet{
|
|
Header: rtp.Header{
|
|
Version: 2,
|
|
PayloadType: 96,
|
|
SequenceNumber: 3,
|
|
Timestamp: 3,
|
|
SSRC: 123,
|
|
},
|
|
Payload: []byte{0xff, 0xff, 0xff, 0xfd, 0xb4, 0x9f, 0x94, 0x1},
|
|
}
|
|
buf, err = vp8Pkt.Marshal()
|
|
require.NoError(t, err)
|
|
_, err = buff.Write(buf)
|
|
require.NoError(t, err)
|
|
|
|
select {
|
|
case c := <-changedCodec:
|
|
require.Equal(t, vp8Codec, c)
|
|
case <-time.After(1 * time.Second):
|
|
t.Fatalf("expected codec change")
|
|
}
|
|
fmt.Printf("done second codec change\n") // REMOVE
|
|
|
|
// out of order pkts can't cause codec change
|
|
// rewrite the VP8 packet to start the sequence after a stream restart
|
|
_, err = buff.Write(buf)
|
|
require.NoError(t, err)
|
|
|
|
h265Pkt.SequenceNumber = 2
|
|
h265Pkt.Timestamp = 2
|
|
buf, err = h265Pkt.Marshal()
|
|
require.NoError(t, err)
|
|
_, err = buff.Write(buf)
|
|
require.NoError(t, err)
|
|
select {
|
|
case <-changedCodec:
|
|
t.Fatalf("unexpected codec change")
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
|
|
// unknown codec should not cause change even if it is in order
|
|
h265Pkt.SequenceNumber = 4
|
|
h265Pkt.Timestamp = 4
|
|
h265Pkt.PayloadType = 117
|
|
buf, err = h265Pkt.Marshal()
|
|
require.NoError(t, err)
|
|
_, err = buff.Write(buf)
|
|
require.NoError(t, err)
|
|
select {
|
|
case <-changedCodec:
|
|
t.Fatalf("unexpected codec change")
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
|
|
// an in-order packet should change codec again
|
|
h265Pkt.SequenceNumber = 5
|
|
h265Pkt.Timestamp = 5
|
|
h265Pkt.PayloadType = 116
|
|
buf, err = h265Pkt.Marshal()
|
|
require.NoError(t, err)
|
|
_, err = buff.Write(buf)
|
|
require.NoError(t, err)
|
|
select {
|
|
case c := <-changedCodec:
|
|
require.Equal(t, h265Codec, c)
|
|
case <-time.After(1 * time.Second):
|
|
t.Fatalf("expected codec change")
|
|
}
|
|
}
|
|
|
|
// Write unmarshals into reused scratch and callers reuse the input buffer, so each ExtPacket
|
|
// must keep its own header and extensions, and Write plus ReadExtended must not allocate per packet
|
|
func TestIngestNoAlloc(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
codec webrtc.RTPCodecParameters
|
|
payload []byte
|
|
maxAllocs float64
|
|
}{
|
|
{"opus", opusCodec, make([]byte, 100), 0},
|
|
// VP8 boxes the parsed descriptor into ExtPacket.Payload
|
|
{"vp8", vp8Codec, []byte{0x90, 0xe0, 0x80, 0x01, 0x05, 0x00, 0x01}, 1},
|
|
}
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
buff := NewBuffer(123, 100, 100)
|
|
require.NoError(t, buff.Bind(webrtc.RTPParameters{
|
|
HeaderExtensions: []webrtc.RTPHeaderExtensionParameter{
|
|
{URI: sdp.AudioLevelURI, ID: 1},
|
|
{URI: act.AbsCaptureTimeURI, ID: 2},
|
|
},
|
|
Codecs: []webrtc.RTPCodecParameters{tc.codec},
|
|
}, tc.codec.RTPCodecCapability, 0))
|
|
defer buff.Close()
|
|
|
|
marshal := func(sn uint16, level byte) []byte {
|
|
p := rtp.Packet{
|
|
Header: rtp.Header{
|
|
Version: 2,
|
|
PayloadType: uint8(tc.codec.PayloadType),
|
|
SequenceNumber: sn,
|
|
Timestamp: uint32(sn) * 960,
|
|
SSRC: 123,
|
|
},
|
|
Payload: tc.payload,
|
|
}
|
|
require.NoError(t, p.SetExtension(1, []byte{level}))
|
|
require.NoError(t, p.SetExtension(3, []byte{0, 0}))
|
|
raw, err := p.Marshal()
|
|
require.NoError(t, err)
|
|
return raw
|
|
}
|
|
|
|
// one input buffer for all writes, like the transport read loop
|
|
input := make([]byte, 1500)
|
|
write := func(sn uint16, level byte) {
|
|
n := copy(input, marshal(sn, level))
|
|
_, err := buff.Write(input[:n])
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
readBuf := make([]byte, 1500)
|
|
write(1, 10)
|
|
write(2, 20)
|
|
for _, want := range []struct {
|
|
sn uint16
|
|
level byte
|
|
}{{1, 10}, {2, 20}} {
|
|
ep, err := buff.ReadExtended(readBuf)
|
|
require.NoError(t, err)
|
|
require.Equal(t, want.sn, ep.Packet.SequenceNumber)
|
|
require.Equal(t, []byte{want.level}, ep.Packet.GetExtension(1))
|
|
require.Equal(t, tc.payload, ep.Packet.Payload)
|
|
require.Nil(t, ep.AbsCaptureTimeExt)
|
|
ReleaseExtPacket(ep)
|
|
}
|
|
|
|
raw := marshal(3, 30)
|
|
sn := uint16(3)
|
|
ingest := func() {
|
|
binary.BigEndian.PutUint16(raw[2:], sn)
|
|
binary.BigEndian.PutUint32(raw[4:], uint32(sn)*960)
|
|
sn++
|
|
_, err := buff.Write(raw)
|
|
require.NoError(t, err)
|
|
ep, err := buff.ReadExtended(readBuf)
|
|
require.NoError(t, err)
|
|
ReleaseExtPacket(ep)
|
|
}
|
|
allocs := testing.AllocsPerRun(1000, ingest)
|
|
if !utils.RaceEnabled {
|
|
require.LessOrEqual(t, allocs, tc.maxAllocs, "allocations per Write plus ReadExtended")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// a dropped padding only packet shifts later sequence numbers, ReadExtended must keep the shifted number
|
|
// in the header and in RawPacket
|
|
func TestIngestDroppedPaddingKeepsAdjustedSequenceNumber(t *testing.T) {
|
|
buff := NewBuffer(123, 100, 100)
|
|
require.NoError(t, buff.Bind(webrtc.RTPParameters{
|
|
Codecs: []webrtc.RTPCodecParameters{opusCodec},
|
|
}, opusCodec.RTPCodecCapability, 0))
|
|
defer buff.Close()
|
|
|
|
write := func(sn uint16, paddingOnly bool) {
|
|
p := rtp.Packet{
|
|
Header: rtp.Header{
|
|
Version: 2,
|
|
PayloadType: uint8(opusCodec.PayloadType),
|
|
SequenceNumber: sn,
|
|
Timestamp: uint32(sn) * 960,
|
|
SSRC: 123,
|
|
},
|
|
}
|
|
if paddingOnly {
|
|
p.Header.Padding = true
|
|
p.Header.PaddingSize = 20
|
|
} else {
|
|
p.Payload = make([]byte, 100)
|
|
}
|
|
raw, err := p.Marshal()
|
|
require.NoError(t, err)
|
|
_, err = buff.Write(raw)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
write(40, false)
|
|
write(41, true)
|
|
write(42, false)
|
|
|
|
readBuf := make([]byte, 1500)
|
|
for _, wantSN := range []uint16{40, 41} {
|
|
ep, err := buff.ReadExtended(readBuf)
|
|
require.NoError(t, err)
|
|
require.Equal(t, wantSN, ep.Packet.SequenceNumber)
|
|
require.Equal(t, wantSN, binary.BigEndian.Uint16(ep.RawPacket[2:]))
|
|
ReleaseExtPacket(ep)
|
|
}
|
|
}
|
|
|
|
func BenchmarkMemcpu(b *testing.B) {
|
|
buf := make([]byte, 1500*1500*10)
|
|
buf2 := make([]byte, 1500*1500*20)
|
|
|
|
for b.Loop() {
|
|
copy(buf2, buf)
|
|
}
|
|
}
|
|
|
|
func BenchmarkExtPacketFactory(b *testing.B) {
|
|
|
|
for b.Loop() {
|
|
extPkt := ExtPacketFactory.Get().(*ExtPacket)
|
|
*extPkt = ExtPacket{}
|
|
ExtPacketFactory.Put(extPkt)
|
|
}
|
|
}
|
|
|
|
func TestGetPacketConcurrentWithWrite(t *testing.T) {
|
|
newBound := func(t *testing.T) *Buffer {
|
|
buff := NewBuffer(123, 1, 1)
|
|
buff.OnRtcpFeedback(func(_ []rtcp.Packet) {})
|
|
require.NoError(t, buff.Bind(webrtc.RTPParameters{Codecs: []webrtc.RTPCodecParameters{opusCodec}}, opusCodec.RTPCodecCapability, 0))
|
|
return buff
|
|
}
|
|
// the payload carries the sequence number, so a read can check it got the packet it asked for;
|
|
// assert, not require, as this also runs off the test goroutine
|
|
write := func(t *testing.T, buff *Buffer, sn uint16) {
|
|
payload := make([]byte, 100)
|
|
binary.BigEndian.PutUint16(payload, sn)
|
|
pkt := &rtp.Packet{
|
|
Header: rtp.Header{Version: 2, PayloadType: 111, SequenceNumber: sn, Timestamp: uint32(sn) * 960, SSRC: 123},
|
|
Payload: payload,
|
|
}
|
|
b, err := pkt.Marshal()
|
|
if assert.NoError(t, err) {
|
|
_, err = buff.Write(b)
|
|
assert.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
t.Run("a read runs while another read holds the lock, a write waits", func(t *testing.T) {
|
|
buff := newBound(t)
|
|
for sn := uint16(1000); sn < 1010; sn++ {
|
|
write(t, buff, sn)
|
|
}
|
|
esn := buff.rtpStats.ExtendedHighestSequenceNumber()
|
|
|
|
buff.RLock() // a read in progress
|
|
var unlockOnce sync.Once
|
|
unlock := func() { unlockOnce.Do(buff.RUnlock) }
|
|
t.Cleanup(unlock) // so a failure below does not leave the lock held
|
|
|
|
readDone := make(chan struct{})
|
|
go func() {
|
|
defer close(readDone)
|
|
n, err := buff.GetPacket(make([]byte, 1500), esn)
|
|
assert.NoError(t, err)
|
|
assert.Positive(t, n)
|
|
}()
|
|
require.Eventually(t, func() bool {
|
|
select {
|
|
case <-readDone:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}, time.Second, 5*time.Millisecond, "a read must not wait for another read")
|
|
|
|
writeDone := make(chan struct{})
|
|
go func() {
|
|
defer close(writeDone)
|
|
write(t, buff, 1010)
|
|
}()
|
|
select {
|
|
case <-writeDone:
|
|
t.Fatal("a write must wait for the read to finish")
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
unlock()
|
|
select {
|
|
case <-writeDone:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("the write must go through once the read is done")
|
|
}
|
|
})
|
|
|
|
t.Run("concurrent reads get the packet they ask for while writes continue", func(t *testing.T) {
|
|
buff := newBound(t)
|
|
const first, numPackets = uint16(2000), 2000
|
|
write(t, buff, first)
|
|
base := buff.rtpStats.ExtendedHighestSequenceNumber() - uint64(first)
|
|
|
|
var wg sync.WaitGroup
|
|
stop := make(chan struct{})
|
|
for r := 0; r < 4; r++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
buf := make([]byte, 1500)
|
|
var pkt rtp.Packet
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
default:
|
|
}
|
|
esn := buff.rtpStats.ExtendedHighestSequenceNumber()
|
|
n, err := buff.GetPacket(buf, esn)
|
|
if err != nil {
|
|
continue // the ring may have moved on
|
|
}
|
|
if assert.NoError(t, pkt.Unmarshal(buf[:n])) {
|
|
assert.Equal(t, uint16(esn-base), pkt.SequenceNumber)
|
|
assert.Equal(t, pkt.SequenceNumber, binary.BigEndian.Uint16(pkt.Payload))
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
for sn := first + 1; sn < first+numPackets; sn++ {
|
|
write(t, buff, sn)
|
|
}
|
|
close(stop)
|
|
wg.Wait()
|
|
})
|
|
}
|
|
|
|
func TestSendPLIThrottle(t *testing.T) {
|
|
buff := NewBuffer(123, 1, 1)
|
|
buff.codecType = webrtc.RTPCodecTypeVideo
|
|
var plis atomic.Int32
|
|
buff.OnRtcpFeedback(func(fb []rtcp.Packet) {
|
|
for _, pkt := range fb {
|
|
if _, ok := pkt.(*rtcp.PictureLossIndication); ok {
|
|
plis.Inc()
|
|
}
|
|
}
|
|
})
|
|
require.NoError(t, buff.Bind(webrtc.RTPParameters{
|
|
Codecs: []webrtc.RTPCodecParameters{vp8Codec},
|
|
}, vp8Codec.RTPCodecCapability, 0))
|
|
buff.SetPLIThrottle(int64(time.Minute))
|
|
|
|
// many subscribers asking at once send one PLI upstream
|
|
var wg sync.WaitGroup
|
|
for range 100 {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
buff.SendPLI(false)
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
require.Equal(t, int32(1), plis.Load())
|
|
|
|
// force bypasses the throttle and restarts the window
|
|
buff.SendPLI(true)
|
|
require.Equal(t, int32(2), plis.Load())
|
|
buff.SendPLI(false)
|
|
require.Equal(t, int32(2), plis.Load())
|
|
|
|
// a restarted stream needs a key frame right away, the old window does not apply
|
|
buff.Lock()
|
|
buff.restartStreamLocked("test", false)
|
|
buff.Unlock()
|
|
buff.SendPLI(false)
|
|
require.Equal(t, int32(3), plis.Load())
|
|
}
|