From c3050b9e430a61653ceb008d9859a6d47d65e8d3 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Wed, 15 Apr 2026 13:28:38 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20WebRTC=20audio=20=E2=80=94=20glare=20han?= =?UTF-8?q?dling=20(Perfect=20Negotiation)=20+=20autoplay=20+=20signalingS?= =?UTF-8?q?tate=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/js/classroom-rtc.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/frontend/js/classroom-rtc.js b/frontend/js/classroom-rtc.js index cddbe2d..7023615 100644 --- a/frontend/js/classroom-rtc.js +++ b/frontend/js/classroom-rtc.js @@ -193,6 +193,18 @@ class ClassroomRTC { async _handleOffer(fromUid, payload) { const peer = this._getOrCreate(fromUid); + + // Perfect Negotiation: resolve offer collision (both peers sending offers simultaneously). + // Polite peer (higher uid) yields — rolls back its own offer and handles the incoming one. + // Impolite peer (lower uid) ignores the collision offer and waits for the answer to its own. + const polite = this._uid > fromUid; + const collision = peer.pc.signalingState !== 'stable' || peer.negotiating; + if (collision) { + if (!polite) return; + // Polite: explicit rollback for older browsers; modern Chrome handles it implicitly + await peer.pc.setLocalDescription({ type: 'rollback' }).catch(() => {}); + } + await peer.pc.setRemoteDescription({ type: 'offer', sdp: payload.sdp }); peer.remoteSet = true; await this._flushCandidates(peer); @@ -277,7 +289,7 @@ class ClassroomRTC { /* Renegotiation: fires when tracks are added/removed (e.g. screen share) */ pc.onnegotiationneeded = async () => { - if (peer.negotiating || !peer.remoteSet) return; + if (peer.negotiating || !peer.remoteSet || pc.signalingState !== 'stable') return; peer.negotiating = true; try { const offer = await pc.createOffer(); @@ -299,6 +311,7 @@ class ClassroomRTC { document.body.appendChild(peer.audioEl); } peer.audioEl.srcObject = stream; + peer.audioEl.play().catch(() => {}); this._startVAD(uid, stream); } else if (track.kind === 'video') { if (this._onScreen) this._onScreen(stream);