Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/utils/signaling.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import CancelableRequest from './CancelableRequest.ts'
import Encryption from './e2ee/encryption.js'
import { convertToUnix } from './formattedTime.ts'
import { messagePleaseTryToReload } from './talkDesktopUtils.ts'
import { DEFAULT_MAX_STREAM_BITS } from './webrtc/simplewebrtc/simulcastBitrates.ts'

const actorStore = useActorStore(pinia)

Expand Down Expand Up @@ -71,6 +72,7 @@ function Base(settings) {
this.signalingConnectionTimeout = null
this.signalingConnectionWarning = null
this.signalingConnectionError = null
this.maxStreamBits = DEFAULT_MAX_STREAM_BITS
}

Signaling.Base = Base
Expand Down Expand Up @@ -1297,6 +1299,16 @@ Signaling.Standalone.prototype.joinResponseReceived = function(data, token) {
}

this._rejoinRoomAfterInvalidSession = null

// Apply the room-specific max bitrate provided in server response
// Falls back to the default value when a room has no configured limit
const totalBps = data.room?.bandwidth?.maxstreambitrate
if (typeof totalBps === 'number' && totalBps > 0) {
this.maxStreamBits = totalBps
} else {
this.maxStreamBits = DEFAULT_MAX_STREAM_BITS
}

