snapshot of https://github.com/livekit/livekit.git @ 08c5433449c106943231037d0b892895f6e92d3b (2026-09-15)
Some checks failed
Release to Docker / docker (push) Failing after 2s
Release Test Server to Docker / docker (push) Failing after 3s
Test / test (push) Failing after 16s

This commit is contained in:
upstream-mirror
2026-09-15 16:14:22 +05:00
commit 78704103fc
422 changed files with 154805 additions and 0 deletions

View 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
}

View 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)
}

View 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()
}

View 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
}