snapshot of https://github.com/livekit/livekit.git @ 08c5433449c106943231037d0b892895f6e92d3b (2026-09-15)
This commit is contained in:
175
pkg/sfu/datachannel/bitrate.go
Normal file
175
pkg/sfu/datachannel/bitrate.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package datachannel
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gammazero/deque"
|
||||
|
||||
"github.com/livekit/protocol/utils/mono"
|
||||
)
|
||||
|
||||
const (
|
||||
BitrateDuration = 2 * time.Second
|
||||
BitrateWindow = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
// BitrateMode selects how the sliding-window rate is computed.
|
||||
type BitrateMode int
|
||||
|
||||
const (
|
||||
// BitrateModeBusyOnly divides drained bytes by backlogged ("busy") time
|
||||
// only; idle gaps are excluded from the denominator. All drained bytes count.
|
||||
BitrateModeBusyOnly BitrateMode = iota
|
||||
|
||||
// BitrateModeExcludeIdleDrain is like BitrateModeBusyOnly but also excludes
|
||||
// bytes drained across a non-backlogged interval from the numerator.
|
||||
BitrateModeExcludeIdleDrain
|
||||
|
||||
// BitrateModeWallClock divides all bytes by elapsed wall-clock time. For
|
||||
// consumers with no send buffer, e.g. a receiver measuring its incoming rate.
|
||||
BitrateModeWallClock
|
||||
)
|
||||
|
||||
func (m BitrateMode) String() string {
|
||||
switch m {
|
||||
case BitrateModeBusyOnly:
|
||||
return "busyOnly"
|
||||
case BitrateModeExcludeIdleDrain:
|
||||
return "excludeIdleDrain"
|
||||
case BitrateModeWallClock:
|
||||
return "wallClock"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// BitrateCalculator calculates bitrate over sliding window
|
||||
type BitrateCalculator struct {
|
||||
lock sync.Mutex
|
||||
windowDuration time.Duration
|
||||
duration time.Duration
|
||||
|
||||
windows deque.Deque[bitrateWindow]
|
||||
active bitrateWindow
|
||||
|
||||
bytes int
|
||||
busy time.Duration
|
||||
lastBufferedAmount int
|
||||
last time.Time
|
||||
start time.Time
|
||||
mode BitrateMode
|
||||
}
|
||||
|
||||
// NewBitrateCalculator creates a calculator. See BitrateMode for how mode
|
||||
// governs the numerator and denominator.
|
||||
func NewBitrateCalculator(duration time.Duration, window time.Duration, mode BitrateMode) *BitrateCalculator {
|
||||
windowCnt := int((duration + (window - 1)) / window)
|
||||
if windowCnt == 0 {
|
||||
windowCnt = 1
|
||||
}
|
||||
now := mono.Now()
|
||||
c := &BitrateCalculator{
|
||||
duration: duration,
|
||||
windowDuration: window,
|
||||
start: now,
|
||||
active: bitrateWindow{start: now},
|
||||
mode: mode,
|
||||
}
|
||||
c.windows.SetBaseCap(windowCnt + 1)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *BitrateCalculator) AddBytes(bytes int, bufferedAmout int, ts time.Time) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
// bufferedAmout is sampled right after the write, so it includes the bytes
|
||||
// just enqueued; subtracting them gives the buffer occupancy before the
|
||||
// write. Between writes the buffer only drains, so a non-zero pre-write
|
||||
// level means it stayed backlogged for the whole interval.
|
||||
backlogged := bufferedAmout-bytes > 0
|
||||
var busy time.Duration
|
||||
if !c.last.IsZero() && backlogged {
|
||||
if busy = ts.Sub(c.last); busy < 0 {
|
||||
busy = 0
|
||||
}
|
||||
}
|
||||
c.last = ts
|
||||
|
||||
// bytes that actually drained since the previous sample
|
||||
bytes -= bufferedAmout - c.lastBufferedAmount
|
||||
if bytes < 0 {
|
||||
// it is possible that internal buffering (non-data like DCEP packet from webrtc) caused bytes to be negative
|
||||
bytes = 0
|
||||
}
|
||||
c.lastBufferedAmount = bufferedAmout
|
||||
|
||||
if c.mode == BitrateModeExcludeIdleDrain && !backlogged {
|
||||
// drain across a non-backlogged interval finished at an unknown point,
|
||||
// so it is not a reliable rate sample
|
||||
bytes = 0
|
||||
}
|
||||
|
||||
if ts.Sub(c.active.start) >= c.windowDuration {
|
||||
c.windows.PushBack(c.active)
|
||||
c.active.start = ts
|
||||
c.active.bytes = 0
|
||||
c.active.busy = 0
|
||||
|
||||
for c.windows.Len() > 0 {
|
||||
// pop expired windows
|
||||
if w := c.windows.Front(); ts.Sub(w.start) > (c.duration + c.windowDuration) {
|
||||
c.bytes -= w.bytes
|
||||
c.busy -= w.busy
|
||||
c.windows.PopFront()
|
||||
} else {
|
||||
c.start = w.start
|
||||
break
|
||||
}
|
||||
}
|
||||
if c.windows.Len() == 0 {
|
||||
c.start = ts
|
||||
c.bytes = 0
|
||||
c.busy = 0
|
||||
}
|
||||
}
|
||||
c.bytes += bytes
|
||||
c.active.bytes += bytes
|
||||
c.busy += busy
|
||||
c.active.busy += busy
|
||||
}
|
||||
|
||||
func (c *BitrateCalculator) Bitrate(ts time.Time) (int, bool) {
|
||||
return c.bitrate(ts, false)
|
||||
}
|
||||
|
||||
func (c *BitrateCalculator) ForceBitrate(ts time.Time) (int, bool) {
|
||||
return c.bitrate(ts, true)
|
||||
}
|
||||
|
||||
func (c *BitrateCalculator) bitrate(ts time.Time, force bool) (int, bool) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
// busy modes divide by backlogged time; wall-clock mode by elapsed time
|
||||
denom := c.busy
|
||||
if c.mode == BitrateModeWallClock {
|
||||
denom = ts.Sub(c.start)
|
||||
}
|
||||
if denom < c.windowDuration {
|
||||
if force {
|
||||
denom = c.windowDuration
|
||||
} else {
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
return c.bytes * 8 * 1000 / int(denom.Milliseconds()), true
|
||||
}
|
||||
|
||||
type bitrateWindow struct {
|
||||
start time.Time
|
||||
bytes int
|
||||
busy time.Duration
|
||||
}
|
||||
130
pkg/sfu/datachannel/bitrate_test.go
Normal file
130
pkg/sfu/datachannel/bitrate_test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package datachannel
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var busyModes = []BitrateMode{BitrateModeBusyOnly, BitrateModeExcludeIdleDrain}
|
||||
|
||||
// 1 KB written every second, each fully drained before the next write: never
|
||||
// backlogged, so there is no estimate.
|
||||
func TestBitrateCalculatorSparseFastAck(t *testing.T) {
|
||||
for _, mode := range busyModes {
|
||||
t.Run(mode.String(), func(t *testing.T) {
|
||||
c := NewBitrateCalculator(BitrateDuration, BitrateWindow, mode)
|
||||
require.NotNil(t, c)
|
||||
|
||||
t0 := time.Now()
|
||||
for i := 0; i < 5; i++ {
|
||||
ts := t0.Add(time.Duration(i) * time.Second)
|
||||
c.AddBytes(1000, 1000, ts)
|
||||
_, ok := c.Bitrate(ts)
|
||||
require.False(t, ok)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Standing backlog of 1000 bytes; every 100ms write 800 and 800 drain, sampled
|
||||
// buffer 1000+800=1800. 800 B / 100ms = 64000 bps.
|
||||
func TestBitrateCalculatorSustainedBacklog(t *testing.T) {
|
||||
for _, mode := range busyModes {
|
||||
t.Run(mode.String(), func(t *testing.T) {
|
||||
c := NewBitrateCalculator(BitrateDuration, BitrateWindow, mode)
|
||||
|
||||
t0 := time.Now()
|
||||
c.AddBytes(800, 1800, t0)
|
||||
for i := 1; i <= 30; i++ {
|
||||
c.AddBytes(800, 1800, t0.Add(time.Duration(i)*100*time.Millisecond))
|
||||
}
|
||||
|
||||
bitrate, ok := c.Bitrate(t0.Add(3 * time.Second))
|
||||
require.True(t, ok)
|
||||
require.InDelta(t, 64000, bitrate, 2000)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Buffer only climbs and nothing drains (stalled connection): the estimate
|
||||
// converges to ~0.
|
||||
func TestBitrateCalculatorStalledConnection(t *testing.T) {
|
||||
for _, mode := range busyModes {
|
||||
t.Run(mode.String(), func(t *testing.T) {
|
||||
c := NewBitrateCalculator(BitrateDuration, BitrateWindow, mode)
|
||||
|
||||
t0 := time.Now()
|
||||
buffered := 0
|
||||
for i := 0; i < 20; i++ {
|
||||
buffered += 1000 // wrote 1000, nothing drained
|
||||
c.AddBytes(1000, buffered, t0.Add(time.Duration(i)*50*time.Millisecond))
|
||||
}
|
||||
|
||||
bitrate, ok := c.Bitrate(t0.Add(time.Second))
|
||||
require.True(t, ok)
|
||||
require.Less(t, bitrate, 1000)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A single write that drains to empty is not backlogged at the next sample, so
|
||||
// no estimate is produced.
|
||||
func TestBitrateCalculatorFlushNotCounted(t *testing.T) {
|
||||
for _, mode := range busyModes {
|
||||
t.Run(mode.String(), func(t *testing.T) {
|
||||
c := NewBitrateCalculator(BitrateDuration, BitrateWindow, mode)
|
||||
|
||||
t0 := time.Now()
|
||||
c.AddBytes(100, 100, t0)
|
||||
ts := t0.Add(100 * time.Millisecond)
|
||||
c.AddBytes(0, 0, ts)
|
||||
_, ok := c.Bitrate(ts)
|
||||
require.False(t, ok)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// An idle drain followed by a backlog: BusyOnly counts the idle-drained bytes
|
||||
// over busy time only and reports a higher rate than ExcludeIdleDrain.
|
||||
func TestBitrateCalculatorMixedIdleThenBacklog(t *testing.T) {
|
||||
off := NewBitrateCalculator(BitrateDuration, BitrateWindow, BitrateModeBusyOnly)
|
||||
on := NewBitrateCalculator(BitrateDuration, BitrateWindow, BitrateModeExcludeIdleDrain)
|
||||
|
||||
t0 := time.Now()
|
||||
feed := func(written, buffered int, ts time.Time) {
|
||||
off.AddBytes(written, buffered, ts)
|
||||
on.AddBytes(written, buffered, ts)
|
||||
}
|
||||
|
||||
// 5000 drains during an idle gap (not backlogged), then a sustained backlog
|
||||
// of 800 every 100ms over a standing 5000.
|
||||
feed(5000, 5000, t0)
|
||||
feed(5000, 5000, t0.Add(100*time.Millisecond))
|
||||
for i := 1; i <= 5; i++ {
|
||||
feed(800, 5800, t0.Add(time.Duration(100+i*100)*time.Millisecond))
|
||||
}
|
||||
queryTs := t0.Add(700 * time.Millisecond)
|
||||
|
||||
rOff, okOff := off.Bitrate(queryTs)
|
||||
rOn, okOn := on.Bitrate(queryTs)
|
||||
require.True(t, okOff)
|
||||
require.True(t, okOn)
|
||||
require.Greater(t, rOff, rOn)
|
||||
}
|
||||
|
||||
// No observable buffer: throughput over elapsed time. 1000 B every 100ms =
|
||||
// 80000 bps.
|
||||
func TestBitrateCalculatorWallClock(t *testing.T) {
|
||||
c := NewBitrateCalculator(BitrateDuration, BitrateWindow, BitrateModeWallClock)
|
||||
|
||||
t0 := time.Now()
|
||||
for i := 0; i < 20; i++ {
|
||||
c.AddBytes(1000, 0, t0.Add(time.Duration(i)*100*time.Millisecond))
|
||||
}
|
||||
|
||||
bitrate, ok := c.Bitrate(t0.Add(2 * time.Second))
|
||||
require.True(t, ok)
|
||||
require.InDelta(t, 80000, bitrate, 8000)
|
||||
}
|
||||
152
pkg/sfu/datachannel/datachannel_writer.go
Normal file
152
pkg/sfu/datachannel/datachannel_writer.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package datachannel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pion/datachannel"
|
||||
|
||||
"github.com/livekit/protocol/utils/mono"
|
||||
)
|
||||
|
||||
const (
|
||||
singleWriteTimeout = 50 * time.Millisecond
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDataDroppedBySlowReader = errors.New("data dropped by slow reader")
|
||||
ErrDataDroppedByHighBufferedAmount = errors.New("data dropped due to high buffered amount")
|
||||
)
|
||||
|
||||
type BufferedAmountGetter interface {
|
||||
BufferedAmount() uint64
|
||||
}
|
||||
|
||||
type DataChannelWriter[T BufferedAmountGetter] struct {
|
||||
bufferGetter T
|
||||
rawDC datachannel.ReadWriteCloserDeadliner
|
||||
slowThreshold int
|
||||
rate *BitrateCalculator
|
||||
reliable bool
|
||||
targetLatency time.Duration
|
||||
minBufferedAmount uint64
|
||||
}
|
||||
|
||||
// NewDataChannelWriterReliable creates a new DataChannelWriter for reliable data channel by
|
||||
// detaching it, when writing to the datachanel times out, it will block and retry if the
|
||||
// receiver's bitrate is above the slowThreshold or drop the data if it's below the threshold.
|
||||
// If the slowThreshold is 0, it will never retry on write timeout.
|
||||
func NewDataChannelWriterReliable[T BufferedAmountGetter](bufferGetter T, rawDC datachannel.ReadWriteCloserDeadliner, slowThreshold int) *DataChannelWriter[T] {
|
||||
var rate *BitrateCalculator
|
||||
if slowThreshold > 0 {
|
||||
rate = NewBitrateCalculator(BitrateDuration, BitrateWindow, BitrateModeBusyOnly)
|
||||
}
|
||||
return &DataChannelWriter[T]{
|
||||
bufferGetter: bufferGetter,
|
||||
rawDC: rawDC,
|
||||
slowThreshold: slowThreshold,
|
||||
rate: rate,
|
||||
reliable: true,
|
||||
}
|
||||
}
|
||||
|
||||
// NewDataChannelWriterUnreliable creates a new DataChannelWriter for unreliable data channel.
|
||||
// It will drop data when the buffered amount is too high to maintain the target latency.
|
||||
// The latency is estimated based on the bitrate in past 1 second. If targetLatency is 0, no
|
||||
// buffering control is applied.
|
||||
func NewDataChannelWriterUnreliable[T BufferedAmountGetter](bufferGetter T, rawDC datachannel.ReadWriteCloserDeadliner, targetLatency time.Duration, minBufferedAmount uint64) *DataChannelWriter[T] {
|
||||
var rate *BitrateCalculator
|
||||
if targetLatency > 0 {
|
||||
rate = NewBitrateCalculator(BitrateDuration, BitrateWindow, BitrateModeBusyOnly)
|
||||
}
|
||||
return &DataChannelWriter[T]{
|
||||
bufferGetter: bufferGetter,
|
||||
rawDC: rawDC,
|
||||
rate: rate,
|
||||
targetLatency: targetLatency,
|
||||
minBufferedAmount: minBufferedAmount,
|
||||
reliable: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *DataChannelWriter[T]) BufferedAmountGetter() T {
|
||||
return w.bufferGetter
|
||||
}
|
||||
|
||||
func (w *DataChannelWriter[T]) Write(p []byte) (n int, err error) {
|
||||
if w.reliable {
|
||||
return w.writeReliable(p)
|
||||
} else {
|
||||
return w.writeUnreliable(p)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *DataChannelWriter[T]) writeReliable(p []byte) (n int, err error) {
|
||||
for {
|
||||
err = w.rawDC.SetWriteDeadline(time.Now().Add(singleWriteTimeout))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, err = w.rawDC.Write(p)
|
||||
if w.slowThreshold == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
now := mono.Now()
|
||||
w.rate.AddBytes(n, int(w.bufferGetter.BufferedAmount()), now)
|
||||
// retry if the write timed out on a non-slow receiver
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
if bitrate, ok := w.rate.Bitrate(now); !ok || bitrate >= w.slowThreshold {
|
||||
continue
|
||||
} else {
|
||||
err = fmt.Errorf("%w: bitrate %d, threshold %d", ErrDataDroppedBySlowReader, bitrate, w.slowThreshold)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (w *DataChannelWriter[T]) writeUnreliable(p []byte) (n int, err error) {
|
||||
if w.targetLatency == 0 {
|
||||
err = w.rawDC.SetWriteDeadline(time.Now().Add(singleWriteTimeout))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return w.rawDC.Write(p)
|
||||
}
|
||||
|
||||
if bitrate, ok := w.rate.Bitrate(time.Now()); ok {
|
||||
// control buffer latency to target
|
||||
bufferedAmount := w.bufferGetter.BufferedAmount()
|
||||
targetLatencyLimit := uint64(time.Duration(bitrate) * w.targetLatency / 8 / time.Second)
|
||||
if bufferedAmount > targetLatencyLimit && bufferedAmount > w.minBufferedAmount {
|
||||
return 0, fmt.Errorf(
|
||||
"%w: bitrate %d, buffered amount %d, target latency limit %d, min buffered amount %d",
|
||||
ErrDataDroppedByHighBufferedAmount,
|
||||
bitrate,
|
||||
bufferedAmount,
|
||||
targetLatencyLimit,
|
||||
w.minBufferedAmount,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
err = w.rawDC.SetWriteDeadline(time.Now().Add(singleWriteTimeout))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, err = w.rawDC.Write(p)
|
||||
// Track drained bytes on every write (not just failures) so the bitrate
|
||||
// calculator has samples; otherwise Bitrate() has no data and the
|
||||
// latency-based drop threshold above collapses to the static minBufferedAmount.
|
||||
w.rate.AddBytes(n, int(w.bufferGetter.BufferedAmount()), mono.Now())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (w *DataChannelWriter[T]) Close() error {
|
||||
return w.rawDC.Close()
|
||||
}
|
||||
160
pkg/sfu/datachannel/datachannel_writer_test.go
Normal file
160
pkg/sfu/datachannel/datachannel_writer_test.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package datachannel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
|
||||
"github.com/pion/datachannel"
|
||||
"github.com/pion/transport/v4/deadline"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDataChannelWriter(t *testing.T) {
|
||||
mockDC := newMockDataChannelWriter()
|
||||
// slow threshold is 1000B/s
|
||||
w := NewDataChannelWriterReliable(mockDC, mockDC, 8000)
|
||||
require.Equal(t, mockDC, w.BufferedAmountGetter())
|
||||
buf := make([]byte, 2000)
|
||||
// write 2000 bytes so it should not drop in 2 seconds
|
||||
t0 := time.Now()
|
||||
n, err := w.Write(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2000, n)
|
||||
|
||||
t1 := time.Now()
|
||||
mockDC.SetNextWriteCompleteAt(t0.Add(time.Second * 3 / 2))
|
||||
n, err = w.Write(buf[:10])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 10, n)
|
||||
require.GreaterOrEqual(t, time.Since(t1), time.Second)
|
||||
|
||||
// bitrate below slow threshold(2000bytes/3sec), should drop by timeout
|
||||
mockDC.SetNextWriteCompleteAt(t0.Add(3 * time.Second))
|
||||
n, err = w.Write(buf[:1000])
|
||||
require.ErrorIs(t, err, ErrDataDroppedBySlowReader, err)
|
||||
require.Equal(t, 0, n)
|
||||
}
|
||||
|
||||
func TestDataChannelWriter_NoSlowThreshold(t *testing.T) {
|
||||
mockDC := newMockDataChannelWriter()
|
||||
w := NewDataChannelWriterReliable(mockDC, mockDC, 0)
|
||||
buf := make([]byte, 2000)
|
||||
n, err := w.Write(buf)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2000, n)
|
||||
mockDC.SetNextWriteCompleteAt(time.Now().Add(singleWriteTimeout / 2))
|
||||
n, err = w.Write(buf[:10])
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 10, n)
|
||||
|
||||
// slow threshold is 0, should not block & retry
|
||||
mockDC.SetNextWriteCompleteAt(time.Now().Add(singleWriteTimeout * 2))
|
||||
n, err = w.Write(buf[:1000])
|
||||
require.ErrorIs(t, err, context.DeadlineExceeded, err)
|
||||
require.Equal(t, 0, n)
|
||||
}
|
||||
|
||||
func TestDataChannelWriter_Unreliable(t *testing.T) {
|
||||
mockDC := newMockLossyDataChannelWriter(8192)
|
||||
w := NewDataChannelWriterUnreliable(mockDC, mockDC, 100*time.Millisecond, 2000)
|
||||
for range 10 {
|
||||
buf := make([]byte, 128)
|
||||
_, err := w.Write(buf)
|
||||
require.NoError(t, err)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
buf := make([]byte, 4096)
|
||||
_, err := w.Write(buf)
|
||||
require.NoError(t, err)
|
||||
// should drop due to high buffered amount
|
||||
_, err = w.Write(buf)
|
||||
require.ErrorIs(t, err, ErrDataDroppedByHighBufferedAmount)
|
||||
}
|
||||
|
||||
// mockDataChannelWriter
|
||||
type mockDataChannelWriter struct {
|
||||
datachannel.ReadWriteCloserDeadliner
|
||||
nextWriteCompleteAt time.Time
|
||||
deadline *deadline.Deadline
|
||||
buffered atomic.Int64
|
||||
}
|
||||
|
||||
func newMockDataChannelWriter() *mockDataChannelWriter {
|
||||
return &mockDataChannelWriter{
|
||||
deadline: deadline.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockDataChannelWriter) BufferedAmount() uint64 {
|
||||
return uint64(m.buffered.Load())
|
||||
}
|
||||
|
||||
func (m *mockDataChannelWriter) Write(b []byte) (int, error) {
|
||||
// buffered while the write is in flight, cleared once it completes
|
||||
m.buffered.Store(int64(len(b)))
|
||||
wait := time.Until(m.nextWriteCompleteAt)
|
||||
if wait <= 0 {
|
||||
m.buffered.Store(0)
|
||||
return len(b), nil
|
||||
}
|
||||
select {
|
||||
case <-m.deadline.Done():
|
||||
return 0, m.deadline.Err()
|
||||
case <-time.After(wait):
|
||||
m.buffered.Store(0)
|
||||
return len(b), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockDataChannelWriter) SetWriteDeadline(t time.Time) error {
|
||||
m.deadline.Set(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockDataChannelWriter) SetNextWriteCompleteAt(t time.Time) {
|
||||
m.nextWriteCompleteAt = t
|
||||
}
|
||||
|
||||
// mockLossyDataChannelWriter
|
||||
type mockLossyDataChannelWriter struct {
|
||||
datachannel.ReadWriteCloserDeadliner
|
||||
bufferedAmount atomic.Int64
|
||||
targetBitrate int
|
||||
lastWriteAt time.Time
|
||||
}
|
||||
|
||||
func newMockLossyDataChannelWriter(targetBitrate int) *mockLossyDataChannelWriter {
|
||||
return &mockLossyDataChannelWriter{
|
||||
targetBitrate: targetBitrate,
|
||||
lastWriteAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockLossyDataChannelWriter) BufferedAmount() uint64 {
|
||||
return uint64(m.bufferedAmount.Load())
|
||||
}
|
||||
|
||||
func (m *mockLossyDataChannelWriter) Write(b []byte) (int, error) {
|
||||
m.bufferedAmount.Add(int64(len(b)))
|
||||
if time.Now().Before(m.lastWriteAt) {
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
// drain buffer based on target bitrate
|
||||
canWriteBytes := time.Since(m.lastWriteAt) * time.Duration(m.targetBitrate) / time.Second / 8
|
||||
if m.bufferedAmount.Load() <= int64(canWriteBytes) {
|
||||
m.lastWriteAt = m.lastWriteAt.Add(time.Duration(int64(time.Second) * int64(m.BufferedAmount()) / (int64(m.targetBitrate) / 8)))
|
||||
m.bufferedAmount.Store(0)
|
||||
} else {
|
||||
m.lastWriteAt = time.Now()
|
||||
m.bufferedAmount.Add(-int64(canWriteBytes))
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (m *mockLossyDataChannelWriter) SetWriteDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user