this.signalingRoomJoined = token
if (this.pendingJoinCall && token === this.pendingJoinCall.token) {
const pendingJoinCallResolve = this.pendingJoinCall.resolve
Expand Down
19 changes: 16 additions & 3 deletions src/utils/webrtc/simplewebrtc/peer.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import adapter from 'webrtc-adapter'
import webrtcSupport from 'webrtcsupport'
import WildEmitter from 'wildemitter'
import { isSafari } from '../../browserCheck.ts'
import { getSimulcastMaxBitrates } from './simulcastBitrates.ts'

/**
* @param {object} stream the stream object.
Expand Down Expand Up @@ -356,6 +357,17 @@ function mungeSdpForSimulcasting(sdp) {
}
/* eslint-enable */

Peer.prototype._getMaxBitrates = function() {
// Read the live per-stream limit from the signaling connection so a room
// switch or reconnect is reflected, instead of the value frozen at
// SimpleWebRTC construction time.
const maxStreamBits = this.parent?.config?.connection?.maxStreamBits
if (maxStreamBits) {
return getSimulcastMaxBitrates(maxStreamBits)
}
return this.maxBitrates
}

Peer.prototype.offer = function(options) {
const sendVideo = this.sendVideoIfAvailable && this.type !== 'screen'
if (sendVideo && this.enableSimulcast && adapter.browserDetails.browser === 'firefox') {
Expand All @@ -368,22 +380,23 @@ Peer.prototype.offer = function(options) {
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,
},
]
Expand Down
7 changes: 2 additions & 5 deletions src/utils/webrtc/simplewebrtc/simplewebrtc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import mockconsole from 'mockconsole'
import webrtcSupport from 'webrtcsupport'
import WildEmitter from 'wildemitter'
import { getSimulcastMaxBitrates } from './simulcastBitrates.ts'
import WebRTC from './webrtc.js'

/**
Expand All @@ -19,11 +20,7 @@ export default function SimpleWebRTC(opts) {
debug: false,
enableDataChannels: true,
enableSimulcast: false,
maxBitrates: {
high: 900000,
medium: 300000,
low: 100000,
},
maxBitrates: getSimulcastMaxBitrates(opts.connection?.maxStreamBits),
autoRequestMedia: false,
receiveMedia: {
offerToReceiveAudio: 1,
Expand Down
44 changes: 44 additions & 0 deletions src/utils/webrtc/simplewebrtc/simulcastBitrates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

/**
* Default maximum bitrate for a single published stream
* when the signaling server does not advertise a limit. (1 Mbps)
*/
export const DEFAULT_MAX_STREAM_BITS = 1_048_576
/**
* Expected maximum bitrate for a hi-quality stream (1920x1080, 30fps)
* Over-provisioning for a minimum "high" resolution (1280x720, 30fps)
*/
export const HIGH_MAX_STREAM_BITS = 2_764_800
/**
* Expected maximum bitrate for a mid-quality stream (640x360, 30fps)
*/
export const MEDIUM_MAX_STREAM_BITS = 300_000
/**
* Expected maximum bitrate for a low-quality stream (320x180, 30fps)
*/
export const LOW_MAX_STREAM_BITS = 100_000

/**
* Compute per-layer simulcast maxBitrate ceilings from the publisher's total
* bandwidth limit. Low and medium layers use fixed, resolution-appropriate
* caps; only the high layer scales with the available budget.
*
* Firefox only (applied via RTCRtpSender.setParameters());
* Chromium/Safari use SDP munging and let REMB distribute the bitrate.
*
* @param totalBps - Total bandwidth in bps.
*/
export function getSimulcastMaxBitrates(totalBps: number = DEFAULT_MAX_STREAM_BITS) {
if (typeof totalBps !== 'number' || totalBps <= 0) {
totalBps = DEFAULT_MAX_STREAM_BITS
}
return {
high: Math.min(Math.round(0.9 * totalBps), HIGH_MAX_STREAM_BITS),
medium: MEDIUM_MAX_STREAM_BITS,
low: LOW_MAX_STREAM_BITS,
}
}
1 change: 1 addition & 0 deletions src/utils/webrtc/simplewebrtc/webrtc.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default function WebRTC(opts) {
},
enableDataChannels: true,
enableSimulcast: false,
// Overwritten by simplewebrtc.js
maxBitrates: {
high: 900000,
medium: 300000,
Expand Down
78 changes: 78 additions & 0 deletions src/utils/webrtc/webrtc.js
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,84 @@
}
window.OCA.Talk.SimpleWebRTC = webrtc

// debug helper to inspect publisher simulcast layers
// (configured ceilings + live per-layer sent bitrate/res).
// Call OCA.Talk.debugPublisherStats() to start, again to stop.
window.OCA.Talk.debugPublisherStats = function() {
if (window.__pubStats) {
clearInterval(window.__pubStats)
window.__pubStats = null
console.log('[pub] stopped')
return
}
const rtc = window.OCA?.Talk?.SimpleWebRTC?.webrtc
if (!rtc) {
console.warn('No SimpleWebRTC instance')
return
}
const publishers = rtc.peers.filter((p) => p.pc && p.pc.getSenders().some((s) => s.track && s.track.kind === 'video'))
if (!publishers.length) {
console.warn('No publishing video peer (are you sending video?)')
return
}
publishers.forEach((p) => p.pc.getSenders().forEach((s) => {
if (s.track && s.track.kind === 'video') {
console.log(
'[pub]',
p.id,
'configured:',
(s.getParameters().encodings || []).map((e) => ({ rid: e.rid, maxBitrate: e.maxBitrate })),
)
}
}))
const prev = new Map()
const history = new Map() // key -> rolling window of recent kbps samples
const WINDOW = 10 // ticks to average over (~20s at the 2s interval)
const avg = (arr) => (arr.length ? Math.round(arr.reduce((a, b) => a + b, 0) / arr.length) : '…')
window.__pubStats = setInterval(async () => {
for (const p of publishers) {
const rows = []
let totalCur = 0
let totalAvg = 0
;(await p.pc.getStats()).forEach((r) => {
if (r.type === 'outbound-rtp' && r.kind === 'video') {
const key = p.id + ':' + (r.rid ?? r.ssrc)
const p0 = prev.get(key)
const kbps = (p0 && r.timestamp > p0.ts)
? Math.round(8 * (r.bytesSent - p0.bytes) / (r.timestamp - p0.ts))
: null
prev.set(key, { bytes: r.bytesSent, ts: r.timestamp })
const h = history.get(key) || []
if (kbps !== null) {
h.push(kbps)
while (h.length > WINDOW) {
h.shift()
}
history.set(key, h)
totalCur += kbps
totalAvg += avg(h)
}
rows.push({
rid: r.rid ?? '-',
res: (r.frameWidth || '?') + 'x' + (r.frameHeight || '?'),
fps: r.framesPerSecond ?? '-',
cur_kbps: kbps ?? '…',
avg_kbps: avg(h),
active: r.active ?? '-',
limit: r.qualityLimitationReason ?? '-',
})
}
})
if (rows.length) {
console.log('[pub]', p.id, new Date().toLocaleTimeString(),

Check failure on line 700 in src/utils/webrtc/webrtc.js

View workflow job for this annotation

GitHub Actions / NPM lint

There should be no line break here

Check failure on line 700 in src/utils/webrtc/webrtc.js

View workflow job for this annotation

GitHub Actions / NPM lint

Should not have line break(s) between ',' and '`— TOTAL cur ${'

Check failure on line 700 in src/utils/webrtc/webrtc.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected newline after '('
`— TOTAL cur ${totalCur} kbps / avg ${totalAvg} kbps (window ${WINDOW})`)

Check failure on line 701 in src/utils/webrtc/webrtc.js

View workflow job for this annotation

GitHub Actions / NPM lint

Expected newline before ')'
console.table(rows)
}
}
}, 2000)
console.log('[pub] polling every 2s, averaging last', WINDOW, 'ticks — call OCA.Talk.debugPublisherStats() again to stop')
}

signaling.on('pullMessagesStoppedOnFail', function() {
// Force leaving the call in WebRTC; when pulling messages stops due
// to failures the room is left, and leaving the room indirectly
Expand Down
Loading