-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
JitterBuffer: Add a JitterBuffer-based Interceptor
The JitterBufferInterceptor is designed to fit in a RemoteStream pipeline and buffer incoming packets for a short period (currently defaulting to 50 packets) before emitting packets to be consumed by the next step in the pipeline. The caller must ensure they are prepared to handle an ErrPopWhileBuffering in the case that insufficient packets have been received by the jitter buffer. The caller should retry the operation at some point later as the buffer may have been filled in the interim. The caller should also be aware that an ErrBufferUnderrun may be returned in the case that the initial buffering was sufficient and playback began but the caller is consuming packets (or they are not arriving) quickly enough.
- Loading branch information
1 parent
35da023
commit 72c0be8
Showing
9 changed files
with
288 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly> | ||
// SPDX-License-Identifier: MIT | ||
|
||
package jitterbuffer | ||
|
||
import ( | ||
"github.com/pion/logging" | ||
) | ||
|
||
// ReceiverInterceptorOption can be used to configure ReceiverInterceptor | ||
type ReceiverInterceptorOption func(d *ReceiverInterceptor) error | ||
|
||
// Log sets a logger for the interceptor | ||
func Log(log logging.LeveledLogger) ReceiverInterceptorOption { | ||
return func(d *ReceiverInterceptor) error { | ||
d.log = log | ||
return nil | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly> | ||
// SPDX-License-Identifier: MIT | ||
|
||
package jitterbuffer | ||
|
||
import ( | ||
"sync" | ||
|
||
"github.com/pion/interceptor" | ||
"github.com/pion/logging" | ||
"github.com/pion/rtp" | ||
) | ||
|
||
// InterceptorFactory is a interceptor.Factory for a GeneratorInterceptor | ||
type InterceptorFactory struct { | ||
opts []ReceiverInterceptorOption | ||
} | ||
|
||
// NewInterceptor constructs a new ReceiverInterceptor | ||
func (g *InterceptorFactory) NewInterceptor(_ string) (interceptor.Interceptor, error) { | ||
i := &ReceiverInterceptor{ | ||
close: make(chan struct{}), | ||
log: logging.NewDefaultLoggerFactory().NewLogger("jitterbuffer"), | ||
buffer: New(), | ||
} | ||
|
||
for _, opt := range g.opts { | ||
if err := opt(i); err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
return i, nil | ||
} | ||
|
||
// ReceiverInterceptor places a JitterBuffer in the chain to smooth packet arrival | ||
// and allow for network jitter | ||
// | ||
// The Interceptor is designed to fit in a RemoteStream | ||
// pipeline and buffer incoming packets for a short period (currently | ||
// defaulting to 50 packets) before emitting packets to be consumed by the | ||
// next step in the pipeline. | ||
// | ||
// The caller must ensure they are prepared to handle an | ||
// ErrPopWhileBuffering in the case that insufficient packets have been | ||
// received by the jitter buffer. The caller should retry the operation | ||
// at some point later as the buffer may have been filled in the interim. | ||
// | ||
// The caller should also be aware that an ErrBufferUnderrun may be | ||
// returned in the case that the initial buffering was sufficient and | ||
// playback began but the caller is consuming packets (or they are not | ||
// arriving) quickly enough. | ||
type ReceiverInterceptor struct { | ||
interceptor.NoOp | ||
buffer *JitterBuffer | ||
m sync.Mutex | ||
wg sync.WaitGroup | ||
close chan struct{} | ||
log logging.LeveledLogger | ||
} | ||
|
||
// NewInterceptor returns a new InterceptorFactory | ||
func NewInterceptor(opts ...ReceiverInterceptorOption) (*InterceptorFactory, error) { | ||
return &InterceptorFactory{opts}, nil | ||
} | ||
|
||
// BindRemoteStream lets you modify any incoming RTP packets. It is called once for per RemoteStream. The returned method | ||
// will be called once per rtp packet. | ||
func (i *ReceiverInterceptor) BindRemoteStream(_ *interceptor.StreamInfo, reader interceptor.RTPReader) interceptor.RTPReader { | ||
return interceptor.RTPReaderFunc(func(b []byte, a interceptor.Attributes) (int, interceptor.Attributes, error) { | ||
buf := make([]byte, len(b)) | ||
n, attr, err := reader.Read(buf, a) | ||
if err != nil { | ||
return n, attr, err | ||
} | ||
packet := &rtp.Packet{} | ||
if err := packet.Unmarshal(buf); err != nil { | ||
return 0, nil, err | ||
} | ||
i.m.Lock() | ||
defer i.m.Unlock() | ||
i.buffer.Push(packet) | ||
if i.buffer.state == Emitting { | ||
newPkt, err := i.buffer.Pop() | ||
if err != nil { | ||
return 0, nil, err | ||
} | ||
nlen, err := newPkt.MarshalTo(b) | ||
return nlen, attr, err | ||
} | ||
return n, attr, ErrPopWhileBuffering | ||
}) | ||
} | ||
|
||
// UnbindRemoteStream is called when the Stream is removed. It can be used to clean up any data related to that track. | ||
func (i *ReceiverInterceptor) UnbindRemoteStream(_ *interceptor.StreamInfo) { | ||
defer i.wg.Wait() | ||
i.m.Lock() | ||
defer i.m.Unlock() | ||
i.buffer.Clear(true) | ||
} | ||
|
||
// Close closes the interceptor | ||
func (i *ReceiverInterceptor) Close() error { | ||
defer i.wg.Wait() | ||
i.m.Lock() | ||
defer i.m.Unlock() | ||
i.buffer.Clear(true) | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly> | ||
// SPDX-License-Identifier: MIT | ||
|
||
package jitterbuffer | ||
|
||
import ( | ||
"bytes" | ||
"testing" | ||
"time" | ||
|
||
"github.com/pion/interceptor" | ||
"github.com/pion/interceptor/internal/test" | ||
"github.com/pion/logging" | ||
"github.com/pion/rtcp" | ||
"github.com/pion/rtp" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestBufferStart(t *testing.T) { | ||
buf := bytes.Buffer{} | ||
|
||
factory, err := NewInterceptor( | ||
Log(logging.NewDefaultLoggerFactory().NewLogger("test")), | ||
) | ||
assert.NoError(t, err) | ||
|
||
i, err := factory.NewInterceptor("") | ||
assert.NoError(t, err) | ||
|
||
assert.Zero(t, buf.Len()) | ||
|
||
stream := test.NewMockStream(&interceptor.StreamInfo{ | ||
SSRC: 123456, | ||
ClockRate: 90000, | ||
}, i) | ||
defer func() { | ||
assert.NoError(t, stream.Close()) | ||
}() | ||
|
||
stream.ReceiveRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{ | ||
SenderSSRC: 123, | ||
MediaSSRC: 456, | ||
}}) | ||
stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{ | ||
SequenceNumber: uint16(0), | ||
}}) | ||
|
||
// Give time for packets to be handled and stream written to. | ||
time.Sleep(50 * time.Millisecond) | ||
select { | ||
case pkt := <-stream.ReadRTP(): | ||
assert.EqualValues(t, nil, pkt) | ||
default: | ||
// No data ready to read, this is what we expect | ||
} | ||
err = i.Close() | ||
assert.NoError(t, err) | ||
assert.Zero(t, buf.Len()) | ||
} | ||
|
||
func TestReceiverBuffersAndPlaysout(t *testing.T) { | ||
buf := bytes.Buffer{} | ||
|
||
factory, err := NewInterceptor( | ||
Log(logging.NewDefaultLoggerFactory().NewLogger("test")), | ||
) | ||
assert.NoError(t, err) | ||
|
||
i, err := factory.NewInterceptor("") | ||
assert.NoError(t, err) | ||
|
||
assert.EqualValues(t, 0, buf.Len()) | ||
|
||
stream := test.NewMockStream(&interceptor.StreamInfo{ | ||
SSRC: 123456, | ||
ClockRate: 90000, | ||
}, i) | ||
|
||
stream.ReceiveRTCP([]rtcp.Packet{&rtcp.PictureLossIndication{ | ||
SenderSSRC: 123, | ||
MediaSSRC: 456, | ||
}}) | ||
for s := 0; s < 61; s++ { | ||
stream.ReceiveRTP(&rtp.Packet{Header: rtp.Header{ | ||
SequenceNumber: uint16(s), | ||
}}) | ||
} | ||
// Give time for packets to be handled and stream written to. | ||
time.Sleep(50 * time.Millisecond) | ||
for s := 0; s < 10; s++ { | ||
read := <-stream.ReadRTP() | ||
seq := read.Packet.Header.SequenceNumber | ||
assert.EqualValues(t, uint16(s), seq) | ||
} | ||
assert.NoError(t, stream.Close()) | ||
err = i.Close() | ||
assert.NoError(t, err) | ||
} |