Skip RTP padding packets instead of concealing them

Padding/probe packets use a sequence number but carry no audio. They were
treated as lost, so each one played an invented 20 ms frame (a glitch) and
pushed the buffer a frame deeper for drift correction to remove again.
Signature in the first live test: lost ~= drift-drops (74 vs 72 per minute).
They are now skipped and counted as 'padding'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
namandClaude Opus 5.5 committed 2026-11-04 11:22:00 -06:00
1 parent de7ed62d96
commit f8d0e6fc36
4 files changed
+79 -11

No files matched your search

+28 -9
View File
@@ -266,8 +266,8 @@ func (c *Call) Close() {
c.jbMu.Unlock()
if jb != nil {
k, _, t := jb.snapshot()
log.Printf("call totals: lost %d (fec %d) concealed %d late %d underruns %d drift-drops %d inserted %d, final target %d ms",
k.Lost, k.FEC, k.PLC, k.Late, k.Underruns, k.Dropped, k.Inserted, t)
log.Printf("call totals: lost %d (fec %d) concealed %d late %d underruns %d drift-drops %d inserted %d padding %d, final target %d ms",
k.Lost, k.FEC, k.PLC, k.Late, k.Underruns, k.Dropped, k.Inserted, k.Padding, t)
}
})
}
@@ -285,8 +285,8 @@ func (c *Call) logRx(jb *jitterBuffer) {
case <-t.C:
}
k, d, tg := jb.snapshot()
log.Printf("rx 1m: buffer %d/%d ms, lost %d (fec %d), concealed %d, late %d, underruns %d, drift-drops %d, inserted %d",
d, tg, k.Lost-prev.Lost, k.FEC-prev.FEC, k.PLC-prev.PLC, k.Late-prev.Late, k.Underruns-prev.Underruns, k.Dropped-prev.Dropped, k.Inserted-prev.Inserted)
log.Printf("rx 1m: buffer %d/%d ms, lost %d (fec %d), concealed %d, late %d, underruns %d, drift-drops %d, inserted %d, padding %d",
d, tg, k.Lost-prev.Lost, k.FEC-prev.FEC, k.PLC-prev.PLC, k.Late-prev.Late, k.Underruns-prev.Underruns, k.Dropped-prev.Dropped, k.Inserted-prev.Inserted, k.Padding-prev.Padding)
prev = k
}
}
@@ -411,7 +411,7 @@ func (c *Call) startPlay(t *webrtc.TrackRemote) {
log.Printf("decoder: %v", err)
return
}
jb := newJitterBuffer(dec)
jb := newJitterBuffer(dec, uint8(t.PayloadType()))
c.jbMu.Lock()
c.jb = jb
c.jbMu.Unlock()
@@ -529,6 +529,8 @@ type jitterBuffer struct {
mu sync.Mutex
dec *opus.Decoder
pkts map[uint16][]byte
skip map[uint16]bool // sequence numbers that carry no audio (padding/probes)
pt uint8 // the negotiated Opus payload type
nextSeq uint16
haveSeq bool // nextSeq is meaningful
playing bool // false = filling up to target
@@ -545,6 +547,7 @@ type jitterBuffer struct {
type jbCounters struct {
Lost, FEC, PLC, Dropped, Inserted, Underruns, Late int
Padding int // packets with no audio (skipped, not concealed)
}
const (
@@ -567,14 +570,15 @@ func (j *jitterBuffer) isQuiet(pcm []int16) bool {
return r < lim
}
func newJitterBuffer(dec *opus.Decoder) *jitterBuffer {
return &jitterBuffer{dec: dec, pkts: map[uint16][]byte{}, target: jbFloor, pcm: make([]int16, 5760*channels), lastBad: time.Now(), floor: quietRMS}
func newJitterBuffer(dec *opus.Decoder, pt uint8) *jitterBuffer {
return &jitterBuffer{dec: dec, pt: pt, pkts: map[uint16][]byte{}, skip: map[uint16]bool{}, target: jbFloor, pcm: make([]int16, 5760*channels), lastBad: time.Now(), floor: quietRMS}
}
func seqLess(a, b uint16) bool { return int16(a-b) < 0 }
func (j *jitterBuffer) reset() {
j.pkts = map[uint16][]byte{}
j.skip = map[uint16]bool{}
j.haveSeq, j.playing, j.lateRun, j.insertNow = false, false, 0, false
}
@@ -591,7 +595,17 @@ func (j *jitterBuffer) bump() {
func (j *jitterBuffer) push(p *rtp.Packet) {
j.mu.Lock()
defer j.mu.Unlock()
if len(p.Payload) == 0 {
if len(p.Payload) == 0 || (j.pt != 0 && p.PayloadType != j.pt) {
// Padding / bandwidth probe: it uses a sequence number but carries no audio and no
// time. v1/v2a ignored it, which left a hole that was then "concealed" - an
// invented 20 ms frame that the drift correction later had to remove again.
j.c.Padding++
if !j.haveSeq || !seqLess(p.SequenceNumber, j.nextSeq) {
if len(j.skip) > 200 {
j.skip = map[uint16]bool{}
}
j.skip[p.SequenceNumber] = true
}
return
}
if j.haveSeq {
@@ -695,6 +709,11 @@ func (j *jitterBuffer) next() []int16 {
}
for {
if j.skip[j.nextSeq] {
delete(j.skip, j.nextSeq)
j.nextSeq++
continue
}
if p, ok := j.pkts[j.nextSeq]; ok {
delete(j.pkts, j.nextSeq)
j.nextSeq++
@@ -744,6 +763,6 @@ func (j *jitterBuffer) stats() map[string]any {
return map[string]any{
"depthMs": d, "targetMs": t,
"lost": c.Lost, "fec": c.FEC, "plc": c.PLC, "dropped": c.Dropped, "inserted": c.Inserted,
"underruns": c.Underruns, "late": c.Late,
"underruns": c.Underruns, "late": c.Late, "padding": c.Padding,
}
}