From 04bc729077a4d7b4934dfd8618f7154385aca1f0 Mon Sep 17 00:00:00 2001 From: Tarek Loubani Date: Wed, 22 Apr 2026 20:24:21 -0400 Subject: [PATCH] fix(webrtc): Fix Firefox simulcast race condition and respect HPB bandwidth limits - Chain sender.setParameters() before createOffer() in Firefox to avoid a race where the offer SDP is generated before simulcast encodings are applied. Previously the promise was ignored, causing simulcast to fail silently on low-bandwidth links and fall back to a single high-bitrate stream that congested the connection. - Add _getMaxBitrates() helper that derives simulcast tiers from the signaling server's maxstreambitrate instead of hardcoded values. The server already sent this limit in the room join response but the client never used it, so publishers always tried to push 1.3 Mbps even when the uplink or server policy was much lower. - Parse data.room.bandwidth.maxstreambitrate in joinResponseReceived and expose it on the signaling connection so Peer objects can read it. - Guard s.track before accessing s.track.kind in getSenders() find, matching the existing upstream fix for disabled tracks in Firefox. Fixes #17774 Signed-off-by: Tarek Loubani --- src/utils/signaling.js | 6 ++ src/utils/webrtc/simplewebrtc/peer.js | 93 +++++++++++++++++---------- 2 files changed, 65 insertions(+), 34 deletions(-) diff --git a/src/utils/signaling.js b/src/utils/signaling.js index 31526c985d8..b73224d1818 100644 --- a/src/utils/signaling.js +++ b/src/utils/signaling.js @@ -1282,6 +1282,12 @@ Signaling.Standalone.prototype.joinCall = function(token, flags, silent, recordi Signaling.Standalone.prototype.joinResponseReceived = function(data, token) { console.debug('Joined', data, token) this.signalingRoomJoined = token + if (data.room && data.room.bandwidth && data.room.bandwidth.maxstreambitrate) { + const totalBps = data.room.bandwidth.maxstreambitrate + if (typeof totalBps === 'number' && totalBps > 0) { + this.maxStreamBits = totalBps + } + } if (this.pendingJoinCall && token === this.pendingJoinCall.token) { const pendingJoinCallResolve = this.pendingJoinCall.resolve const pendingJoinCallReject = this.pendingJoinCall.reject diff --git a/src/utils/webrtc/simplewebrtc/peer.js b/src/utils/webrtc/simplewebrtc/peer.js index 2f592176c44..ade6c045385 100644 --- a/src/utils/webrtc/simplewebrtc/peer.js +++ b/src/utils/webrtc/simplewebrtc/peer.js @@ -356,69 +356,94 @@ function mungeSdpForSimulcasting(sdp) { } /* eslint-enable */ +Peer.prototype._getMaxBitrates = function() { + const connection = this.parent ? this.parent.config.connection : null + if (connection && connection.maxStreamBits) { + const partValue = Math.floor(connection.maxStreamBits / 21 / 100) * 100 + return { + high: partValue * 16, + medium: partValue * 4, + low: partValue, + } + } + return this.maxBitrates +} + +Peer.prototype._createOffer = function(options, sendVideo) { + const self = this + this.pc.createOffer(options).then(function(offer) { + if (sendVideo && self.enableSimulcast) { + // This SDP munging only works with Chrome (Safari STP may support it too) + if (adapter.browserDetails.browser === 'chrome' || adapter.browserDetails.browser === 'safari') { + console.debug('Enabling Simulcasting for Chrome (SDP munging)') + offer.sdp = mungeSdpForSimulcasting(offer.sdp) + } else if (adapter.browserDetails.browser !== 'firefox') { + console.debug('Simulcast can only be enabled on Chrome or Firefox') + } + } + + self.pc.setLocalDescription(offer).then(function() { + if (self.parent.config.nick) { + // The offer is a RTCSessionDescription that only serializes + // its own attributes to JSON, so if extra attributes are needed + // a regular object has to be sent instead. + offer = { + type: offer.type, + sdp: offer.sdp, + nick: self.parent.config.nick, + } + } + self.send('offer', offer) + }).catch(function(error) { + console.warn('setLocalDescription for offer failed: ', error) + }) + }).catch(function(error) { + console.warn('createOffer failed: ', error) + }) +} + Peer.prototype.offer = function(options) { const sendVideo = this.sendVideoIfAvailable && this.type !== 'screen' if (sendVideo && this.enableSimulcast && adapter.browserDetails.browser === 'firefox') { console.debug('Enabling Simulcasting for Firefox (RID)') const sender = this.pc.getSenders().find(function(s) { - return s.track.kind === 'video' + return s.track && s.track.kind === 'video' }) if (sender) { let parameters = sender.getParameters() if (!parameters) { parameters = {} } + const maxBitrates = this._getMaxBitrates() parameters.encodings = [ { rid: 'h', active: true, - maxBitrate: this.maxBitrates.high, + maxBitrate: maxBitrates.high, }, { rid: 'm', active: true, - maxBitrate: this.maxBitrates.medium, + maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2, }, { rid: 'l', active: true, - maxBitrate: this.maxBitrates.low, + maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4, }, ] - sender.setParameters(parameters) + sender.setParameters(parameters).then(function() { + this._createOffer(options, sendVideo) + }.bind(this)).catch(function(error) { + console.warn('Failed to set simulcast parameters in Firefox, creating offer anyway', error) + this._createOffer(options, sendVideo) + }.bind(this)) + return } } - this.pc.createOffer(options).then(function(offer) { - if (sendVideo && this.enableSimulcast) { - // This SDP munging only works with Chrome (Safari STP may support it too) - if (adapter.browserDetails.browser === 'chrome' || adapter.browserDetails.browser === 'safari') { - console.debug('Enabling Simulcasting for Chrome (SDP munging)') - offer.sdp = mungeSdpForSimulcasting(offer.sdp) - } else if (adapter.browserDetails.browser !== 'firefox') { - console.debug('Simulcast can only be enabled on Chrome or Firefox') - } - } - - this.pc.setLocalDescription(offer).then(function() { - if (this.parent.config.nick) { - // The offer is a RTCSessionDescription that only serializes - // its own attributes to JSON, so if extra attributes are needed - // a regular object has to be sent instead. - offer = { - type: offer.type, - sdp: offer.sdp, - nick: this.parent.config.nick, - } - } - this.send('offer', offer) - }.bind(this)).catch(function(error) { - console.warn('setLocalDescription for offer failed: ', error) - }) - }.bind(this)).catch(function(error) { - console.warn('createOffer failed: ', error) - }) + this._createOffer(options, sendVideo) } Peer.prototype.handleOffer = function(offer) {