From 2d35b1149ba2206847339d3bf0c337fbba027e6a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 9 Sep 2026 07:24:25 +0000 Subject: [PATCH] fix: await ICE gathering before publisher ICE restart A FAST reconnect calls restart_ice() on the publisher PeerConnection, but the ICE agent rejects a restart while it is still gathering candidates ("ICE Agent can not be restarted when gathering"). When a FAST reconnect immediately follows a freshly recreated publisher PC, it can race the gatherer and fail every attempt, so a reconnect that should recover to Joined never settles. Wait for the publisher's ICE gathering state to leave Gathering (bounded by a 3s timeout) before restarting, so the restart succeeds once candidates have settled. Browsers restart ICE implicitly during gathering; this brings the native path in line. Co-authored-by: Neevash Ramdial (Nash) --- src/rtc/publisher.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/rtc/publisher.rs b/src/rtc/publisher.rs index c7ad679..ffa1613 100644 --- a/src/rtc/publisher.rs +++ b/src/rtc/publisher.rs @@ -81,10 +81,38 @@ pub(crate) async fn restart_ice( if tracks.is_empty() { return Ok(()); } + // The ICE agent rejects a restart while it is still gathering candidates + // (browsers restart implicitly instead). A FAST reconnect that follows a + // freshly recreated publisher PeerConnection can race the gatherer, so wait + // for gathering to leave the `Gathering` state before restarting. + wait_for_ice_gathering_to_settle(publisher, ICE_GATHERING_SETTLE_TIMEOUT).await; publisher.restart_ice().await.map_err(neg)?; negotiate_publish(publisher, signal, session_id, tracks, publish_options).await } +/// Upper bound on how long [`restart_ice`] waits for the publisher's ICE agent +/// to finish gathering before it attempts the restart anyway. +const ICE_GATHERING_SETTLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3); + +/// Poll the publisher's ICE gathering state until it is no longer `Gathering` +/// (i.e. `New` or `Complete`) or `timeout` elapses. Returning while still +/// gathering leaves the subsequent restart to fail and be retried by the +/// reconnect driver rather than blocking indefinitely. +async fn wait_for_ice_gathering_to_settle( + publisher: &Arc, + timeout: std::time::Duration, +) { + use webrtc::ice_transport::ice_gathering_state::RTCIceGatheringState; + + let deadline = tokio::time::Instant::now() + timeout; + while publisher.ice_gathering_state() == RTCIceGatheringState::Gathering { + if tokio::time::Instant::now() >= deadline { + return; + } + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } +} + fn neg(e: impl std::fmt::Display) -> RtcError { RtcError::Negotiation(NegotiationError(e.to_string())) }