snapshot of https://github.com/pion/webrtc.git (main, 2026-09-15; blobs > 131072B omitted)
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

This commit is contained in:
upstream-mirror
2026-09-15 20:06:07 +05:00
commit 8acb1a3a29
394 changed files with 78746 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
// Package samplebuilder provides functionality to reconstruct media frames from RTP packets.
package samplebuilder
type sampleSequenceLocation struct {
// head is the first packet in a sequence
head uint16
// tail is always set to one after the final sequence number,
// so if head == tail then the sequence is empty
tail uint16
}
func (l sampleSequenceLocation) empty() bool {
return l.head == l.tail
}
func (l sampleSequenceLocation) hasData() bool {
return l.head != l.tail
}
func (l sampleSequenceLocation) count() uint16 {
return seqnumDistance(l.head, l.tail)
}
const (
slCompareVoid = iota
slCompareBefore
slCompareInside
slCompareAfter
)
func (l sampleSequenceLocation) compare(pos uint16) int {
if l.head == l.tail {
return slCompareVoid
}
if l.head < l.tail {
if l.head <= pos && pos < l.tail {
return slCompareInside
}
} else {
if l.head <= pos || pos < l.tail {
return slCompareInside
}
}
if l.head-pos <= pos-l.tail {
return slCompareBefore
}
return slCompareAfter
}

View File

@@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package samplebuilder
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSampleSequenceLocationCompare(t *testing.T) {
s1 := sampleSequenceLocation{32, 42}
assert.Equal(t, slCompareBefore, s1.compare(16))
assert.Equal(t, slCompareInside, s1.compare(32))
assert.Equal(t, slCompareInside, s1.compare(38))
assert.Equal(t, slCompareInside, s1.compare(41))
assert.Equal(t, slCompareAfter, s1.compare(42))
assert.Equal(t, slCompareAfter, s1.compare(0x57))
s2 := sampleSequenceLocation{0xffa0, 32}
assert.Equal(t, slCompareBefore, s2.compare(0xff00))
assert.Equal(t, slCompareInside, s2.compare(0xffa0))
assert.Equal(t, slCompareInside, s2.compare(0xffff))
assert.Equal(t, slCompareInside, s2.compare(0))
assert.Equal(t, slCompareInside, s2.compare(31))
assert.Equal(t, slCompareAfter, s2.compare(32))
assert.Equal(t, slCompareAfter, s2.compare(128))
}

View File

