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) }