mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 12:17:06 +00:00
175 lines
5.1 KiB
Go
175 lines
5.1 KiB
Go
package xmppsig
|
|
|
|
import (
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pion/webrtc/v4"
|
|
"github.com/pion/webrtc/v4/pkg/media"
|
|
)
|
|
|
|
// pion OnTrack fires inside SetRemoteDescription, but setupAudio runs
|
|
// later (from OnRemoteDescriptionApplied) - so hookTrack must install
|
|
// a permanent forwarder at PC construction and buffer until
|
|
// SetTrackHandler. This drives a real pion<->pion roundtrip with the
|
|
// handler installed after the track has already landed.
|
|
func TestHookTrack_BuffersTrackThatArrivesBeforeSetHandler(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("e2e ICE handshake")
|
|
}
|
|
|
|
// gateway-side PC, same shape as xmppsig.NewCaller
|
|
pcGateway, err := webrtc.NewPeerConnection(webrtc.Configuration{})
|
|
if err != nil {
|
|
t.Fatalf("gateway PC: %v", err)
|
|
}
|
|
defer pcGateway.Close()
|
|
if _, err := pcGateway.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RTPTransceiverInit{
|
|
Direction: webrtc.RTPTransceiverDirectionSendrecv,
|
|
}); err != nil {
|
|
t.Fatalf("gateway AddTransceiver: %v", err)
|
|
}
|
|
|
|
// the forwarder under test
|
|
var base xmppBase
|
|
base.hookTrack(pcGateway)
|
|
|
|
// remote pion PC with an audio track to send
|
|
pcPeer, err := webrtc.NewPeerConnection(webrtc.Configuration{})
|
|
if err != nil {
|
|
t.Fatalf("peer PC: %v", err)
|
|
}
|
|
defer pcPeer.Close()
|
|
peerTrack, err := webrtc.NewTrackLocalStaticSample(
|
|
webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 2},
|
|
"audio", "peer-stream",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("peer track: %v", err)
|
|
}
|
|
if _, err := pcPeer.AddTrack(peerTrack); err != nil {
|
|
t.Fatalf("peer AddTrack: %v", err)
|
|
}
|
|
|
|
// pion offer/answer + ICE-complete handshake
|
|
gatewayConnected := make(chan struct{})
|
|
pcGateway.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
|
|
if s == webrtc.PeerConnectionStateConnected {
|
|
select {
|
|
case <-gatewayConnected:
|
|
default:
|
|
close(gatewayConnected)
|
|
}
|
|
}
|
|
})
|
|
completePionHandshake(t, pcGateway, pcPeer)
|
|
select {
|
|
case <-gatewayConnected:
|
|
case <-time.After(15 * time.Second):
|
|
t.Fatalf("gateway PC never reached Connected (state=%s)", pcGateway.ConnectionState())
|
|
}
|
|
|
|
// real RTP so OnTrack fires (pion needs the first packet to dispatch)
|
|
stopRTP := make(chan struct{})
|
|
go pumpSilenceOpus(peerTrack, stopRTP)
|
|
defer close(stopRTP)
|
|
|
|
// wait for pion to dispatch the first packet through hookTrack
|
|
waitUntilTrue(t, "track buffered by hookTrack", 5*time.Second, func() bool {
|
|
base.mu.Lock()
|
|
defer base.mu.Unlock()
|
|
return len(base.pendingTracks) > 0
|
|
})
|
|
|
|
// install handler late, like setupAudio from OnRemoteDescriptionApplied
|
|
var delivered atomic.Int64
|
|
base.SetTrackHandler(func(t *webrtc.TrackRemote) {
|
|
delivered.Add(1)
|
|
})
|
|
if delivered.Load() != 1 {
|
|
t.Errorf("SetTrackHandler did not drain pendingTracks: delivered=%d, want 1", delivered.Load())
|
|
}
|
|
|
|
// subsequent track events should go direct to the handler
|
|
base.mu.Lock()
|
|
if len(base.pendingTracks) != 0 {
|
|
t.Errorf("pendingTracks not cleared after drain: %d entries", len(base.pendingTracks))
|
|
}
|
|
base.mu.Unlock()
|
|
}
|
|
|
|
// completePionHandshake drives a pion offer/answer + trickle ICE between
|
|
// two PeerConnections, with the gateway as the answerer (matching the
|
|
// xmpp-originated incoming call shape - Dino's session-initiate carries
|
|
// the offer, the gateway sends the answer).
|
|
func completePionHandshake(t *testing.T, pcAnswerer, pcOfferer *webrtc.PeerConnection) {
|
|
t.Helper()
|
|
// trickle ICE: forward each side's candidates to the other
|
|
pcOfferer.OnICECandidate(func(c *webrtc.ICECandidate) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
if err := pcAnswerer.AddICECandidate(c.ToJSON()); err != nil {
|
|
t.Logf("answerer AddICECandidate: %v", err)
|
|
}
|
|
})
|
|
pcAnswerer.OnICECandidate(func(c *webrtc.ICECandidate) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
if err := pcOfferer.AddICECandidate(c.ToJSON()); err != nil {
|
|
t.Logf("offerer AddICECandidate: %v", err)
|
|
}
|
|
})
|
|
|
|
offer, err := pcOfferer.CreateOffer(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pcOfferer.SetLocalDescription(offer); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pcAnswerer.SetRemoteDescription(*pcOfferer.LocalDescription()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
answer, err := pcAnswerer.CreateAnswer(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pcAnswerer.SetLocalDescription(answer); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := pcOfferer.SetRemoteDescription(*pcAnswerer.LocalDescription()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// writes 20ms opus silence every 20ms until stop closes
|
|
func pumpSilenceOpus(track *webrtc.TrackLocalStaticSample, stop <-chan struct{}) {
|
|
// minimal opus silence frame: TOC byte + 1-byte SILK
|
|
silence := []byte{0xF8, 0xFF, 0xFE}
|
|
tick := time.NewTicker(20 * time.Millisecond)
|
|
defer tick.Stop()
|
|
for {
|
|
select {
|
|
case <-stop:
|
|
return
|
|
case <-tick.C:
|
|
_ = track.WriteSample(media.Sample{Data: silence, Duration: 20 * time.Millisecond})
|
|
}
|
|
}
|
|
}
|
|
|
|
// local poller; duplicates the jingle one to avoid test-file ordering deps
|
|
func waitUntilTrue(t *testing.T, what string, d time.Duration, cond func() bool) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(d)
|
|
for time.Now().Before(deadline) {
|
|
if cond() {
|
|
return
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
t.Fatalf("waitUntilTrue(%s): timed out after %v", what, d)
|
|
}
|