@@ -0,0 +1,402 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
// Package samplebuilder provides functionality to reconstruct media frames from RTP packets.
package samplebuilder
import (
"math"
"time"
"github.com/pion/rtp"
"github.com/pion/webrtc/v4/pkg/media"
)
// SampleBuilder buffers packets until media frames are complete.
type SampleBuilder struct {
maxLate uint16 // how many packets to wait until we get a valid Sample
maxLateTimestamp uint32 // max timestamp between old and new timestamps before dropping packets
buffer [math.MaxUint16 + 1]*rtp.Packet
preparedSamples [math.MaxUint16 + 1]*media.Sample
// Interface that allows us to take RTP packets to samples
depacketizer rtp.Depacketizer
// sampleRate allows us to compute duration of media.SamplecA
sampleRate uint32
// the handler to be called when the builder is about to remove the
// reference to some packet.
packetReleaseHandler func(*rtp.Packet)
// filled contains the head/tail of the packets inserted into the buffer
filled sampleSequenceLocation
// active contains the active head/tail of the timestamp being actively processed
active sampleSequenceLocation
// prepared contains the samples that have been processed to date
prepared sampleSequenceLocation
lastSampleTimestamp *uint32
// number of packets forced to be dropped
droppedPackets uint16
// number of padding packets detected and dropped (this will be a subset of `droppedPackets`)
paddingPackets uint16
// allows inspecting head packets of each sample and then returns a custom metadata
packetHeadHandler func(headPacket any) any
// return array of RTP headers as Sample.RTPHeaders
returnRTPHeaders bool
}
// New constructs a new SampleBuilder.
// maxLate is how long to wait until we can construct a completed media.Sample.
// maxLate is measured in RTP packet sequence numbers.
// A large maxLate will result in less packet loss but higher latency.
// The depacketizer extracts media samples from RTP packets.
// Several depacketizers are available in package github.com/pion/rtp/codecs.
func New(maxLate uint16, depacketizer rtp.Depacketizer, sampleRate uint32, opts ...Option) *SampleBuilder {
s := &SampleBuilder{maxLate: maxLate, depacketizer: depacketizer, sampleRate: sampleRate}
for _, o := range opts {
o(s)
}
return s
}
func (s *SampleBuilder) tooOld(location sampleSequenceLocation) bool {
if s.maxLateTimestamp == 0 {
return false
}
var foundHead *rtp.Packet
var foundTail *rtp.Packet
for i := location.head; i != location.tail; i++ {
if packet := s.buffer[i]; packet != nil {
foundHead = packet
break
}
}
if foundHead == nil {
return false
}
for i := location.tail - 1; i != location.head; i-- {
if packet := s.buffer[i]; packet != nil {
foundTail = packet
break
}
}
if foundTail == nil {
return false
}
return timestampDistance(foundHead.Timestamp, foundTail.Timestamp) > s.maxLateTimestamp
}
// fetchTimestamp returns the timestamp associated with a given sample location.
func (s *SampleBuilder) fetchTimestamp(location sampleSequenceLocation) (timestamp uint32, hasData bool) {
if location.empty() {
return 0, false
}
packet := s.buffer[location.head]
if packet == nil {
return 0, false
}
return packet.Timestamp, true
}
func (s *SampleBuilder) releasePacket(i uint16) {
var p *rtp.Packet
p, s.buffer[i] = s.buffer[i], nil
if p != nil && s.packetReleaseHandler != nil {
s.packetReleaseHandler(p)
}
}
// purgeConsumedBuffers clears all buffers that have already been consumed by
// popping.
func (s *SampleBuilder) purgeConsumedBuffers() {
s.purgeConsumedLocation(s.active, false)
}
// purgeConsumedLocation clears all buffers that have already been consumed
// during a sample building method.
func (s *SampleBuilder) purgeConsumedLocation(consume sampleSequenceLocation, forceConsume bool) {
if !s.filled.hasData() {
return
}
switch consume.compare(s.filled.head) {
case slCompareInside:
if !forceConsume {
break
}
fallthrough
case slCompareBefore:
s.releasePacket(s.filled.head)
s.filled.head++
}
}
// purgeBuffers flushes all buffers that are already consumed or those buffers
// that are too late to consume.
func (s *SampleBuilder) purgeBuffers(flush bool) {
s.purgeConsumedBuffers()
for (s.tooOld(s.filled) || (s.filled.count() > s.maxLate) || flush) && s.filled.hasData() {
if s.active.empty() {
// refill the active based on the filled packets
s.active = s.filled
}
if s.active.hasData() && (s.active.head == s.filled.head) {
// attempt to force the active packet to be consumed even though
// outstanding data may be pending arrival
if s.buildSample(true) != nil {
continue
}
// could not build the sample so drop it
s.active.head++
s.droppedPackets++
}
s.releasePacket(s.filled.head)
s.filled.head++
}
}
// Push adds an RTP Packet to s's buffer.
//
// Push does not copy the input. If you wish to reuse
// this memory make sure to copy before calling Push.
func (s *SampleBuilder) Push(packet *rtp.Packet) {
s.buffer[packet.SequenceNumber] = packet
switch s.filled.compare(packet.SequenceNumber) {
case slCompareVoid:
s.filled.head = packet.SequenceNumber
s.filled.tail = packet.SequenceNumber + 1
case slCompareBefore:
s.filled.head = packet.SequenceNumber
case slCompareAfter:
s.filled.tail = packet.SequenceNumber + 1
case slCompareInside:
break
}
s.purgeBuffers(false)
}
// Flush marks all samples in the buffer to be popped.
func (s *SampleBuilder) Flush() {
s.purgeBuffers(true)
}
const secondToNanoseconds = 1000000000
// buildSample creates a sample from a valid collection of RTP Packets by
// walking forwards building a sample if everything looks good clear and
// update buffer+values
//
//nolint:gocognit,cyclop
func (s *SampleBuilder) buildSample(purgingBuffers bool) *media.Sample {
if s.active.empty() {
s.active = s.filled
}
if s.active.empty() {
return nil
}
if s.filled.compare(s.active.tail) == slCompareInside {
s.active.tail = s.filled.tail
}
var consume sampleSequenceLocation
for i := s.active.head; s.buffer[i] != nil && s.active.compare(i) != slCompareAfter; i++ {
if s.depacketizer.IsPartitionTail(s.buffer[i].Marker, s.buffer[i].Payload) {
consume.head = s.active.head
consume.tail = i + 1
break
}
headTimestamp, hasData := s.fetchTimestamp(s.active)
if hasData && s.buffer[i].Timestamp != headTimestamp {
consume.head = s.active.head
consume.tail = i
break
}
}
if consume.empty() {
return nil
}
if !purgingBuffers && s.buffer[consume.tail] == nil {
// wait for the next packet after this set of packets to arrive
// to ensure at least one post sample timestamp is known
// (unless we have to release right now)
return nil
}
sampleTimestamp, _ := s.fetchTimestamp(s.active)
afterTimestamp := sampleTimestamp
// scan for any packet after the current and use that time stamp as the diff point
for i := consume.tail; i < s.active.tail; i++ {
if s.buffer[i] != nil {
afterTimestamp = s.buffer[i].Timestamp
break
}
}
// the head set of packets is now fully consumed
s.active.head = consume.tail
// prior to decoding all the packets, check if this packet
// would end being disposed anyway
if !s.depacketizer.IsPartitionHead(s.buffer[consume.head].Payload) {
isPadding := false
for i := consume.head; i != consume.tail; i++ {
if s.lastSampleTimestamp != nil && *s.lastSampleTimestamp == s.buffer[i].Timestamp && len(s.buffer[i].Payload) == 0 {
isPadding = true
}
}
s.droppedPackets += consume.count()
if isPadding {
s.paddingPackets += consume.count()
}
s.purgeConsumedLocation(consume, true)
s.purgeConsumedBuffers()
return nil
}
// merge all the buffers into a sample
data := []byte{}
var metadata any
var rtpHeaders []*rtp.Header
for i := consume.head; i != consume.tail; i++ {
payload, err := s.depacketizer.Unmarshal(s.buffer[i].Payload)
if err != nil {
return nil
}
if i == consume.head && s.packetHeadHandler != nil {
metadata = s.packetHeadHandler(s.depacketizer)
}
if s.returnRTPHeaders {
h := s.buffer[i].Header.Clone()
rtpHeaders = append(rtpHeaders, &h)
}
data = append(data, payload...)
}
samples := afterTimestamp - sampleTimestamp
sample := &media.Sample{
Data: data,
Duration: time.Duration((float64(samples)/float64(s.sampleRate))*secondToNanoseconds) * time.Nanosecond,
PacketTimestamp: sampleTimestamp,
PrevDroppedPackets: s.droppedPackets,
Metadata: metadata,
RTPHeaders: rtpHeaders,
}
s.droppedPackets = 0
s.paddingPackets = 0
s.lastSampleTimestamp = new(uint32)
*s.lastSampleTimestamp = sampleTimestamp
s.preparedSamples[s.prepared.tail] = sample
s.prepared.tail++
s.purgeConsumedLocation(consume, true)
s.purgeConsumedBuffers()
return sample
}
// Pop compiles pushed RTP packets into media samples and then
// returns the next valid sample (or nil if no sample is compiled).
func (s *SampleBuilder) Pop() *media.Sample {
_ = s.buildSample(false)
if s.prepared.empty() {
return nil
}
var result *media.Sample
result, s.preparedSamples[s.prepared.head] = s.preparedSamples[s.prepared.head], nil
s.prepared.head++
return result
}
// seqnumDistance computes the distance between two sequence numbers.
func seqnumDistance(x, y uint16) uint16 {
diff := int16(x - y) //nolint:gosec // G115
if diff < 0 {
return uint16(-diff)
}
return uint16(diff)
}
// timestampDistance computes the distance between two timestamps.
func timestampDistance(x, y uint32) uint32 {
diff := int32(x - y) //nolint:gosec // G115
if diff < 0 {
return uint32(-diff)
}
return uint32(diff)
}
// An Option configures a SampleBuilder.
type Option func(o *SampleBuilder)
// WithPacketReleaseHandler set a callback when the builder is about to release
// some packet.
func WithPacketReleaseHandler(h func(*rtp.Packet)) Option {
return func(o *SampleBuilder) {
o.packetReleaseHandler = h
}
}
// WithPacketHeadHandler set a head packet handler to allow inspecting
// the packet to extract certain information and return as custom metadata.
func WithPacketHeadHandler(h func(headPacket any) any) Option {
return func(o *SampleBuilder) {
o.packetHeadHandler = h
}
}
// WithMaxTimeDelay ensures that packets that are too old in the buffer get
// purged based on time rather than building up an extraordinarily long delay.
func WithMaxTimeDelay(maxLateDuration time.Duration) Option {
return func(o *SampleBuilder) {
totalMillis := maxLateDuration.Milliseconds()
o.maxLateTimestamp = uint32(int64(o.sampleRate) * totalMillis / 1000) //nolint:gosec // G5G115
}
}
// WithRTPHeaders enables to collect RTP headers forming a Sample.
// Useful for accessing RTP extensions associated to the Sample.
func WithRTPHeaders(enable bool) Option {
return func(o *SampleBuilder) {
o.returnRTPHeaders = enable
}
}

