🎉 live server seems to be working now
This commit is contained in:
115
third_party/sendspin-go/pkg/sendspin/buffer_tracker.go
vendored
Normal file
115
third_party/sendspin-go/pkg/sendspin/buffer_tracker.go
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
// ABOUTME: Per-client buffer tracker for send-ahead pacing
|
||||
// ABOUTME: Tracks in-flight bytes and duration to prevent client buffer overflow
|
||||
package sendspin
|
||||
|
||||
const (
|
||||
// DefaultMaxBufferDurationUs caps buffer duration at 30 seconds
|
||||
// regardless of byte capacity, matching aiosendspin's behavior.
|
||||
DefaultMaxBufferDurationUs = 30_000_000
|
||||
)
|
||||
|
||||
// bufferedChunk records one sent chunk's end time, size, and duration.
|
||||
type bufferedChunk struct {
|
||||
endTimeUs int64
|
||||
byteCount int
|
||||
durationUs int64
|
||||
}
|
||||
|
||||
// BufferTracker tracks the estimated bytes and duration of audio that a
|
||||
// client has buffered (sent but not yet played). The server checks
|
||||
// CanSend before each chunk send and skips the client if the buffer is
|
||||
// full. PruneConsumed removes chunks whose playback time has passed.
|
||||
//
|
||||
// All methods are called from the same goroutine (the audio tick loop)
|
||||
// so no synchronization is needed.
|
||||
type BufferTracker struct {
|
||||
ring []bufferedChunk
|
||||
head int // index of the oldest entry
|
||||
count int // number of valid entries
|
||||
|
||||
bufferedBytes int
|
||||
bufferedDurationUs int64
|
||||
|
||||
capacityBytes int
|
||||
maxDurationUs int64
|
||||
}
|
||||
|
||||
// NewBufferTracker creates a tracker with the given byte capacity and
|
||||
// a 30-second duration cap.
|
||||
func NewBufferTracker(capacityBytes int) *BufferTracker {
|
||||
// Pre-allocate for ~30s of 20ms chunks = 1500 slots.
|
||||
// The ring grows if needed but this avoids early resizes.
|
||||
initialCap := 1500
|
||||
if capacityBytes <= 0 {
|
||||
capacityBytes = 1048576 // 1MB default
|
||||
}
|
||||
return &BufferTracker{
|
||||
ring: make([]bufferedChunk, initialCap),
|
||||
capacityBytes: capacityBytes,
|
||||
maxDurationUs: DefaultMaxBufferDurationUs,
|
||||
}
|
||||
}
|
||||
|
||||
// PruneConsumed removes all chunks whose end time has passed. Call this
|
||||
// once per tick before CanSend.
|
||||
func (t *BufferTracker) PruneConsumed(nowUs int64) {
|
||||
for t.count > 0 {
|
||||
entry := t.ring[t.head]
|
||||
if entry.endTimeUs > nowUs {
|
||||
break
|
||||
}
|
||||
t.bufferedBytes -= entry.byteCount
|
||||
t.bufferedDurationUs -= entry.durationUs
|
||||
t.head = (t.head + 1) % len(t.ring)
|
||||
t.count--
|
||||
}
|
||||
}
|
||||
|
||||
// CanSend reports whether a chunk of the given size and duration can be
|
||||
// sent without exceeding the client's buffer capacity or the duration cap.
|
||||
func (t *BufferTracker) CanSend(byteCount int, durationUs int64) bool {
|
||||
if t.bufferedBytes+byteCount > t.capacityBytes {
|
||||
return false
|
||||
}
|
||||
if t.bufferedDurationUs+durationUs > t.maxDurationUs {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Register records a sent chunk. Call this after CanSend returns true
|
||||
// and the chunk has been enqueued for transmission.
|
||||
func (t *BufferTracker) Register(endTimeUs int64, byteCount int, durationUs int64) {
|
||||
if t.count == len(t.ring) {
|
||||
t.grow()
|
||||
}
|
||||
idx := (t.head + t.count) % len(t.ring)
|
||||
t.ring[idx] = bufferedChunk{
|
||||
endTimeUs: endTimeUs,
|
||||
byteCount: byteCount,
|
||||
durationUs: durationUs,
|
||||
}
|
||||
t.count++
|
||||
t.bufferedBytes += byteCount
|
||||
t.bufferedDurationUs += durationUs
|
||||
}
|
||||
|
||||
// BufferedBytes returns the current estimated bytes in the client's buffer.
|
||||
func (t *BufferTracker) BufferedBytes() int {
|
||||
return t.bufferedBytes
|
||||
}
|
||||
|
||||
// BufferedDurationUs returns the current estimated duration in the client's buffer.
|
||||
func (t *BufferTracker) BufferedDurationUs() int64 {
|
||||
return t.bufferedDurationUs
|
||||
}
|
||||
|
||||
func (t *BufferTracker) grow() {
|
||||
newCap := len(t.ring) * 2
|
||||
newRing := make([]bufferedChunk, newCap)
|
||||
for i := 0; i < t.count; i++ {
|
||||
newRing[i] = t.ring[(t.head+i)%len(t.ring)]
|
||||
}
|
||||
t.ring = newRing
|
||||
t.head = 0
|
||||
}
|
||||
Reference in New Issue
Block a user