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
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:
54
pkg/media/samplebuilder/sampleSequenceLocation.go
Normal file
54
pkg/media/samplebuilder/sampleSequenceLocation.go
Normal 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
|
||||
}
|
||||
29
pkg/media/samplebuilder/sampleSequenceLocation_test.go
Normal file
29
pkg/media/samplebuilder/sampleSequenceLocation_test.go
Normal 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))
|
||||
}
|
||||
402
pkg/media/samplebuilder/samplebuilder.go
Normal file
402
pkg/media/samplebuilder/samplebuilder.go
Normal 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
|
||||
}
|
||||
}
|
||||
757
pkg/media/samplebuilder/samplebuilder_test.go
Normal file
757
pkg/media/samplebuilder/samplebuilder_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user