feat: Add in-place ICE restart of the JVB session - #3083
Merged
Conversation
SDPUtil.replaceIceCredentialsAndStripCandidates() rewrites every a=ice-ufrag/a=ice-pwd line and removes every a=candidate and a=end-of-candidates line, preserving the rest of the SDP (and its line separators) verbatim. This is the transformation an in-place ICE restart needs to build the patched remote offer with: applying the new candidates in the same setRemoteDescription() as the new credentials makes libwebrtc treat them as brand new candidates for the same remote address and tear down the selected pair, so they have to be trickled in afterwards instead.
JitsiConference.restartJvbIce() / JingleSessionPC.restartIce() ask Jicofo to restart ICE for this endpoint by sending a Jingle 'session-info' with a <bridge-session ice-restart="true"/>. The bridge creates a NEW ICE agent with fresh credentials while the old one keeps carrying media (make-before-break) and its transport is relayed back to us as a 'transport-info' whose <transport> is tagged with an 'ice-generation'. strophe.jingle routes such a transport-info to JingleSessionPC.onBridgeIceRestartTransport(), which applies it as a single modification queue task: drop anything whose generation is not newer than the last one applied, build a patched offer from the current remote description with the new credentials and no candidates, apply it, answer it with a real createAnswer() (the client stays the answerer, so there is no signalling round trip on the critical path), and only then trickle in the bridge's new candidates. Adding those candidates as part of the offer instead makes libwebrtc destroy and rebuild the selected candidate pair synchronously, which is the media freeze the whole design exists to avoid. The local ICE credentials the browser rotates to while answering are signalled back with a 'transport-info' carrying the same generation; the bridge is the controlling agent and needs them for its connectivity checks. Gated by the enableIceRestart config option. Wired into the reactive ICE failure path as well: an ICE failure attempts an in-place restart first and falls back to the legacy session restart if the request is rejected or ICE does not recover in time. Includes a short-lived media stats sampler for diagnosing restarts, enabled with the testing.debugIceRestart config option. Every stage logs under the '[ice-restart]' prefix with the ICE generation.
Covers the shape of the 'session-info' restart request, the offer/answer ordering (candidates added only after setLocalDescription), the generation tagging of the local credentials sent back, and the monotonic generation guard against duplicate, reordered and malformed pushes.
The reason an in-place ICE restart has to strip the candidates from the patched offer and trickle them in after the offer/answer cycle is a libwebrtc behaviour we reported upstream: https://issues.webrtc.org/issues/543082385
This was referenced Aug 6, 2026
jitsi-ci Bot
pushed a commit
to jitsi/jitsi-pr-tests-pages
that referenced
this pull request
Aug 6, 2026
A restart that is going to work completes well inside a second, so 15s of a broken connection before falling back to a session restart was far more than the happy path needs. It also matters for the unhappy path: the bridge answers a restart request it will not honour with a plain response carrying no transport, and that is not propagated back to the client, so the restart never lands and this timeout is the only thing that notices. That case is not exotic - a bridge with videobridge.ice.restart.enabled=false, or one whose transport is not established yet, hits it - and it happens throughout a rollout where clients have the feature enabled and only some bridges do. At 15s this made recovery from an ICE failure slower than it was before the feature existed. 5s keeps ample headroom for a slow network while staying under the bridge's own restart timeout (10s by default), so the client stops waiting before the bridge gives up rather than after. Also log the recovery check being armed and its outcome, so the fallback decision is visible either way.
This reverts commit 630f3e5. Shortening the timeout only narrowed the window in which a restart the bridge never applied goes unnoticed. Making jicofo react to that directly addresses it instead, so the client-side timeout stays as it was.
|
|
||
| const answer = await this.peerconnection.createAnswer(this.mediaConstraints as RTCOfferOptions); | ||
|
|
||
| await this.peerconnection.setLocalDescription(answer); |
Member
There was a problem hiding this comment.
can you call this._renegotiate({ sdp: patchedOffer, type: 'offer' }) instead of calling sRD/cA/sLD on the native pc? This will also take care of signaling the SSRCs if the browser produces a different set of SSRCs after this operation.
The offer/answer cycle was driven by calling setRemoteDescription on the native peer connection and createAnswer/setLocalDescription on TraceablePeerConnection. That split left TPC's own view of the session stale, suppressed the REMOTE_UFRAG_CHANGED event, and - the reason this matters - skipped notifyMySSRCUpdate, so any SSRCs the browser regenerated while answering the restart offer were never signalled. Go through _renegotiate() instead, which is the path the rest of the class uses. The original reason for bypassing it, that the patched SDP is already the transformed remote description and must not go through the pipeline twice, does not hold: the no-argument _renegotiate() feeds peerconnection.remoteDescription straight back into TraceablePeerConnection.setRemoteDescription and is used that way throughout. The tests mocked the native setRemoteDescription, so they move to the TPC one and the mock gains the remoteDescription getter _renegotiate() reads.
jitsi-ci Bot
pushed a commit
to jitsi/jitsi-pr-tests-pages
that referenced
this pull request
Aug 10, 2026
jallamsetty1
approved these changes
Aug 11, 2026
jitsi-ci Bot
pushed a commit
to jitsi/jitsi-pr-tests-pages
that referenced
this pull request
Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the client side of an in-place ICE restart of the JVB session. Instead of tearing the session down and being re-invited, the client applies a new set of bridge ICE credentials in place, keeping the same DTLS session, SSRCs and RTP state so media keeps flowing throughout.
Flow
JitsiConference.restartJvbIce(reason)asks jicofo for a restart with a Jinglesession-infocarrying<bridge-session ice-restart="true"/>, and resolves when the IQ is acked.transport-infotagged with anice-generation.strophe.jingle.jsroutes any such transport-info toJingleSessionPC.onBridgeIceRestartTransport().setRemoteDescription/createAnswer/setLocalDescription, then the bridge's candidates added back viaaddIceCandidate(), then our own new credentials signalled back tagged with the same generation.The client stays the answerer throughout and uses a real
createAnswer(). Since jicofo already drives renegotiation with offer-shaped Jingle IQs in production, no new offerer machinery is needed, and the whole restart becomes a local operation with no signaling round trip on the critical path. Measured against a real bridge, the queue is held for 85ms.Why the candidates are stripped and re-added
Applying the new candidates in the same
setRemoteDescription()as the new credentials makes libwebrtc stamp them with the new ICE generation (candidate lines carry no credentials of their own), treat them as brand new candidates for the same remote address, and synchronously tear down the selected candidate pair, which defeats the make-before-break this feature exists for. Trickling them in after the offer/answer cycle avoids it.Reported upstream as https://issues.webrtc.org/issues/543082385
Gating and fallback
Gated on the
enableIceRestartconfig flag. The existing reactive path is unchanged:_restartJvbIceWithFallback()still falls back to the legacy full session restart viaIceFailedHandlingwhen an in-place restart is rejected or does not recover the connection.APP.conference._room.restartJvbIce()triggers one manually.A 3s/100ms media-stats sampler is available behind
config.testing.debugIceRestart, off by default.Tests
15 new tests.
SDPUtil.replaceIceCredentialsAndStripCandidatesis covered for credential replacement, candidate stripping, byte-for-byte preservation of every other line, and both line-separator conventions.onBridgeIceRestartTransportis covered for the request element shape, the generation guard (duplicate, older, invalid, and newer-after-a-failure), and foraddIceCandidatehappening strictly aftersetLocalDescription, asserted via jasmine invocation order.Part of a multi-repo change; all PRs share the
in-place-ice-restartbranch name so CI tests them together: