Files
upstream-pion-webrtc/rtpreceiver_test.go
upstream-mirror 8acb1a3a29
Some checks failed
REUSE Compliance Check / lint (push) Failing after 0s
Browser E2E / Test (push) Failing after 3s
Test / test (1.24) (push) Failing after 0s
Test / test-i386 (1.25) (push) Failing after 0s
Test / test-i386 (1.24) (push) Failing after 0s
Test / test-windows (1.25) (push) Failing after 0s
Test / test-windows (1.24) (push) Failing after 0s
Test / test (1.25) (push) Failing after 0s
Examples Tests / Test (push) Failing after 3s
Test / test-macos (1.25) (push) Failing after 0s
Test / test-macos (1.24) (push) Failing after 0s
Go mod tidy / tidy (push) Failing after 0s
Test / test-wasm (push) Failing after 0s
CodeQL / analyze (push) Failing after 1s
Fuzz / fuzz (push) Failing after 0s
snapshot of https://github.com/pion/webrtc.git (main, 2026-09-15; blobs > 131072B omitted)
2026-09-15 20:06:07 +05:00

1039 lines
29 KiB
Go

// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
//go:build !js
package webrtc
import (
"context"
"io"
"math"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/pion/interceptor"
mock_interceptor "github.com/pion/interceptor/pkg/mock"
"github.com/pion/interceptor/pkg/stats"
"github.com/pion/logging"
"github.com/pion/rtp"
"github.com/pion/transport/v4/packetio"
"github.com/pion/transport/v4/test"
"github.com/pion/webrtc/v4/pkg/media"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// Assert that SetReadDeadline works as expected
// This test uses VNet since we must have zero loss.
func Test_RTPReceiver_SetReadDeadline(t *testing.T) {
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
sender, receiver, wan := createVNetPair(t, &interceptor.Registry{})
track, err := NewTrackLocalStaticSample(RTPCodecCapability{MimeType: MimeTypeVP8}, "video", "pion")
assert.NoError(t, err)
_, err = sender.AddTrack(track)
assert.NoError(t, err)
seenPacket, seenPacketCancel := context.WithCancel(context.Background())
receiver.OnTrack(func(trackRemote *TrackRemote, r *RTPReceiver) {
// Set Deadline for both RTP and RTCP Stream
assert.NoError(t, r.SetReadDeadline(time.Now().Add(time.Second)))
assert.NoError(t, trackRemote.SetReadDeadline(time.Now().Add(time.Second)))
// First call will not error because we cache for probing
_, _, readErr := trackRemote.ReadRTP()
assert.NoError(t, readErr)
_, _, readErr = trackRemote.ReadRTP()
assert.Error(t, readErr)
_, _, readErr = r.ReadRTCP()
assert.Error(t, readErr)
seenPacketCancel()
})
peerConnectionsConnected := untilConnectionState(PeerConnectionStateConnected, sender, receiver)
assert.NoError(t, signalPair(sender, receiver))
peerConnectionsConnected.Wait()
assert.NoError(t, track.WriteSample(media.Sample{Data: []byte{0xAA}, Duration: time.Second}))
<-seenPacket.Done()
assert.NoError(t, wan.Stop())
closePairNow(t, sender, receiver)
}
func TestRTPReceiver_ClosedReceiveForRIDAndRTX(t *testing.T) {
lim := test.TimeOut(time.Second * 5)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
api := NewAPI()
dtlsTransport, err := api.NewDTLSTransport(nil, nil)
require.NoError(t, err)
receiver, err := api.NewRTPReceiver(RTPCodecTypeVideo, dtlsTransport)
require.NoError(t, err)
receiver.configureReceive(RTPReceiveParameters{
Encodings: []RTPDecodingParameters{
{
RTPCodingParameters: RTPCodingParameters{
RID: "rid",
SSRC: 1111,
RTX: RTPRtxParameters{
SSRC: 2222,
},
},
},
},
})
require.NoError(t, receiver.Stop())
params := RTPParameters{
Codecs: []RTPCodecParameters{
{
RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeVP8},
},
},
}
ridStreamInfo := &interceptor.StreamInfo{SSRC: 1111}
rtxStreamInfo := &interceptor.StreamInfo{SSRC: 2222}
readCalled := make(chan struct{}, 1)
rtpInterceptor := interceptor.RTPReaderFunc(
func(_ []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) {
select {
case readCalled <- struct{}{}:
default:
}
return 0, a, io.EOF
},
)
for range 50 {
track, err := receiver.receiveForRid("rid", params, ridStreamInfo, nil, nil, false, nil, nil, nil)
assert.Nil(t, track)
assert.ErrorIs(t, err, io.EOF)
err = receiver.receiveForRtx(SSRC(0), "rid", rtxStreamInfo, nil, rtpInterceptor, false, nil, nil)
assert.ErrorIs(t, err, io.EOF)
}
select {
case <-readCalled:
assert.Fail(t, "repair reader invoked after Stop")
case <-time.After(100 * time.Millisecond):
}
}
func TestRTPReceiverRepairReaderPolicy(t *testing.T) {
for _, tt := range []struct {
name string
customBuffer bool
wrapped bool
}{
{name: "default buffer passthrough"},
{name: "default buffer wrapped", wrapped: true},
{name: "custom buffer passthrough", customBuffer: true},
{name: "custom buffer wrapped", customBuffer: true, wrapped: true},
} {
t.Run(tt.name, func(t *testing.T) {
startImmediately := tt.wrapped && !tt.customBuffer
receiver, track := newRepairReaderPolicyTestReceiver(t, tt.customBuffer, 2222, nil)
var calls atomic.Int32
releaseReader := make(chan struct{})
t.Cleanup(func() { close(releaseReader) })
repairReader := interceptor.RTPReaderFunc(
func(_ []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) {
calls.Add(1)
<-releaseReader
return 0, a, io.EOF
},
)
require.NoError(t, receiver.receiveForRtx(
2222, "", &interceptor.StreamInfo{SSRC: 2222}, nil, repairReader, startImmediately, nil, nil,
))
repairReaderStarted := func() bool {
receiver.mu.RLock()
defer receiver.mu.RUnlock()
return receiver.tracks[0].repairReaderStarted
}
assert.Equal(t, startImmediately, repairReaderStarted())
if startImmediately {
require.Eventually(t, func() bool { return calls.Load() == 1 }, time.Second, time.Millisecond)
} else {
assert.Zero(t, calls.Load())
}
b := make([]byte, receiveMTU)
_, err := track.peek(b)
require.NoError(t, err)
if !startImmediately {
assert.False(t, repairReaderStarted())
assert.Zero(t, calls.Load())
}
_, _, err = track.Read(b)
require.NoError(t, err)
assert.True(t, repairReaderStarted())
require.Eventually(t, func() bool { return calls.Load() == 1 }, time.Second, time.Millisecond)
_, _, err = track.Read(b)
require.NoError(t, err)
assert.Never(t, func() bool { return calls.Load() > 1 }, 25*time.Millisecond, time.Millisecond)
})
}
}
func TestTrackRemoteReadUsesEagerRepairChannel(t *testing.T) {
var primaryCalls atomic.Int32
primaryReader := interceptor.RTPReaderFunc(
func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) {
primaryCalls.Add(1)
return copy(b, []byte{
0x80, 96, 0, 1, 0, 0, 0, 0, 0, 0, 0x04, 0x57, 0xAA,
}), a, nil
},
)
receiver, track := newRepairReaderPolicyTestReceiver(t, false, 2222, primaryReader)
var repairCalls atomic.Int32
repairReader := interceptor.RTPReaderFunc(
func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) {
if repairCalls.Add(1) != 1 {
return 0, a, io.EOF
}
return copy(b, []byte{
0x80, 97, 0x13, 0x88, 0, 0, 0, 0, 0, 0, 0x08, 0xAE,
0x04, 0xD2, 0xA1,
}), a, nil
},
)
require.NoError(t, receiver.receiveForRtx(
2222, "", &interceptor.StreamInfo{SSRC: 2222}, nil, repairReader, true, nil, nil,
))
require.Eventually(t, func() bool {
receiver.mu.RLock()
defer receiver.mu.RUnlock()
return len(receiver.tracks[0].repairStreamChannel) == 1
}, time.Second, time.Millisecond)
packet, _, err := track.ReadRTP()
require.NoError(t, err)
require.NotNil(t, packet)
assert.Equal(t, uint16(1234), packet.SequenceNumber)
assert.Equal(t, []byte{0xA1}, packet.Payload)
assert.Zero(t, primaryCalls.Load())
}
func TestRTPReceiverLateRepairBindAfterTrackRead(t *testing.T) {
receiver, track := newRepairReaderPolicyTestReceiver(t, true, 0, nil)
_, _, err := track.Read(make([]byte, receiveMTU))
require.NoError(t, err)
var calls atomic.Int32
releaseReader := make(chan struct{})
t.Cleanup(func() { close(releaseReader) })
repairReader := interceptor.RTPReaderFunc(
func(_ []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) {
calls.Add(1)
<-releaseReader
return 0, a, io.EOF
},
)
require.NoError(t, receiver.receiveForRtx(
0, "rid", &interceptor.StreamInfo{SSRC: 2222}, nil, repairReader, false, nil, nil,
))
receiver.mu.RLock()
started := receiver.tracks[0].repairReaderStarted
receiver.mu.RUnlock()
assert.True(t, started)
require.Eventually(t, func() bool { return calls.Load() == 1 }, time.Second, time.Millisecond)
}
func TestRTPReceiverRIDRepairReaderStartsForPrimaryWrapper(t *testing.T) {
for _, primaryFirst := range []bool{true, false} {
name := "RTX first"
if primaryFirst {
name = "primary first"
}
t.Run(name, func(t *testing.T) {
api := NewAPI()
receiver, err := api.NewRTPReceiver(RTPCodecTypeVideo, &DTLSTransport{api: api})
require.NoError(t, err)
receiver.configureReceive(RTPReceiveParameters{Encodings: []RTPDecodingParameters{{
RTPCodingParameters: RTPCodingParameters{RID: "rid"},
}}})
close(receiver.received)
t.Cleanup(func() {
assert.NoError(t, receiver.Stop())
})
var repairCalls atomic.Int32
releaseReader := make(chan struct{})
t.Cleanup(func() { close(releaseReader) })
repairReader := interceptor.RTPReaderFunc(
func(_ []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) {
repairCalls.Add(1)
<-releaseReader
return 0, a, io.EOF
},
)
params := RTPParameters{Codecs: []RTPCodecParameters{{
RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeVP8},
PayloadType: 96,
}}}
bindPrimary := func() error {
_, bindErr := receiver.receiveForRid(
"rid",
params,
&interceptor.StreamInfo{SSRC: 1111},
nil,
interceptor.RTPReaderFunc(nil),
true,
nil,
nil,
nil,
)
return bindErr
}
bindRTX := func() error {
return receiver.receiveForRtx(
0,
"rid",
&interceptor.StreamInfo{SSRC: 2222},
nil,
repairReader,
false,
nil,
nil,
)
}
first, second := bindRTX, bindPrimary
if primaryFirst {
first, second = bindPrimary, bindRTX
}
require.NoError(t, first())
receiver.mu.RLock()
startedAfterFirstBind := receiver.tracks[0].repairReaderStarted
receiver.mu.RUnlock()
assert.False(t, startedAfterFirstBind)
assert.Zero(t, repairCalls.Load())
require.NoError(t, second())
track := receiver.Track()
require.NotNil(t, track)
assert.False(t, track.repairReadRequested.Load())
receiver.mu.RLock()
started := receiver.tracks[0].repairReaderStarted
startImmediately := receiver.tracks[0].startRepairReaderImmediately
receiver.mu.RUnlock()
assert.True(t, startImmediately)
assert.True(t, started)
require.Eventually(t, func() bool { return repairCalls.Load() == 1 }, time.Second, time.Millisecond)
})
}
}
func TestRTPReceiverReadAfterStopDoesNotStartRepairReader(t *testing.T) {
receiver, track := newRepairReaderPolicyTestReceiver(t, true, 2222, nil)
var calls atomic.Int32
repairReader := interceptor.RTPReaderFunc(
func(_ []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) {
calls.Add(1)
return 0, a, io.EOF
},
)
require.NoError(t, receiver.receiveForRtx(
2222, "", &interceptor.StreamInfo{SSRC: 2222}, nil, repairReader, false, nil, nil,
))
require.NoError(t, receiver.Stop())
_, _, err := track.Read(make([]byte, receiveMTU))
require.ErrorIs(t, err, io.EOF)
receiver.mu.RLock()
started := receiver.tracks[0].repairReaderStarted
receiver.mu.RUnlock()
assert.False(t, started)
assert.Zero(t, calls.Load())
}
func newRepairReaderPolicyTestReceiver(
t *testing.T,
customBuffer bool,
rtxSSRC SSRC,
primaryReader interceptor.RTPReader,
) (*RTPReceiver, *TrackRemote) {
t.Helper()
transportSettings := SettingEngine{}
if customBuffer {
transportSettings.BufferFactory = func(packetio.BufferPacketType, uint32) io.ReadWriteCloser {
return nil
}
}
transportAPI := NewAPI(WithSettingEngine(transportSettings))
receiverAPI := NewAPI()
receiver, err := receiverAPI.NewRTPReceiver(
RTPCodecTypeVideo,
&DTLSTransport{api: transportAPI},
)
require.NoError(t, err)
receiver.configureReceive(RTPReceiveParameters{Encodings: []RTPDecodingParameters{{
RTPCodingParameters: RTPCodingParameters{
RID: "rid",
SSRC: 1111,
RTX: RTPRtxParameters{SSRC: rtxSSRC},
},
}}})
if primaryReader == nil {
primaryReader = interceptor.RTPReaderFunc(
func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) {
return copy(b, []byte{
0x80, 96, 0, 1, 0, 0, 0, 0, 0, 0, 0x04, 0x57, 0xAA,
}), a, nil
},
)
}
params := RTPParameters{Codecs: []RTPCodecParameters{{
RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeVP8},
PayloadType: 96,
}}}
track, err := receiver.receiveForRid(
"rid", params, &interceptor.StreamInfo{SSRC: 1111}, nil, primaryReader, false, nil, nil, nil,
)
require.NoError(t, err)
close(receiver.received)
t.Cleanup(func() {
assert.NoError(t, receiver.Stop())
})
return receiver, track
}
func TestRTPReceiver_readRTX_ChannelAccessSafe(t *testing.T) {
receiver := &RTPReceiver{
kind: RTPCodecTypeVideo,
received: make(chan any),
closedChan: make(chan any),
rtxPool: sync.Pool{New: func() any {
return make([]byte, 1200)
}},
}
receiver.configureReceive(RTPReceiveParameters{
Encodings: []RTPDecodingParameters{
{
RTPCodingParameters: RTPCodingParameters{
RID: "rid",
SSRC: 1111,
RTX: RTPRtxParameters{
SSRC: 2222,
},
},
},
},
})
params := RTPParameters{
Codecs: []RTPCodecParameters{
{
RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeVP8},
PayloadType: 96,
},
},
}
ridStreamInfo := &interceptor.StreamInfo{SSRC: 1111}
track, err := receiver.receiveForRid("rid", params, ridStreamInfo, nil, nil, false, nil, nil, nil)
require.NoError(t, err)
close(receiver.received)
stop := make(chan struct{})
done := make(chan struct{})
go func() {
defer close(done)
for {
select {
case <-stop:
return
default:
_ = receiver.readRTX(track)
}
}
}()
repairStreamInfo := &interceptor.StreamInfo{SSRC: 2222}
rtpInterceptor := interceptor.RTPReaderFunc(
func(_ []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) {
return 0, a, io.EOF
},
)
for range 50 {
require.NoError(t, receiver.receiveForRtx(
SSRC(2222), "", repairStreamInfo, nil, rtpInterceptor, false, nil, nil,
))
}
close(stop)
<-done
}
func TestRTPReceiver_ReadRTP_SimulcastNoRace(t *testing.T) {
receiver := &RTPReceiver{
kind: RTPCodecTypeVideo,
received: make(chan any),
closedChan: make(chan any),
rtxPool: sync.Pool{New: func() any {
return make([]byte, 1200)
}},
}
receiver.configureReceive(RTPReceiveParameters{
Encodings: []RTPDecodingParameters{
{RTPCodingParameters: RTPCodingParameters{RID: "low", SSRC: 1111}},
{RTPCodingParameters: RTPCodingParameters{RID: "high", SSRC: 2222}},
},
})
params := RTPParameters{
Codecs: []RTPCodecParameters{
{
RTPCodecCapability: RTPCodecCapability{MimeType: MimeTypeVP8},
PayloadType: 96,
},
},
}
lowPkt, err := rtp.Packet{
Header: rtp.Header{
Version: 2,
PayloadType: 96,
SequenceNumber: 1,
Timestamp: 1,
SSRC: 1111,
},
Payload: []byte{0x01},
}.Marshal()
require.NoError(t, err)
lowCh := make(chan []byte, 10)
lowInterceptor := interceptor.RTPReaderFunc(
func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) {
pkt, ok := <-lowCh
if !ok {
return 0, a, io.EOF
}
n := copy(b, pkt)
return n, a, nil
},
)
lowTrack, err := receiver.receiveForRid(
"low", params, &interceptor.StreamInfo{SSRC: 1111}, nil, lowInterceptor, false, nil, nil, nil,
)
require.NoError(t, err)
lowTrack.mu.Lock()
lowTrack.payloadType = 96
lowTrack.codec = params.Codecs[0]
lowTrack.params = params
lowTrack.mu.Unlock()
close(receiver.received)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for range 5 {
_, _, err = lowTrack.Read(make([]byte, 1500))
require.NoError(t, err)
}
}()
repairStreamInfo := &interceptor.StreamInfo{SSRC: 3333}
repairInterceptor := interceptor.RTPReaderFunc(
func(_ []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) {
return 0, a, io.EOF
},
)
require.NoError(t, receiver.receiveForRtx(
SSRC(0), "low", repairStreamInfo, nil, repairInterceptor, false, nil, nil,
))
highInterceptor := interceptor.RTPReaderFunc(
func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) {
return 0, a, io.EOF
},
)
_, err = receiver.receiveForRid(
"high", params, &interceptor.StreamInfo{SSRC: 2222}, nil, highInterceptor, false, nil, nil, nil,
)
require.NoError(t, err)
receiver.tracks[1].track.mu.Lock()
receiver.tracks[1].track.payloadType = 96
receiver.tracks[1].track.codec = params.Codecs[0]
receiver.tracks[1].track.params = params
receiver.tracks[1].track.mu.Unlock()
for range 5 {
lowCh <- lowPkt
}
close(lowCh)
wg.Wait()
}
// TestRTPReceiver_CollectStats_Mapping validates that collectStats maps
// interceptor/pkg/stats values into InboundRTPStreamStats.
func TestRTPReceiver_CollectStats_Mapping(t *testing.T) {
ssrc := SSRC(1234)
now := time.Now()
pr := uint64(math.MaxUint32) + 42
pl := int64(math.MaxInt32) + 7
jitter := 0.123
bytes := uint64(98765)
hdrBytes := uint64(4321)
fir := uint32(3)
pli := uint32(5)
nack := uint32(7)
fg := &fakeGetter{s: stats.Stats{
InboundRTPStreamStats: stats.InboundRTPStreamStats{
ReceivedRTPStreamStats: stats.ReceivedRTPStreamStats{
PacketsReceived: pr,
PacketsLost: pl,
Jitter: jitter,
},
LastPacketReceivedTimestamp: now,
HeaderBytesReceived: hdrBytes,
BytesReceived: bytes,
FIRCount: fir,
PLICount: pli,
NACKCount: nack,
},
}}
// Minimal RTPReceiver with one track
receiver := &RTPReceiver{
kind: RTPCodecTypeVideo,
log: logging.NewDefaultLoggerFactory().NewLogger("RTPReceiverTest"),
}
tr := newTrackRemote(RTPCodecTypeVideo, ssrc, 0, "", receiver)
receiver.tracks = []trackStreams{{track: tr}}
collector := newStatsReportCollector()
receiver.collectStats(collector, nil)
report := collector.Ready()
// Fetch the generated inbound-rtp stat by ID
statID := "inbound-rtp-1234"
_, ok := report[statID]
require.False(t, ok, "unexpected inbound stat")
receiver.collectStats(collector, fg)
report = collector.Ready()
got, ok := report[statID]
require.True(t, ok, "missing inbound stat")
inbound, ok := got.(InboundRTPStreamStats)
require.True(t, ok)
// Wrap-around semantics for casts
assert.Equal(t, uint32(pr), inbound.PacketsReceived) //nolint:gosec
assert.Equal(t, int32(pl), inbound.PacketsLost) //nolint:gosec
assert.Equal(t, jitter, inbound.Jitter)
assert.Equal(t, bytes, inbound.BytesReceived)
assert.Equal(t, hdrBytes, inbound.HeaderBytesReceived)
assert.Equal(t, fir, inbound.FIRCount)
assert.Equal(t, pli, inbound.PLICount)
assert.Equal(t, nack, inbound.NACKCount)
// Timestamp should be set (millisecond precision)
assert.Greater(t, float64(inbound.LastPacketReceivedTimestamp), 0.0)
}
func TestRTPReceiver_CollectStats_AudioPlayoutPull(t *testing.T) {
receiver := &RTPReceiver{
kind: RTPCodecTypeAudio,
log: logging.NewDefaultLoggerFactory().NewLogger("RTPReceiverTest"),
}
track := newTrackRemote(RTPCodecTypeAudio, 7777, 0, "", receiver)
receiver.tracks = []trackStreams{{track: track}}
provider := &fakeAudioPlayoutStatsProvider{
stats: AudioPlayoutStats{
ID: "media-playout-7777",
Type: StatsTypeMediaPlayout,
Kind: string(MediaKindAudio),
TotalSamplesCount: 960,
TotalSamplesDuration: float64(960) / 48000,
TotalPlayoutDelay: 0.5,
},
ok: true,
}
_ = provider.AddTrack(track)
collector := newStatsReportCollector()
receiver.collectStats(collector, &fakeGetter{})
report := collector.Ready()
got, ok := report["media-playout-7777"]
require.True(t, ok, "missing audio playout stats entry")
playout, ok := got.(AudioPlayoutStats)
require.True(t, ok)
assert.Equal(t, provider.stats.TotalSamplesCount, playout.TotalSamplesCount)
assert.Equal(t, provider.stats.TotalSamplesDuration, playout.TotalSamplesDuration)
assert.Equal(t, provider.stats.TotalPlayoutDelay, playout.TotalPlayoutDelay)
assert.NotZero(t, playout.Timestamp)
assert.Equal(t, 1, provider.calls)
}
func TestRTPReceiver_CollectStats_AudioPlayoutSharedProvider(t *testing.T) {
receiver := &RTPReceiver{
kind: RTPCodecTypeAudio,
log: logging.NewDefaultLoggerFactory().NewLogger("RTPReceiverTest"),
}
trackOne := newTrackRemote(RTPCodecTypeAudio, 5555, 0, "", receiver)
trackTwo := newTrackRemote(RTPCodecTypeAudio, 6666, 0, "", receiver)
receiver.tracks = []trackStreams{{track: trackOne}, {track: trackTwo}}
provider := &fakeAudioPlayoutStatsProvider{
stats: AudioPlayoutStats{
ID: "shared-playout",
Type: StatsTypeMediaPlayout,
Kind: string(MediaKindAudio),
TotalSamplesCount: 100,
},
ok: true,
}
_ = provider.AddTrack(trackOne)
_ = provider.AddTrack(trackTwo)
collector := newStatsReportCollector()
receiver.collectStats(collector, &fakeGetter{})
report := collector.Ready()
got, ok := report["shared-playout"]
require.True(t, ok, "shared provider stats missing")
playout, ok := got.(AudioPlayoutStats)
require.True(t, ok)
assert.Equal(t, provider.stats.TotalSamplesCount, playout.TotalSamplesCount)
assert.Equal(t, provider.stats.Type, playout.Type)
assert.Equal(t, provider.stats.Kind, playout.Kind)
assert.Equal(t, provider.stats.ID, playout.ID)
assert.NotZero(t, playout.Timestamp)
assert.Equal(t, 2, provider.calls)
}
func TestRTPReceiver_CollectStats_AudioPlayoutTimestampAlignment(t *testing.T) {
receiver := &RTPReceiver{
kind: RTPCodecTypeAudio,
log: logging.NewDefaultLoggerFactory().NewLogger("RTPReceiverTest"),
}
track := newTrackRemote(RTPCodecTypeAudio, 9999, 0, "", receiver)
receiver.tracks = []trackStreams{{track: track}}
provider := &fakeAudioPlayoutStatsProvider{
stats: AudioPlayoutStats{
ID: "media-playout-9999",
Type: StatsTypeMediaPlayout,
Kind: string(MediaKindAudio),
TotalSamplesCount: 1,
},
ok: true,
}
_ = provider.AddTrack(track)
collector := newStatsReportCollector()
receiver.collectStats(collector, &fakeGetter{})
report := collector.Ready()
got, ok := report["media-playout-9999"]
require.True(t, ok, "playout stats missing")
playout, ok := got.(AudioPlayoutStats)
require.True(t, ok, "playout stats type assertion failed")
require.NotZero(t, provider.lastNow)
assert.Equal(t, statsTimestampFrom(provider.lastNow), playout.Timestamp)
}
type fakeGetter struct{ s stats.Stats }
func (f *fakeGetter) Get(uint32) *stats.Stats { return &f.s }
type fakeAudioPlayoutStatsProvider struct {
stats AudioPlayoutStats
ok bool
calls int
lastNow time.Time
}
func (f *fakeAudioPlayoutStatsProvider) Snapshot(now time.Time) (AudioPlayoutStats, bool) {
f.calls++
f.lastNow = now
return f.stats, f.ok
}
func (f *fakeAudioPlayoutStatsProvider) AddTrack(track *TrackRemote) error {
track.addProvider(f)
return nil
}
func (f *fakeAudioPlayoutStatsProvider) RemoveTrack(track *TrackRemote) {
track.removeProvider(f)
}
func TestRTPReceiverRTXStreamInfoMimeType(t *testing.T) {
for _, tt := range []struct {
name string
configureNack bool
}{
{name: "passthrough"},
{name: "ConfigureNack", configureNack: true},
} {
t.Run(tt.name, func(t *testing.T) {
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
mediaEngine := &MediaEngine{}
require.NoError(t, mediaEngine.RegisterDefaultCodecs())
ir := &interceptor.Registry{}
if tt.configureNack {
require.NoError(t, ConfigureNack(mediaEngine, ir))
}
// Collect the final readers and StreamInfos bound on the receiver side.
var (
boundStreamInfos []*interceptor.StreamInfo
readerWrappedByMimeType = map[string]bool{}
)
mockInterceptor := &mock_interceptor.Interceptor{
BindRemoteStreamFn: func(
info *interceptor.StreamInfo,
reader interceptor.RTPReader,
) interceptor.RTPReader {
boundStreamInfos = append(boundStreamInfos, info)
_, passthrough := reader.(*srtpRTPReader)
readerWrappedByMimeType[info.MimeType] = !passthrough
return reader
},
}
ir.Add(&mock_interceptor.Factory{
NewInterceptorFn: func(_ string) (interceptor.Interceptor, error) { return mockInterceptor, nil },
})
sender, receiver, err := NewAPI(
WithMediaEngine(mediaEngine),
WithInterceptorRegistry(ir),
).newPair(Configuration{})
require.NoError(t, err)
defer closePairNow(t, sender, receiver)
track, err := NewTrackLocalStaticSample(
RTPCodecCapability{MimeType: MimeTypeVP8},
"video",
"pion",
)
require.NoError(t, err)
_, err = sender.AddTrack(track)
require.NoError(t, err)
// OnTrack is reached through internal probing, without a public TrackRemote.Read.
trackReceived, trackReceivedCancel := context.WithCancel(context.Background())
rtpReceiverReceived := make(chan *RTPReceiver, 1)
receiver.OnTrack(func(_ *TrackRemote, rtpReceiver *RTPReceiver) {
rtpReceiverReceived <- rtpReceiver
trackReceivedCancel()
})
require.NoError(t, signalPair(sender, receiver))
func() {
ticker := time.NewTicker(time.Millisecond * 20)
defer ticker.Stop()
for {
select {
case <-trackReceived.Done():
return
case <-ticker.C:
require.NoError(t, track.WriteSample(media.Sample{Data: []byte{0xAA}, Duration: time.Second}))
}
}
}()
rtxCount := 0
for _, info := range boundStreamInfos {
if info.MimeType == MimeTypeRTX {
rtxCount++
}
}
assert.Equal(t, 1, rtxCount,
"expected exactly one RTX StreamInfo with MimeType %q, got %d (all types: %v)",
MimeTypeRTX, rtxCount, mimeTypes(boundStreamInfos))
assert.Equal(t, tt.configureNack, readerWrappedByMimeType[MimeTypeVP8], "primary reader wrapper")
assert.False(t, readerWrappedByMimeType[MimeTypeRTX], "RTX reader wrapper")
rtpReceiver := <-rtpReceiverReceived
rtpReceiver.mu.RLock()
trackCount := len(rtpReceiver.tracks)
repairReaderStarted := trackCount == 1 && rtpReceiver.tracks[0].repairReaderStarted
rtpReceiver.mu.RUnlock()
require.Equal(t, 1, trackCount)
assert.Equal(t, tt.configureNack, repairReaderStarted)
})
}
}
// helper to print all mime types for debugging.
func mimeTypes(infos []*interceptor.StreamInfo) []string {
out := make([]string, len(infos))
for i, info := range infos {
out[i] = info.MimeType
}
return out
}
// TestRTPReceiver_CollectStats_RID validates that collectStats correctly maps RID
// from TrackRemote into InboundRTPStreamStats.
func TestRTPReceiver_CollectStats_RID(t *testing.T) {
ssrc := SSRC(1234)
fg := &fakeGetter{s: stats.Stats{}}
receiver := &RTPReceiver{
kind: RTPCodecTypeVideo,
log: logging.NewDefaultLoggerFactory().NewLogger("RTPReceiverTest"),
}
// Case 1: RID empty
tr := newTrackRemote(RTPCodecTypeVideo, ssrc, 0, "", receiver)
receiver.tracks = []trackStreams{{track: tr}}
collector := newStatsReportCollector()
receiver.collectStats(collector, fg)
report := collector.Ready()
statID := "inbound-rtp-1234"
got, ok := report[statID]
require.True(t, ok)
inbound, ok := got.(InboundRTPStreamStats)
require.True(t, ok)
assert.Equal(t, "", inbound.Rid)
// Case 2: RID present
rid := "f"
tr = newTrackRemote(RTPCodecTypeVideo, ssrc, 0, rid, receiver)
receiver.tracks = []trackStreams{{track: tr}}
collector = newStatsReportCollector()
receiver.collectStats(collector, fg)
report = collector.Ready()
got, ok = report[statID]
require.True(t, ok)
inbound, ok = got.(InboundRTPStreamStats)
require.True(t, ok)
assert.Equal(t, rid, inbound.Rid)
}
func TestRTPReceiverRepairReaderMalformedPacketNoPanic(t *testing.T) {
receiver, track := newRepairReaderPolicyTestReceiver(t, false, 2222, nil)
var reads atomic.Int32
repairReader := interceptor.RTPReaderFunc(
func(b []byte, attributes interceptor.Attributes) (int, interceptor.Attributes, error) {
switch reads.Add(1) {
case 1:
// Zero-length read whose buffer holds garbage (as if recycled
// from a prior packet). Without an i == 0 guard the padding
// length read b[i-1] is b[-1] -> panic.
b[0] = 0xA0 // padding + extension bits set
return 0, attributes, nil
case 2:
// Truncated packet: extension bit set (0x90) but only 12 bytes,
// shorter than the fixed header + extension header. Without a
// length guard the extension length field is parsed from bytes
// beyond the packet.
return copy(b, []byte{
0x90, 97, 0, 1, 0, 0, 0, 0, 0, 0, 0x04, 0x57,
}), attributes, nil
case 3:
// Valid RTX packet: OSN 0x04D2 -> sequence number 1234, payload 0xA1.
return copy(b, []byte{
0x80, 97, 0x13, 0x88, 0, 0, 0, 0, 0, 0, 0x08, 0xAE,
0x04, 0xD2, 0xA1,
}), attributes, nil
default:
// Stop the repair reader once the valid packet was forwarded.
return 0, attributes, io.EOF
}
},
)
require.NoError(t, receiver.receiveForRtx(
2222, "", &interceptor.StreamInfo{SSRC: 2222}, nil, repairReader, true, nil, nil,
))
// The malformed packets must be skipped and the valid packet still forwarded.
require.Eventually(t, func() bool {
receiver.mu.RLock()
defer receiver.mu.RUnlock()
return len(receiver.tracks[0].repairStreamChannel) == 1
}, time.Second, time.Millisecond)
packet, _, err := track.ReadRTP()
require.NoError(t, err)
require.NotNil(t, packet)
assert.Equal(t, uint16(1234), packet.SequenceNumber)
assert.Equal(t, []byte{0xA1}, packet.Payload)
}