From b51a595d0be1d0d0923b2e617e417acc6d6cb284 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Wed, 18 Feb 2026 14:23:18 +0100 Subject: [PATCH 1/2] prevent ongoing renegotiations from declaring the negotiation as timed out --- src/room/PCTransport.ts | 1 + src/room/PCTransportManager.ts | 36 +++++++++++++++++++++++++--------- src/room/RTCEngine.ts | 8 ++++++++ 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/room/PCTransport.ts b/src/room/PCTransport.ts index c84ae8e965..e34125cd4e 100644 --- a/src/room/PCTransport.ts +++ b/src/room/PCTransport.ts @@ -278,6 +278,7 @@ export default class PCTransport extends EventEmitter { await this._pc.setRemoteDescription(currentSD); } else { this.renegotiate = true; + this.log.debug('requesting renegotiation', { ...this.logContext }); return; } } else if (!this._pc || this._pc.signalingState === 'closed') { diff --git a/src/room/PCTransportManager.ts b/src/room/PCTransportManager.ts index beece8e4d4..b01a9e6280 100644 --- a/src/room/PCTransportManager.ts +++ b/src/room/PCTransportManager.ts @@ -229,28 +229,46 @@ export class PCTransportManager { async negotiate(abortController: AbortController) { return new TypedPromise(async (resolve, reject) => { - const negotiationTimeout = setTimeout(() => { + let negotiationTimeout = setTimeout(() => { reject(new NegotiationError('negotiation timed out')); }, this.peerConnectionTimeout); - const abortHandler = () => { + const cleanup = () => { clearTimeout(negotiationTimeout); + this.publisher.off(PCEvents.NegotiationStarted, onNegotiationStarted); + abortController.signal.removeEventListener('abort', abortHandler); + }; + + const abortHandler = () => { + cleanup(); reject(new NegotiationError('negotiation aborted')); }; - abortController.signal.addEventListener('abort', abortHandler); - this.publisher.once(PCEvents.NegotiationStarted, () => { + // Reset the timeout each time a renegotiation cycle starts. This + // prevents premature timeouts when the negotiation machinery is + // actively renegotiating (offers going out, answers coming back) but + // NegotiationComplete hasn't fired yet because new requirements keep + // arriving between offer/answer round-trips. + const onNegotiationStarted = () => { if (abortController.signal.aborted) { return; } - this.publisher.once(PCEvents.NegotiationComplete, () => { - clearTimeout(negotiationTimeout); - resolve(); - }); + clearTimeout(negotiationTimeout); + negotiationTimeout = setTimeout(() => { + cleanup(); + reject(new NegotiationError('negotiation timed out')); + }, this.peerConnectionTimeout); + }; + + abortController.signal.addEventListener('abort', abortHandler); + this.publisher.on(PCEvents.NegotiationStarted, onNegotiationStarted); + this.publisher.once(PCEvents.NegotiationComplete, () => { + cleanup(); + resolve(); }); await this.publisher.negotiate((e) => { - clearTimeout(negotiationTimeout); + cleanup(); if (e instanceof Error) { reject(e); } else { diff --git a/src/room/RTCEngine.ts b/src/room/RTCEngine.ts index 0f8c9fee4a..b49c4f0894 100644 --- a/src/room/RTCEngine.ts +++ b/src/room/RTCEngine.ts @@ -1524,6 +1524,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit reject(new NegotiationError('cannot negotiate on closed engine')); } this.on(EngineEvent.Closing, handleClosed); + this.on(EngineEvent.Restarting, handleClosed); this.pcManager.publisher.once( PCEvents.RTPVideoPayloadTypes, @@ -1543,6 +1544,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit await this.pcManager.negotiate(abortController); resolve(); } catch (e: unknown) { + if (abortController.signal.aborted) { + // negotiation was aborted due to engine close or restart, resolve + // cleanly to avoid triggering a cascading reconnect loop + resolve(); + return; + } if (e instanceof NegotiationError) { this.fullReconnectOnNext = true; } @@ -1554,6 +1561,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit } } finally { this.off(EngineEvent.Closing, handleClosed); + this.off(EngineEvent.Restarting, handleClosed); } }); } From 52c93cc912f79d4683e6e838c678dd52640b04f9 Mon Sep 17 00:00:00 2001 From: lukasIO Date: Wed, 18 Feb 2026 14:24:39 +0100 Subject: [PATCH 2/2] Create great-crabs-yawn.md --- .changeset/great-crabs-yawn.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/great-crabs-yawn.md diff --git a/.changeset/great-crabs-yawn.md b/.changeset/great-crabs-yawn.md new file mode 100644 index 0000000000..7954d844df --- /dev/null +++ b/.changeset/great-crabs-yawn.md @@ -0,0 +1,5 @@ +--- +"livekit-client": patch +--- + +Prevent ongoing renegotiations from declaring the negotiation as timed out