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:
1 parent
de7ed62d96
commit
f8d0e6fc36
4 files changed
+79
-11
No files matched your search
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user