View File

@@ -0,0 +1,757 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package samplebuilder
import (
"fmt"
"runtime"
"slices"
"sync/atomic"
"testing"
"time"
"github.com/pion/rtp"
"github.com/pion/webrtc/v4/pkg/media"
"github.com/stretchr/testify/assert"
)
type sampleBuilderTest struct {
message string
packets []*rtp.Packet
withHeadChecker bool
withRTPHeader bool
headBytes []byte
samples []*media.Sample
maxLate uint16
maxLateTimestamp uint32
}
type fakeDepacketizer struct {
headChecker bool
headBytes []byte
alwaysHead bool
}
func (f *fakeDepacketizer) Unmarshal(r []byte) ([]byte, error) {
return r, nil
}
func (f *fakeDepacketizer) IsPartitionHead(payload []byte) bool {
if !f.headChecker {
// simulates a bug in the 3.0 version
// the tests should be fixed to not assume the bug
return true
}
// skip padding
if len(payload) < 1 {
return false
}
if f.alwaysHead {
return true
}
return slices.Contains(f.headBytes, payload[0])
}
func (f *fakeDepacketizer) IsPartitionTail(marker bool, _ []byte) bool {
return marker
}
func TestSampleBuilder(t *testing.T) { //nolint:maintidx
testData := []sampleBuilderTest{
{
message: "SampleBuilder shouldn't emit anything if only one RTP packet has been pushed",
packets: []*rtp.Packet{
{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5}, Payload: []byte{0x01}},
},
samples: []*media.Sample{},
maxLate: 50,
maxLateTimestamp: 0,
},
{
//nolint:lll
message: "SampleBuilder shouldn't emit anything if only one RTP packet has been pushed even if the market bit is set",
packets: []*rtp.Packet{
{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5, Marker: true}, Payload: []byte{0x01}},
},
samples: []*media.Sample{},
maxLate: 50,
maxLateTimestamp: 0,
},
{
message: "SampleBuilder should emit two packets, we had three packets with unique timestamps",
packets: []*rtp.Packet{
{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5}, Payload: []byte{0x01}},
{Header: rtp.Header{SequenceNumber: 5001, Timestamp: 6}, Payload: []byte{0x02}},
{Header: rtp.Header{SequenceNumber: 5002, Timestamp: 7}, Payload: []byte{0x03}},
},
samples: []*media.Sample{
{Data: []byte{0x01}, Duration: time.Second, PacketTimestamp: 5},
{Data: []byte{0x02}, Duration: time.Second, PacketTimestamp: 6},
},
maxLate: 50,
maxLateTimestamp: 0,
},
{
message: "SampleBuilder should emit one packet, we had a packet end of sequence marker and run out of space",
packets: []*rtp.Packet{
{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5, Marker: true}, Payload: []byte{0x01}},
{Header: rtp.Header{SequenceNumber: 5002, Timestamp: 7}, Payload: []byte{0x02}},
{Header: rtp.Header{SequenceNumber: 5004, Timestamp: 9}, Payload: []byte{0x03}},
{Header: rtp.Header{SequenceNumber: 5006, Timestamp: 11}, Payload: []byte{0x04}},
{Header: rtp.Header{SequenceNumber: 5008, Timestamp: 13}, Payload: []byte{0x05}},
{Header: rtp.Header{SequenceNumber: 5010, Timestamp: 15}, Payload: []byte{0x06}},
{Header: rtp.Header{SequenceNumber: 5012, Timestamp: 17}, Payload: []byte{0x07}},
},
samples: []*media.Sample{
{Data: []byte{0x01}, Duration: time.Second * 2, PacketTimestamp: 5},
},
maxLate: 5,
maxLateTimestamp: 0,
},
{
message: "SampleBuilder shouldn't emit any packet, we do not have a valid end of sequence and run out of space",
packets: []*rtp.Packet{
{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5}, Payload: []byte{0x01}},
{Header: rtp.Header{SequenceNumber: 5002, Timestamp: 7}, Payload: []byte{0x02}},
{Header: rtp.Header{SequenceNumber: 5004, Timestamp: 9}, Payload: []byte{0x03}},
{Header: rtp.Header{SequenceNumber: 5006, Timestamp: 11}, Payload: []byte{0x04}},
{Header: rtp.Header{SequenceNumber: 5008, Timestamp: 13}, Payload: []byte{0x05}},
{Header: rtp.Header{SequenceNumber: 5010, Timestamp: 15}, Payload: []byte{0x06}},
{Header: rtp.Header{SequenceNumber: 5012, Timestamp: 17}, Payload: []byte{0x07}},
},
samples: []*media.Sample{},
maxLate: 5,
maxLateTimestamp: 0,
},
{
message: "SampleBuilder should emit one packet, we had a packet end of sequence marker and run out of space",
packets: []*rtp.Packet{
{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5, Marker: true}, Payload: []byte{0x01}},
{Header: rtp.Header{SequenceNumber: 5002, Timestamp: 7, Marker: true}, Payload: []byte{0x02}},
{Header: rtp.Header{SequenceNumber: 5004, Timestamp: 9}, Payload: []byte{0x03}},
{Header: rtp.Header{SequenceNumber: 5006, Timestamp: 11}, Payload: []byte{0x04}},
{Header: rtp.Header{SequenceNumber: 5008, Timestamp: 13}, Payload: []byte{0x05}},
{Header: rtp.Header{SequenceNumber: 5010, Timestamp: 15}, Payload: []byte{0x06}},
{Header: rtp.Header{SequenceNumber: 5012, Timestamp: 17}, Payload: []byte{0x07}},
},
samples: []*media.Sample{
{Data: []byte{0x01}, Duration: time.Second * 2, PacketTimestamp: 5},
{Data: []byte{0x02}, Duration: time.Second * 2, PacketTimestamp: 7, PrevDroppedPackets: 1},
},
maxLate: 5,
maxLateTimestamp: 0,
},
{
message: "SampleBuilder should emit one packet, we had two packets but two with duplicate timestamps",
packets: []*rtp.Packet{
{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5}, Payload: []byte{0x01}},
{Header: rtp.Header{SequenceNumber: 5001, Timestamp: 6}, Payload: []byte{0x02}},
{Header: rtp.Header{SequenceNumber: 5002, Timestamp: 6}, Payload: []byte{0x03}},
{Header: rtp.Header{SequenceNumber: 5003, Timestamp: 7}, Payload: []byte{0x04}},
},
samples: []*media.Sample{
{Data: []byte{0x01}, Duration: time.Second, PacketTimestamp: 5},
{Data: []byte{0x02, 0x03}, Duration: time.Second, PacketTimestamp: 6},
},
maxLate: 50,
maxLateTimestamp: 0,
},
{
message: "SampleBuilder shouldn't emit a packet because we have a gap before a valid one",
packets: []*rtp.Packet{
{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5}, Payload: []byte{0x01}},
{Header: rtp.Header{SequenceNumber: 5007, Timestamp: 6}, Payload: []byte{0x02}},
{Header: rtp.Header{SequenceNumber: 5008, Timestamp: 7}, Payload: []byte{0x03}},
},
samples: []*media.Sample{},
maxLate: 50,
maxLateTimestamp: 0,
},
{
message: "SampleBuilder shouldn't emit a packet after a gap as there are gaps and have not reached maxLate yet",
packets: []*rtp.Packet{
{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5}, Payload: []byte{0x01}},
{Header: rtp.Header{SequenceNumber: 5007, Timestamp: 6}, Payload: []byte{0x02}},
{Header: rtp.Header{SequenceNumber: 5008, Timestamp: 7}, Payload: []byte{0x03}},
},
withHeadChecker: true,
headBytes: []byte{0x02},
samples: []*media.Sample{},
maxLate: 50,
maxLateTimestamp: 0,
},
{
message: "SampleBuilder shouldn't emit a packet after a gap if PartitionHeadChecker doesn't assume it head",
packets: []*rtp.Packet{
{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5}, Payload: []byte{0x01}},
{Header: rtp.Header{SequenceNumber: 5007, Timestamp: 6}, Payload: []byte{0x02}},
{Header: rtp.Header{SequenceNumber: 5008, Timestamp: 7}, Payload: []byte{0x03}},
},
withHeadChecker: true,
headBytes: []byte{},
samples: []*media.Sample{},
maxLate: 50,
maxLateTimestamp: 0,
},
{
message: "SampleBuilder should emit multiple valid packets",
packets: []*rtp.Packet{
{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 1}, Payload: []byte{0x01}},
{Header: rtp.Header{SequenceNumber: 5001, Timestamp: 2}, Payload: []byte{0x02}},
{Header: rtp.Header{SequenceNumber: 5002, Timestamp: 3}, Payload: []byte{0x03}},
{Header: rtp.Header{SequenceNumber: 5003, Timestamp: 4}, Payload: []byte{0x04}},
{Header: rtp.Header{SequenceNumber: 5004, Timestamp: 5}, Payload: []byte{0x05}},
{Header: rtp.Header{SequenceNumber: 5005, Timestamp: 6}, Payload: []byte{0x06}},
},
samples: []*media.Sample{
{Data: []byte{0x01}, Duration: time.Second, PacketTimestamp: 1},
{Data: []byte{0x02}, Duration: time.Second, PacketTimestamp: 2},
{Data: []byte{0x03}, Duration: time.Second, PacketTimestamp: 3},
{Data: []byte{0x04}, Duration: time.Second, PacketTimestamp: 4},
{Data: []byte{0x05}, Duration: time.Second, PacketTimestamp: 5},
},
maxLate: 50,
maxLateTimestamp: 0,
},
{
message: "SampleBuilder should skip time stamps too old",
packets: []*rtp.Packet{
{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 1}, Payload: []byte{0x01}},
{Header: rtp.Header{SequenceNumber: 5001, Timestamp: 2}, Payload: []byte{0x02}},
{Header: rtp.Header{SequenceNumber: 5002, Timestamp: 3}, Payload: []byte{0x03}},
{Header: rtp.Header{SequenceNumber: 5013, Timestamp: 4000}, Payload: []byte{0x04}},
{Header: rtp.Header{SequenceNumber: 5014, Timestamp: 4000}, Payload: []byte{0x05}},
{Header: rtp.Header{SequenceNumber: 5015, Timestamp: 4002}, Payload: []byte{0x06}},
{Header: rtp.Header{SequenceNumber: 5016, Timestamp: 7000}, Payload: []byte{0x04}},
{Header: rtp.Header{SequenceNumber: 5017, Timestamp: 7001}, Payload: []byte{0x05}},
},
samples: []*media.Sample{
{Data: []byte{0x04, 0x05}, Duration: time.Second * time.Duration(2), PacketTimestamp: 4000, PrevDroppedPackets: 13},
},
withHeadChecker: true,
headBytes: []byte{0x04},
maxLate: 50,
maxLateTimestamp: 2000,
},
{
message: "Sample builder should recognize padding packets",
packets: []*rtp.Packet{
// 1st packet
{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 1}, Payload: []byte{1}},
// 2nd packet
{Header: rtp.Header{SequenceNumber: 5001, Timestamp: 1}, Payload: []byte{2}},
// 3rd packet
{Header: rtp.Header{SequenceNumber: 5002, Timestamp: 1, Marker: true}, Payload: []byte{3}},
// Padding packet 1
{Header: rtp.Header{SequenceNumber: 5003, Timestamp: 1}, Payload: []byte{}},
// Padding packet 2
{Header: rtp.Header{SequenceNumber: 5004, Timestamp: 1}, Payload: []byte{}},
// 6th packet
{Header: rtp.Header{SequenceNumber: 5005, Timestamp: 3}, Payload: []byte{1}},
// 7th packet
{Header: rtp.Header{SequenceNumber: 5006, Timestamp: 3, Marker: true}, Payload: []byte{7}},
// 7th packet
{Header: rtp.Header{SequenceNumber: 5007, Timestamp: 4}, Payload: []byte{1}},
},
withHeadChecker: true,
headBytes: []byte{1},
samples: []*media.Sample{
{Data: []byte{1, 2, 3}, Duration: 0, PacketTimestamp: 1, PrevDroppedPackets: 0}, // first sample
},
maxLate: 50,
maxLateTimestamp: 2000,
},
{
//nolint:lll
message: "Sample builder should build a sample out of a packet that's both start and end following a run of padding packets",
packets: []*rtp.Packet{
// 1st valid packet
{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 1}, Payload: []byte{1}},
// 2nd valid packet
{Header: rtp.Header{SequenceNumber: 5001, Timestamp: 1, Marker: true}, Payload: []byte{2}},
// 1st padding packet
{Header: rtp.Header{SequenceNumber: 5002, Timestamp: 1}, Payload: []byte{}},
// 2nd padding packet
{Header: rtp.Header{SequenceNumber: 5003, Timestamp: 1}, Payload: []byte{}},
// 3rd valid packet
{Header: rtp.Header{SequenceNumber: 5004, Timestamp: 2, Marker: true}, Payload: []byte{1}},
// 4th valid packet, start of next sample
{Header: rtp.Header{SequenceNumber: 5005, Timestamp: 3}, Payload: []byte{1}},
},
withHeadChecker: true,
headBytes: []byte{1},
samples: []*media.Sample{
{Data: []byte{1, 2}, Duration: 0, PacketTimestamp: 1, PrevDroppedPackets: 0}, // 1st sample
},
maxLate: 50,
maxLateTimestamp: 2000,
},
{
message: "SampleBuilder should emit samples with RTP headers when WithRTPHeaders option is enabled",
packets: []*rtp.Packet{
{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5}, Payload: []byte{0x01}},
{Header: rtp.Header{SequenceNumber: 5001, Timestamp: 6}, Payload: []byte{0x02}},
{Header: rtp.Header{SequenceNumber: 5002, Timestamp: 6}, Payload: []byte{0x03}},
{Header: rtp.Header{SequenceNumber: 5003, Timestamp: 7}, Payload: []byte{0x04}},
},
samples: []*media.Sample{
{Data: []byte{0x01}, Duration: time.Second, PacketTimestamp: 5, RTPHeaders: []*rtp.Header{
{SequenceNumber: 5000, Timestamp: 5},
}},
{Data: []byte{0x02, 0x03}, Duration: time.Second, PacketTimestamp: 6, RTPHeaders: []*rtp.Header{
{SequenceNumber: 5001, Timestamp: 6},
{SequenceNumber: 5002, Timestamp: 6},
}},
},
maxLate: 50,
maxLateTimestamp: 0,
withRTPHeader: true,
},
}
t.Run("Pop", func(t *testing.T) {
assert := assert.New(t)
for _, td := range testData {
var opts []Option
if td.maxLateTimestamp != 0 {
opts = append(opts, WithMaxTimeDelay(
time.Millisecond*time.Duration(int64(td.maxLateTimestamp)),
))
}
if td.withRTPHeader {
opts = append(opts, WithRTPHeaders(true))
}
d := &fakeDepacketizer{
headChecker: td.withHeadChecker,
headBytes: td.headBytes,
}
s := New(td.maxLate, d, 1, opts...)
samples := []*media.Sample{}
for _, p := range td.packets {
s.Push(p)
}
for sample := s.Pop(); sample != nil; sample = s.Pop() {
samples = append(samples, sample)
}
assert.Equal(td.samples, samples, td.message)
}
})
}
// SampleBuilder should respect maxLate if we popped successfully but then have a gap larger then maxLate.
func TestSampleBuilderMaxLate(t *testing.T) {
assert := assert.New(t)
fd := New(50, &fakeDepacketizer{}, 1)
fd.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 0, Timestamp: 1}, Payload: []byte{0x01}})
fd.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 1, Timestamp: 2}, Payload: []byte{0x01}})
fd.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 2, Timestamp: 3}, Payload: []byte{0x01}})
assert.Equal(&media.Sample{
Data: []byte{0x01},
Duration: time.Second,
PacketTimestamp: 1,
}, fd.Pop(), "Failed to build samples before gap")
fd.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 500}, Payload: []byte{0x02}})
fd.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5001, Timestamp: 501}, Payload: []byte{0x02}})
fd.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 5002, Timestamp: 502}, Payload: []byte{0x02}})
assert.Equal(&media.Sample{
Data: []byte{0x01},
Duration: time.Second,
PacketTimestamp: 2,
}, fd.Pop(), "Failed to build samples after large gap")
assert.Equal((*media.Sample)(nil), fd.Pop(), "Failed to build samples after large gap")
fd.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 6000, Timestamp: 600}, Payload: []byte{0x03}})
assert.Equal(&media.Sample{
Data: []byte{0x02},
Duration: time.Second,
PacketTimestamp: 500,
PrevDroppedPackets: 4998,
}, fd.Pop(), "Failed to build samples after large gap")
assert.Equal(&media.Sample{
Data: []byte{0x02},
Duration: time.Second,
PacketTimestamp: 501,
}, fd.Pop(), "Failed to build samples after large gap")
}
func TestSeqnumDistance(t *testing.T) {
testData := []struct {
x uint16
y uint16
d uint16
}{
{0x0001, 0x0003, 0x0002},
{0x0003, 0x0001, 0x0002},
{0xFFF3, 0xFFF1, 0x0002},
{0xFFF1, 0xFFF3, 0x0002},
{0xFFFF, 0x0001, 0x0002},
{0x0001, 0xFFFF, 0x0002},
}
for _, data := range testData {
assert.Equalf(t, data.d, seqnumDistance(data.x, data.y), "seqnumDistance(%d, %d)", data.x, data.y)
}
}
func TestSampleBuilderCleanReference(t *testing.T) {
for _, seqStart := range []uint16{
0,
0xFFF8, // check upper boundary
0xFFFE, // check upper boundary
} {
t.Run(fmt.Sprintf("From%d", seqStart), func(t *testing.T) {
fd := New(10, &fakeDepacketizer{}, 1)
fd.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 0 + seqStart, Timestamp: 0}, Payload: []byte{0x01}})
fd.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 1 + seqStart, Timestamp: 0}, Payload: []byte{0x02}})
fd.Push(&rtp.Packet{Header: rtp.Header{SequenceNumber: 2 + seqStart, Timestamp: 0}, Payload: []byte{0x03}})
pkt4 := &rtp.Packet{Header: rtp.Header{SequenceNumber: 14 + seqStart, Timestamp: 120}, Payload: []byte{0x04}}
fd.Push(pkt4)
pkt5 := &rtp.Packet{Header: rtp.Header{SequenceNumber: 12 + seqStart, Timestamp: 120}, Payload: []byte{0x05}}
fd.Push(pkt5)
for i := range 3 {
assert.Nilf(
t, fd.buffer[(i+int(seqStart))%0x10000],
"Old packet (%d) is not unreferenced (maxLate: 10, pushed: 12)", i,
)
}
assert.Equal(
t, pkt4, fd.buffer[(14+int(seqStart))%0x10000],
"New packet must be referenced after jump",
)
assert.Equal(
t, pkt5, fd.buffer[(12+int(seqStart))%0x10000],
"New packet must be referenced after jump",
)
})
}
}
func TestSampleBuilderPushMaxZero(t *testing.T) {
// Test packets released via 'maxLate' of zero.
pkts := []rtp.Packet{
{Header: rtp.Header{SequenceNumber: 0, Timestamp: 0, Marker: true}, Payload: []byte{0x01}},
}
d := &fakeDepacketizer{
headChecker: true,
headBytes: []byte{0x01},
}
s := New(0, d, 1)
s.Push(&pkts[0])
assert.NotNil(t, s.Pop(), "Should expect a sample")
}
func TestSampleBuilderWithPacketReleaseHandler(t *testing.T) {
var released []*rtp.Packet
fakePacketReleaseHandler := func(p *rtp.Packet) {
released = append(released, p)
}
// Test packets released via 'maxLate'.
pkts := []rtp.Packet{
{Header: rtp.Header{SequenceNumber: 0, Timestamp: 0}, Payload: []byte{0x01}},
{Header: rtp.Header{SequenceNumber: 11, Timestamp: 120}, Payload: []byte{0x02}},
{Header: rtp.Header{SequenceNumber: 12, Timestamp: 121}, Payload: []byte{0x03}},
{Header: rtp.Header{SequenceNumber: 13, Timestamp: 122}, Payload: []byte{0x04}},
{Header: rtp.Header{SequenceNumber: 21, Timestamp: 200}, Payload: []byte{0x05}},
}
fd := New(10, &fakeDepacketizer{}, 1, WithPacketReleaseHandler(fakePacketReleaseHandler))
fd.Push(&pkts[0])
fd.Push(&pkts[1])
assert.NotEmpty(t, released, "Old packet is not released")
assert.Equal(t, pkts[0].SequenceNumber, released[0].SequenceNumber, "Unexpected packet released by maxLate")
// Test packets released after samples built.
fd.Push(&pkts[2])
fd.Push(&pkts[3])
fd.Push(&pkts[4])
assert.NotNil(t, fd.Pop(), "Should have some sample here.")
assert.GreaterOrEqual(t, len(released), 3, "packet built with sample is not released")
assert.Equal(t, pkts[2].SequenceNumber, released[2].SequenceNumber, "Unexpected packet released by samples built")
}
func TestSampleBuilderWithPacketHeadHandler(t *testing.T) {
packets := []*rtp.Packet{
{Header: rtp.Header{SequenceNumber: 5000, Timestamp: 5}, Payload: []byte{0x01}},
{Header: rtp.Header{SequenceNumber: 5001, Timestamp: 5}, Payload: []byte{0x02}},
{Header: rtp.Header{SequenceNumber: 5002, Timestamp: 6}, Payload: []byte{0x01}},
{Header: rtp.Header{SequenceNumber: 5003, Timestamp: 6}, Payload: []byte{0x02}},
{Header: rtp.Header{SequenceNumber: 5004, Timestamp: 7}, Payload: []byte{0x01}},
}
headCount := 0
s := New(10, &fakeDepacketizer{}, 1, WithPacketHeadHandler(func(any) any {
headCount++
return true
}))
for _, pkt := range packets {
s.Push(pkt)
}
for {
sample := s.Pop()
if sample == nil {
break
}
assert.NotNil(t, sample.Metadata, "sample metadata shouldn't be nil")
assert.Equal(t, true, sample.Metadata, "sample metadata should've been set to true")
}
assert.Equal(t, 2, headCount, "two sample heads should have been inspected")
}
func TestSampleBuilderData(t *testing.T) {
fd := New(10, &fakeDepacketizer{
headChecker: true,
alwaysHead: true,
}, 1)
validSamples := 0
for i := range 0x20000 {
packet := rtp.Packet{
Header: rtp.Header{
SequenceNumber: uint16(i), //nolint:gosec // G115
Timestamp: uint32(i + 42), //nolint:gosec // G115
},
Payload: []byte{byte(i)},
}
fd.Push(&packet)
for {
sample := fd.Pop()
if sample == nil {
break
}
assert.Equal(t, sample.PacketTimestamp, uint32(validSamples+42), "timestamp") //nolint:gosec // G115
assert.Equal(t, len(sample.Data), 1, "data length")
assert.Equal(t, byte(validSamples), sample.Data[0], "data")
validSamples++
}
}
// only the last packet should be dropped
assert.Equal(t, validSamples, 0x1FFFF)
}
func TestSampleBuilderPacketUnreference(t *testing.T) {
fd := New(10, &fakeDepacketizer{
headChecker: true,
}, 1)
var refs int64
finalizer := func(*rtp.Packet) {
atomic.AddInt64(&refs, -1)
}
for i := range 0x20000 {
atomic.AddInt64(&refs, 1)
packet := rtp.Packet{
Header: rtp.Header{
SequenceNumber: uint16(i), //nolint:gosec // G115
Timestamp: uint32(i + 42), //nolint:gosec // G115
},
Payload: []byte{byte(i)},
}
runtime.SetFinalizer(&packet, finalizer)
fd.Push(&packet)
for {
sample := fd.Pop()
if sample == nil {
break
}
}
}
runtime.GC()
time.Sleep(10 * time.Millisecond)
remainedRefs := atomic.LoadInt64(&refs)
runtime.KeepAlive(fd)
// only the last packet should be still referenced
assert.Equal(t, int64(1), remainedRefs)
}
func TestSampleBuilder_Flush(t *testing.T) {
fd := New(50, &fakeDepacketizer{
headChecker: true,
headBytes: []byte{0x01},
}, 1)
fd.Push(&rtp.Packet{
Header: rtp.Header{SequenceNumber: 999, Timestamp: 0},
Payload: []byte{0x00},
}) // Invalid packet
// Gap preventing below packets to be processed
fd.Push(&rtp.Packet{
Header: rtp.Header{SequenceNumber: 1001, Timestamp: 1, Marker: true},
Payload: []byte{0x01, 0x11},
}) // Valid packet
fd.Push(&rtp.Packet{
Header: rtp.Header{SequenceNumber: 1011, Timestamp: 10, Marker: true},
Payload: []byte{0x01, 0x12},
}) // Valid packet
assert.Nil(t, fd.Pop(), "Unexpected sample is returned. Test precondition may be broken")
fd.Flush()
samples := []*media.Sample{}
for sample := fd.Pop(); sample != nil; sample = fd.Pop() {
samples = append(samples, sample)
}
expected := []*media.Sample{
{Data: []byte{0x01, 0x11}, Duration: 9 * time.Second, PacketTimestamp: 1, PrevDroppedPackets: 2},
{Data: []byte{0x01, 0x12}, Duration: 0, PacketTimestamp: 10, PrevDroppedPackets: 9},
}
assert.Equal(t, expected, samples)
}
func BenchmarkSampleBuilderSequential(b *testing.B) {
fd := New(100, &fakeDepacketizer{}, 1)
b.ResetTimer()
validSamples := 0
for i := 0; i < b.N; i++ {
packet := rtp.Packet{
Header: rtp.Header{
SequenceNumber: uint16(i), //nolint:gosec // G115
Timestamp: uint32(i + 42), //nolint:gosec // G115
},
Payload: make([]byte, 50),
}
fd.Push(&packet)
for {
s := fd.Pop()
if s == nil {
break
}
validSamples++
}
}
if b.N > 200 && validSamples < b.N-100 {
b.Errorf("Got %v (N=%v)", validSamples, b.N)
}
}
func BenchmarkSampleBuilderLoss(b *testing.B) {
fd := New(100, &fakeDepacketizer{}, 1)
b.ResetTimer()
validSamples := 0
for i := 0; i < b.N; i++ {
if i%13 == 0 {
continue
}
packet := rtp.Packet{
Header: rtp.Header{
SequenceNumber: uint16(i), //nolint:gosec // G115
Timestamp: uint32(i + 42), //nolint:gosec // G115
},
Payload: make([]byte, 50),
}
fd.Push(&packet)
for {
s := fd.Pop()
if s == nil {
break
}
validSamples++
}
}
if b.N > 200 && validSamples < b.N/2-100 {
b.Errorf("Got %v (N=%v)", validSamples, b.N)
}
}
func BenchmarkSampleBuilderReordered(b *testing.B) {
fd := New(100, &fakeDepacketizer{}, 1)
b.ResetTimer()
validSamples := 0
for i := 0; i < b.N; i++ {
packet := rtp.Packet{
Header: rtp.Header{
SequenceNumber: uint16(i ^ 3), //nolint:gosec // G115
Timestamp: uint32((i ^ 3) + 42), //nolint:gosec // G115
},
Payload: make([]byte, 50),
}
fd.Push(&packet)
for {
s := fd.Pop()
if s == nil {
break
}
validSamples++
}
}
if b.N > 2 && validSamples < b.N-5 && validSamples > b.N {
b.Errorf("Got %v (N=%v)", validSamples, b.N)
}
}
func BenchmarkSampleBuilderFragmented(b *testing.B) {
fd := New(100, &fakeDepacketizer{}, 1)
b.ResetTimer()
validSamples := 0
for i := 0; i < b.N; i++ {
packet := rtp.Packet{
Header: rtp.Header{
SequenceNumber: uint16(i), //nolint:gosec // G115
Timestamp: uint32(i/2 + 42), //nolint:gosec // G115
},
Payload: make([]byte, 50),
}
fd.Push(&packet)
for {
s := fd.Pop()
if s == nil {
break
}
validSamples++
}
}
if b.N > 200 && validSamples < b.N/2-100 {
b.Errorf("Got %v (N=%v)", validSamples, b.N)
}
}
func BenchmarkSampleBuilderFragmentedLoss(b *testing.B) {
fd := New(100, &fakeDepacketizer{}, 1)
b.ResetTimer()
validSamples := 0
for i := 0; i < b.N; i++ {
if i%13 == 0 {
continue
}
packet := rtp.Packet{
Header: rtp.Header{
SequenceNumber: uint16(i), //nolint:gosec // G115
Timestamp: uint32(i/2 + 42), //nolint:gosec // G115
},
Payload: make([]byte, 50),
}
fd.Push(&packet)
for {
s := fd.Pop()
if s == nil {
break
}
validSamples++
}
}
if b.N > 200 && validSamples < b.N/3-100 {
b.Errorf("Got %v (N=%v)", validSamples, b.N)
}
}