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 }