From 1b871c2d87ec470ef766da1876820b6d64a41cd0 Mon Sep 17 00:00:00 2001 From: SendableMetatype <263203301+SendableMetatype@users.noreply.github.com> Date: Wed, 12 Aug 2026 01:03:19 +0200 Subject: [PATCH] test: buffer trickled candidates until both descriptions are applied Candidate gathering starts inside createOffer and createAnswer via setLocalDescription, so the test forwarded candidates to a peer that often had no remote description yet. The native stack rejects such candidates and nothing retries them, so when both directions lost all candidates during the exchange the connection never formed and the test burned its whole timeout. This was the common cause of the intermittent iceCandidatesRespectPortAllocatorConfig failures on CI. The peers now buffer outgoing candidates and deliver them after both descriptions are set, forwarding directly from then on. The candidate list also became a CopyOnWriteArrayList since it is written on the signaling thread and read on the test thread. --- .../PortAllocatorConfigIntegrationTest.java | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/PortAllocatorConfigIntegrationTest.java b/webrtc/src/test/java/dev/onvoid/webrtc/PortAllocatorConfigIntegrationTest.java index d97cc68c..a12c3240 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/PortAllocatorConfigIntegrationTest.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/PortAllocatorConfigIntegrationTest.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -60,6 +61,11 @@ void iceCandidatesRespectPortAllocatorConfig() throws Exception { callee.setRemoteDescription(caller.createOffer()); caller.setRemoteDescription(callee.createAnswer()); + // Both descriptions are applied now; deliver the candidates gathered + // during the exchange and forward directly from here on. + caller.flushCandidates(); + callee.flushCandidates(); + // Wait until connected (with a timeout to avoid hanging tests). assertTrue(caller.awaitConnected(30, TimeUnit.SECONDS), "Caller failed to connect in time"); assertTrue(callee.awaitConnected(30, TimeUnit.SECONDS), "Callee failed to connect in time"); @@ -118,7 +124,9 @@ private static class AllocPeer implements PeerConnectionObserver { private final RTCPeerConnection pc; private RTCPeerConnection remote; private final CountDownLatch connected = new CountDownLatch(1); - final List candidates = new ArrayList<>(); + private final List pendingCandidates = new ArrayList<>(); + private boolean forwardDirectly; + final List candidates = new CopyOnWriteArrayList<>(); AllocPeer(PeerConnectionFactory factory, RTCConfiguration cfg) { @@ -165,12 +173,37 @@ void close() { pc.close(); } + /** + * Delivers the candidates buffered during the offer/answer exchange + * and switches to direct forwarding. Trickling starts inside + * createOffer/createAnswer via setLocalDescription, so early + * candidates would reach the remote peer before it has a remote + * description; the native stack rejects such candidates and they + * would be lost for good, leaving ICE without a candidate pair. + */ + void flushCandidates() { + List buffered; + synchronized (this) { + forwardDirectly = true; + buffered = new ArrayList<>(pendingCandidates); + pendingCandidates.clear(); + } + for (RTCIceCandidate candidate : buffered) { + remote.addIceCandidate(candidate); + } + } + @Override public void onIceCandidate(RTCIceCandidate candidate) { candidates.add(candidate.sdp); - if (remote != null) { - remote.addIceCandidate(candidate); + + synchronized (this) { + if (!forwardDirectly) { + pendingCandidates.add(candidate); + return; + } } + remote.addIceCandidate(candidate); } @Override