telegabber/calls/audio/xmpp_to_tg_test.go
2026-05-27 08:37:47 -07:00

152 lines
4.1 KiB
Go

package audio
import (
"testing"
"github.com/pion/rtp"
)
func newTestXmppToTg(t *testing.T) (*XmppToTg, *fakeNtg) {
t.Helper()
n := &fakeNtg{}
h, err := NewXmppToTg(XmppToTgOptions{
Ntg: n,
ChatID: 12345,
})
if err != nil {
t.Fatalf("NewXmppToTg: %v", err)
}
return h, n
}
func TestAppendChunks10ms(t *testing.T) {
// One 20ms decode -> two 10ms chunks.
pcm := make([]int16, SamplesPerOpusFrame*Channels)
got := appendChunks10ms(nil, pcm)
if len(got) != 2 {
t.Fatalf("got %d chunks for 20ms, want 2", len(got))
}
for i, c := range got {
if len(c) != NtgFrameBytes {
t.Errorf("chunk %d len=%d, want %d", i, len(c), NtgFrameBytes)
}
}
// Partial trailing samples are dropped (a 15ms slice yields one 10ms chunk).
partial := make([]int16, (SamplesPerNtgFrame+SamplesPerNtgFrame/2)*Channels)
got = appendChunks10ms(nil, partial)
if len(got) != 1 {
t.Errorf("partial slice: got %d chunks, want 1", len(got))
}
}
// encodeOpusFrame returns a real opus packet encoding a 440Hz sine wave.
func encodeOpusFrame(t *testing.T, enc *Encoder) []byte {
t.Helper()
pcm := makeSineStereo(440)
buf := make([]byte, MaxOpusPacketBytes)
n, err := enc.Encode(pcm, buf)
if err != nil {
t.Fatalf("encode: %v", err)
}
return buf[:n]
}
// rtpWithSeq wraps payload in an RTP packet with the given seq.
func rtpWithSeq(seq uint16, payload []byte) *rtp.Packet {
return &rtp.Packet{
Header: rtp.Header{
SequenceNumber: seq,
PayloadType: opusPayloadType,
},
Payload: payload,
}
}
func TestProcessTickSilenceWhenEmpty(t *testing.T) {
h, n := newTestXmppToTg(t)
st := newTickState()
// Buffer is empty + not primed: each tick should ship one silence frame.
for i := 0; i < 3; i++ {
h.processTick(st)
}
if got := n.sentCount(); got != 3 {
t.Errorf("expected 3 silence frames, got %d", got)
}
// All sent buffers should equal h.silence.
n.mu.Lock()
defer n.mu.Unlock()
for i, p := range n.sentPCM {
for j, x := range p {
if x != 0 {
t.Fatalf("frame %d byte %d = %x, want 0 (silence)", i, j, x)
return
}
}
}
}
func TestProcessTickDecodesAndSplits(t *testing.T) {
h, n := newTestXmppToTg(t)
// Push enough packets to prime the buffer (default 2).
enc, err := NewEncoder()
if err != nil {
t.Fatalf("NewEncoder: %v", err)
}
for seq := uint16(100); seq < 102; seq++ {
h.playout.Push(rtpWithSeq(seq, encodeOpusFrame(t, enc)))
}
st := newTickState()
// First tick: pops packet 100 (20ms PCM = 2 chunks queued), ships chunk 1.
h.processTick(st)
// Second tick: ships chunk 2 from same decode (still pending).
h.processTick(st)
// Third tick: pops packet 101, ships its first chunk.
h.processTick(st)
// Fourth tick: ships packet 101's second chunk.
h.processTick(st)
if got := n.sentCount(); got != 4 {
t.Errorf("expected 4 PCM frames shipped, got %d", got)
}
// Verify each is the right size.
n.mu.Lock()
defer n.mu.Unlock()
for i, p := range n.sentPCM {
if len(p) != NtgFrameBytes {
t.Errorf("frame %d len=%d, want %d", i, len(p), NtgFrameBytes)
}
}
}
func TestProcessTickRunsPLCBeforeGapRecovery(t *testing.T) {
h, _ := newTestXmppToTg(t)
enc, err := NewEncoder()
if err != nil {
t.Fatalf("NewEncoder: %v", err)
}
// Prime with two packets, drain them to leave the buffer empty but
// established (playoutHead = 502).
h.playout.Push(rtpWithSeq(500, encodeOpusFrame(t, enc)))
h.playout.Push(rtpWithSeq(501, encodeOpusFrame(t, enc)))
st := newTickState()
for i := 0; i < 4; i++ { // drain 2 packets * 2 chunks each
h.processTick(st)
}
// Now skip 502, 503 entirely; deliver 504.
h.playout.Push(rtpWithSeq(504, encodeOpusFrame(t, enc)))
// Next tick should detect the gap, run 2 PLC decodes (502 + 503),
// then decode 504. That's 6 chunks queued (2 PLC * 2 chunks + 1 real * 2).
// processTick ships one per tick, so 6 ticks are required to drain.
h.processTick(st) // ships first chunk
// pending should still hold 5 more chunks
if len(st.pending10ms) != 5 {
t.Errorf("after first post-gap tick: pending=%d, want 5", len(st.pending10ms))
}
for i := 0; i < 5; i++ {
h.processTick(st)
}
if len(st.pending10ms) != 0 {
t.Errorf("pending should be drained, got %d", len(st.pending10ms))
}
}