From 1d541ddf223dbdb1b2b2708c6ab07ce7ffffc12e Mon Sep 17 00:00:00 2001 From: Tarek Loubani Date: Tue, 18 Aug 2026 18:03:04 -0400 Subject: [PATCH 1/3] refactor(call): extract requestOfferFromMcuAndRetryUntilReceived No functional changes; the logic to request an offer from the MCU and keep requesting it every 10 seconds until it is received is now available as a helper to be reused in upcoming fixes. Assisted-by: OpenCode:moonshotai/kimi-k3 --- src/utils/webrtc/webrtc.js | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/utils/webrtc/webrtc.js b/src/utils/webrtc/webrtc.js index 91869c50964..377466673b3 100644 --- a/src/utils/webrtc/webrtc.js +++ b/src/utils/webrtc/webrtc.js @@ -725,6 +725,27 @@ export function initWebRtc(signaling, _callParticipantCollection, _localCallPart }, 5000) } + /** + * Requests a new offer for the given (subscriber) peer from the MCU and + * keeps requesting it until a new offer is received (the retry interval + * is cleared when the offer message arrives). + * + * @param {object} peer The peer connection to request a new offer for + */ + function requestOfferFromMcuAndRetryUntilReceived(peer) { + console.debug('Request offer again', peer.id, peer) + + signaling.requestOffer(peer.id, 'video') + + clearInterval(delayedConnectionToPeer[peer.id]) + + delayedConnectionToPeer[peer.id] = setInterval(function() { + console.debug('No offer received, request offer again', peer.id, peer) + + signaling.requestOffer(peer.id, 'video') + }, 10000) + } + /** * @param {object} peer The peer connection to handle the state on */ @@ -758,17 +779,7 @@ export function initWebRtc(signaling, _callParticipantCollection, _localCallPart } else { // This handles ICE failures of a receiver peer; ICE failures of // the sender peer are handled in the "iceFailed" event. - console.debug('Request offer again', peer.id, peer) - - signaling.requestOffer(peer.id, 'video') - - clearInterval(delayedConnectionToPeer[peer.id]) - - delayedConnectionToPeer[peer.id] = setInterval(function() { - console.debug('No offer received, request offer again', peer.id, peer) - - signaling.requestOffer(peer.id, 'video') - }, 10000) + requestOfferFromMcuAndRetryUntilReceived(peer) } } From dc74aa626162dc83fd47036e61c672c6ce9f64b1 Mon Sep 17 00:00:00 2001 From: Tarek Loubani Date: Tue, 18 Aug 2026 18:03:53 -0400 Subject: [PATCH 2/3] fix(call): recover subscriber connections stuck in new or checking state With the high performance backend a subscriber connection is only recovered when the ICE connection state changes to "failed". However, ICE has no timeout at all while in "new" state, and "checking" can also get stuck in some browsers, so in those cases the connection is never recovered and the participant is shown with a loading spinner and no audio until the call is rejoined. If a subscriber connection is still in "new" or "checking" state 30 seconds after the peer was created a new offer is requested from the MCU (reusing the existing retry mechanism from the "failed" case). The timeout is armed again when the replacement peer is created, so recovery keeps retrying until it succeeds. Fixes the stuck in "new" state case of #5931. Assisted-by: OpenCode:moonshotai/kimi-k3 --- src/utils/webrtc/webrtc.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/utils/webrtc/webrtc.js b/src/utils/webrtc/webrtc.js index 377466673b3..fe135b11f60 100644 --- a/src/utils/webrtc/webrtc.js +++ b/src/utils/webrtc/webrtc.js @@ -1185,6 +1185,25 @@ export function initWebRtc(signaling, _callParticipantCollection, _localCallPart setHandlerForIceConnectionStateChange(peer) setHandlerForConnectionStateChange(peer) setHandlerForSignalingStateChange(peer) + + if (signaling.hasFeature('mcu')) { + // ICE has no timeout while the connection is in "new" + // state, and "checking" may get stuck too in some + // browsers, so if a subscriber connection is not + // established in a reasonable time a new connection is + // explicitly requested, like in the "failed" case. If + // the new connection also gets stuck the timeout will + // be armed again when its peer is created. + setTimeout(function() { + if (peer.pc.iceConnectionState !== 'new' && peer.pc.iceConnectionState !== 'checking') { + return + } + + console.debug('Subscriber connection not established after 30 seconds, requesting offer again', peer.id, peer) + + requestOfferFromMcuAndRetryUntilReceived(peer) + }, 30000) + } } setHandlerForNegotiationNeeded(peer) From f05a7d40bc6cee514d24bc0df7f635067a32d5d5 Mon Sep 17 00:00:00 2001 From: Tarek Loubani Date: Tue, 18 Aug 2026 18:12:45 -0400 Subject: [PATCH 3/3] fix(call): recover subscriber connections stuck in disconnected state With the high performance backend a subscriber connection is only recovered when the ICE connection state changes to "failed". In most cases connections that enter the "disconnected" state will eventually transition to "failed" (which can take up to around 30 seconds), but in some cases the state may get stuck as "disconnected" without the connection ever being reported as "failed", so the connection is never recovered and the participant is shown with a loading spinner and no audio until the call is rejoined. If a subscriber connection is still in "disconnected" state 10 seconds after entering the state a new offer is requested from the MCU (reusing the existing retry mechanism from the "failed" case). Even if the connection would have eventually transitioned to "failed" and been recovered that way, no media is flowing while disconnected, so recovering earlier just replaces, with the same final result, a connection that at best would have been recovered later. See #5931. Assisted-by: OpenCode:moonshotai/kimi-k3 --- src/utils/webrtc/webrtc.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/utils/webrtc/webrtc.js b/src/utils/webrtc/webrtc.js index fe135b11f60..2b94c24617f 100644 --- a/src/utils/webrtc/webrtc.js +++ b/src/utils/webrtc/webrtc.js @@ -708,8 +708,9 @@ export function initWebRtc(signaling, _callParticipantCollection, _localCallPart peer.emit('extendedIceConnectionStateChange', 'disconnected-long') if (!signaling.hasFeature('mcu')) { - // Disconnections are not handled with the MCU, only - // failures. + // ICE restarts are not performed with the MCU; if the + // connection stays disconnected a new connection is + // requested instead (see below). // If the peer is still disconnected after 5 seconds we try // ICE restart. @@ -723,6 +724,21 @@ export function initWebRtc(signaling, _callParticipantCollection, _localCallPart } } }, 5000) + + if (signaling.hasFeature('mcu')) { + setTimeout(function() { + if (peer.pc.iceConnectionState !== 'disconnected') { + return + } + + // A subscriber connection that stays disconnected is not + // expected to recover on its own, and some browsers may + // never report it as "failed" either, so after a grace + // period a new connection is explicitly requested, like + // in the "failed" case. + requestOfferFromMcuAndRetryUntilReceived(peer) + }, 10000) + } } /**