|
|
@@ -25,6 +25,8 @@ type Runtime struct { |
|
|
prebufferFrames int |
|
|
prebufferFrames int |
|
|
gateOpen bool |
|
|
gateOpen bool |
|
|
seenChunk bool |
|
|
seenChunk bool |
|
|
|
|
|
lastDrainAt time.Time |
|
|
|
|
|
drainAllowance float64 |
|
|
|
|
|
|
|
|
mu sync.RWMutex |
|
|
mu sync.RWMutex |
|
|
active SourceDescriptor |
|
|
active SourceDescriptor |
|
|
@@ -121,6 +123,8 @@ func (r *Runtime) Start(ctx context.Context) error { |
|
|
r.stats.WriteBlocked = false |
|
|
r.stats.WriteBlocked = false |
|
|
r.gateOpen = false |
|
|
r.gateOpen = false |
|
|
r.seenChunk = false |
|
|
r.seenChunk = false |
|
|
|
|
|
r.lastDrainAt = time.Now() |
|
|
|
|
|
r.drainAllowance = 0 |
|
|
r.work.reset() |
|
|
r.work.reset() |
|
|
r.updateBufferedStatsLocked() |
|
|
r.updateBufferedStatsLocked() |
|
|
r.mu.Unlock() |
|
|
r.mu.Unlock() |
|
|
@@ -241,7 +245,9 @@ func (r *Runtime) handleChunk(chunk PCMChunk) { |
|
|
func (r *Runtime) drainWorkingBuffer() { |
|
|
func (r *Runtime) drainWorkingBuffer() { |
|
|
r.mu.Lock() |
|
|
r.mu.Lock() |
|
|
defer r.mu.Unlock() |
|
|
defer r.mu.Unlock() |
|
|
|
|
|
now := time.Now() |
|
|
if r.sink == nil { |
|
|
if r.sink == nil { |
|
|
|
|
|
r.resetDrainPacerLocked(now) |
|
|
r.updateBufferedStatsLocked() |
|
|
r.updateBufferedStatsLocked() |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
@@ -258,20 +264,25 @@ func (r *Runtime) drainWorkingBuffer() { |
|
|
} |
|
|
} |
|
|
r.stats.Prebuffering = false |
|
|
r.stats.Prebuffering = false |
|
|
r.stats.WriteBlocked = false |
|
|
r.stats.WriteBlocked = false |
|
|
|
|
|
r.resetDrainPacerLocked(now) |
|
|
r.updateBufferedStatsLocked() |
|
|
r.updateBufferedStatsLocked() |
|
|
return |
|
|
return |
|
|
case r.prebufferFrames > 0 && bufferedFrames < r.prebufferFrames: |
|
|
case r.prebufferFrames > 0 && bufferedFrames < r.prebufferFrames: |
|
|
r.stats.State = "prebuffering" |
|
|
r.stats.State = "prebuffering" |
|
|
r.stats.Prebuffering = true |
|
|
r.stats.Prebuffering = true |
|
|
r.stats.WriteBlocked = false |
|
|
r.stats.WriteBlocked = false |
|
|
|
|
|
r.resetDrainPacerLocked(now) |
|
|
r.updateBufferedStatsLocked() |
|
|
r.updateBufferedStatsLocked() |
|
|
return |
|
|
return |
|
|
default: |
|
|
default: |
|
|
r.gateOpen = true |
|
|
r.gateOpen = true |
|
|
|
|
|
r.resetDrainPacerLocked(now) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
writeBlocked := false |
|
|
writeBlocked := false |
|
|
for r.work.available() > 0 { |
|
|
|
|
|
|
|
|
limit := r.pacedDrainLimitLocked(now, bufferedFrames) |
|
|
|
|
|
written := 0 |
|
|
|
|
|
for written < limit && r.work.available() > 0 { |
|
|
frame, ok := r.work.peek() |
|
|
frame, ok := r.work.peek() |
|
|
if !ok { |
|
|
if !ok { |
|
|
break |
|
|
break |
|
|
@@ -281,10 +292,18 @@ func (r *Runtime) drainWorkingBuffer() { |
|
|
break |
|
|
break |
|
|
} |
|
|
} |
|
|
r.work.pop() |
|
|
r.work.pop() |
|
|
|
|
|
written++ |
|
|
|
|
|
} |
|
|
|
|
|
if written > 0 { |
|
|
|
|
|
r.drainAllowance -= float64(written) |
|
|
|
|
|
if r.drainAllowance < 0 { |
|
|
|
|
|
r.drainAllowance = 0 |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
if r.work.available() == 0 && r.prebufferFrames > 0 { |
|
|
if r.work.available() == 0 && r.prebufferFrames > 0 { |
|
|
// Re-arm the gate after dry-out to rebuild margin before resuming. |
|
|
// Re-arm the gate after dry-out to rebuild margin before resuming. |
|
|
r.gateOpen = false |
|
|
r.gateOpen = false |
|
|
|
|
|
r.resetDrainPacerLocked(now) |
|
|
} |
|
|
} |
|
|
r.stats.Prebuffering = false |
|
|
r.stats.Prebuffering = false |
|
|
r.stats.WriteBlocked = writeBlocked |
|
|
r.stats.WriteBlocked = writeBlocked |
|
|
@@ -296,6 +315,62 @@ func (r *Runtime) drainWorkingBuffer() { |
|
|
r.updateBufferedStatsLocked() |
|
|
r.updateBufferedStatsLocked() |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (r *Runtime) pacedDrainLimitLocked(now time.Time, bufferedFrames int) int { |
|
|
|
|
|
if bufferedFrames <= 0 { |
|
|
|
|
|
return 0 |
|
|
|
|
|
} |
|
|
|
|
|
rate := r.workSampleRate |
|
|
|
|
|
if r.sink != nil && r.sink.SampleRate > 0 { |
|
|
|
|
|
rate = r.sink.SampleRate |
|
|
|
|
|
} |
|
|
|
|
|
if rate <= 0 { |
|
|
|
|
|
return bufferedFrames |
|
|
|
|
|
} |
|
|
|
|
|
if !r.lastDrainAt.IsZero() { |
|
|
|
|
|
elapsed := now.Sub(r.lastDrainAt) |
|
|
|
|
|
if elapsed > 0 { |
|
|
|
|
|
r.drainAllowance += elapsed.Seconds() * float64(rate) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
r.lastDrainAt = now |
|
|
|
|
|
maxAllowance := maxInt(1, rate/5) // cap accumulated credit at 200 ms |
|
|
|
|
|
if r.drainAllowance > float64(maxAllowance) { |
|
|
|
|
|
r.drainAllowance = float64(maxAllowance) |
|
|
|
|
|
} |
|
|
|
|
|
limit := int(r.drainAllowance) |
|
|
|
|
|
if limit <= 0 { |
|
|
|
|
|
return 0 |
|
|
|
|
|
} |
|
|
|
|
|
maxBurst := maxInt(1, rate/50) // max 20 ms worth of frames per drain call |
|
|
|
|
|
if limit > maxBurst { |
|
|
|
|
|
limit = maxBurst |
|
|
|
|
|
} |
|
|
|
|
|
sinkStats := r.sink.Stats() |
|
|
|
|
|
headroom := sinkStats.Capacity - sinkStats.Available |
|
|
|
|
|
if headroom < 0 { |
|
|
|
|
|
headroom = 0 |
|
|
|
|
|
} |
|
|
|
|
|
if limit > headroom { |
|
|
|
|
|
limit = headroom |
|
|
|
|
|
} |
|
|
|
|
|
if limit > bufferedFrames { |
|
|
|
|
|
limit = bufferedFrames |
|
|
|
|
|
} |
|
|
|
|
|
return limit |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func (r *Runtime) resetDrainPacerLocked(now time.Time) { |
|
|
|
|
|
r.lastDrainAt = now |
|
|
|
|
|
r.drainAllowance = 0 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func maxInt(a, b int) int { |
|
|
|
|
|
if a > b { |
|
|
|
|
|
return a |
|
|
|
|
|
} |
|
|
|
|
|
return b |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
func (r *Runtime) updateBufferedStatsLocked() { |
|
|
func (r *Runtime) updateBufferedStatsLocked() { |
|
|
available := r.work.available() |
|
|
available := r.work.available() |
|
|
capacity := r.work.capacity() |
|
|
capacity := r.work.capacity() |
|
|
|