telegabber/calls/audio/playout.go
2026-05-25 06:27:20 -07:00

76 lines
1.9 KiB
Go

package audio
import (
"errors"
"sync"
"github.com/pion/interceptor/pkg/jitterbuffer"
"github.com/pion/rtp"
)
// pion's jitterbuffer + late-drop on Push + skip-ahead on persistent gap;
// PopOrSkip reports gap counts so the caller can run PLC over them
type Playout struct {
mu sync.Mutex
jb *jitterbuffer.JitterBuffer
started bool // first Pop succeeded -> headSeq is meaningful
headSeq uint16 // expected next seq
// caps the scan in PopOrSkip's slow path
MaxSkip uint16
}
var ErrEmpty = errors.New("playout: empty")
// primePackets=2 is ~40ms at 20ms framing
func NewPlayout(primePackets uint16) *Playout {
if primePackets == 0 {
primePackets = 2
}
return &Playout{
jb: jitterbuffer.New(jitterbuffer.WithMinimumPacketCount(primePackets)),
MaxSkip: 64,
}
}
// late packets (seq behind headSeq) are dropped
func (p *Playout) Push(pkt *rtp.Packet) {
if pkt == nil {
return
}
p.mu.Lock()
defer p.mu.Unlock()
// int16 cast -> signed seq distance
if p.started && int16(pkt.SequenceNumber-p.headSeq) < 0 {
return
}
p.jb.Push(pkt)
}
// returns (next playable packet, gap count to PLC over, err);
// ErrEmpty means priming or genuinely empty
func (p *Playout) PopOrSkip() (*rtp.Packet, int, error) {
p.mu.Lock()
defer p.mu.Unlock()
if pkt, err := p.jb.Pop(); err == nil {
p.started = true
p.headSeq = pkt.SequenceNumber + 1
return pkt, 0, nil
} else if errors.Is(err, jitterbuffer.ErrPopWhileBuffering) {
return nil, 0, ErrEmpty
}
// scan forward for the next available packet
head := p.jb.PlayoutHead()
for i := uint16(1); i <= p.MaxSkip; i++ {
pkt, err := p.jb.PopAtSequence(head + i)
if err == nil {
// PopAtSequence advances by 1; head must skip past the gap
p.jb.SetPlayoutHead(pkt.SequenceNumber + 1)
p.started = true
p.headSeq = pkt.SequenceNumber + 1
return pkt, int(i), nil
}
}
return nil, 0, ErrEmpty
}