fix(lotus): screenshare bitrate cap is a budget across simulcast layers
The same maxBitrate was written into every encoding, so a 1.5 Mbps cap could mean 4.5 Mbps aggregate. Distribute proportionally to the layers' existing ratios (floor, so never over). Tested with a 3-layer case. Fixes #12 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
co-authored by
Claude Opus 5
parent
1c1394b6ef
commit
dcba5b6b7e
@@ -198,6 +198,43 @@ export function buildPatch(
|
||||
return patch;
|
||||
}
|
||||
|
||||
// [lotus] Treat `cap` as a budget for the WHOLE simulcast publication rather
|
||||
// than a per-encoding value, and distribute it across encodings in
|
||||
// proportion to their existing (pre-patch) ratios — instead of writing `cap`
|
||||
// into every encoding (#12), which let the aggregate reach up to N× the
|
||||
// requested cap (and also raised the low layers far above their presets).
|
||||
// With a single encoding this reduces to the previous behaviour: that
|
||||
// encoding simply gets `cap`.
|
||||
// Exported for unit testing only (see lotusQuality.test.ts) — not part of
|
||||
// the module's public surface used by callers.
|
||||
export function applyProportionalMaxBitrate(
|
||||
encodings: RTCRtpEncodingParameters[],
|
||||
cap: number,
|
||||
): void {
|
||||
if (encodings.length <= 1) {
|
||||
for (const enc of encodings) enc.maxBitrate = cap;
|
||||
return;
|
||||
}
|
||||
const total = encodings.reduce(
|
||||
(sum, enc) =>
|
||||
sum + (typeof enc.maxBitrate === "number" ? enc.maxBitrate : 0),
|
||||
0,
|
||||
);
|
||||
if (total <= 0) {
|
||||
// No existing ratios to scale from: fall back to capping only the
|
||||
// highest layer (LiveKit orders simulcast encodings low-to-high, so
|
||||
// that's the last one) and leave the others alone rather than guess.
|
||||
encodings[encodings.length - 1].maxBitrate = cap;
|
||||
return;
|
||||
}
|
||||
// Math.floor (not round) so rounding error can only push the aggregate
|
||||
// under the cap, never over it.
|
||||
for (const enc of encodings) {
|
||||
const prev = typeof enc.maxBitrate === "number" ? enc.maxBitrate : 0;
|
||||
enc.maxBitrate = Math.floor((prev / total) * cap);
|
||||
}
|
||||
}
|
||||
|
||||
// Exported for unit testing only (see lotusQuality.test.ts) — not part of
|
||||
// the module's public surface used by callers.
|
||||
export async function patchSender(
|
||||
@@ -210,10 +247,26 @@ export async function patchSender(
|
||||
const params = sender.getParameters();
|
||||
if (!params.encodings || params.encodings.length === 0)
|
||||
params.encodings = [{}];
|
||||
// Apply to EVERY encoding, not just encodings[0]: screenshare publishes
|
||||
// with simulcast (VP8), so encodings[0] is the small layer and the
|
||||
// full-resolution layer — the real bandwidth hog — is a later encoding.
|
||||
for (const enc of params.encodings) Object.assign(enc, patch);
|
||||
// Screenshare publishes with simulcast (VP8): encodings[0] is the small
|
||||
// layer and the full-resolution layer — the real bandwidth hog — is a
|
||||
// later encoding. maxFramerate (and an `undefined` clear) still apply to
|
||||
// every encoding, but maxBitrate is special-cased (#12): writing the same
|
||||
// value into every encoding let the aggregate reach N× the requested cap
|
||||
// and also raised the low layers far above their presets, so instead we
|
||||
// treat the requested value as a budget for the whole publication and
|
||||
// distribute it across encodings proportionally to their existing
|
||||
// ratios.
|
||||
const { maxBitrate, ...rest } = patch;
|
||||
for (const enc of params.encodings) Object.assign(enc, rest);
|
||||
if ("maxBitrate" in patch) {
|
||||
if (maxBitrate === undefined) {
|
||||
// Clearing the cap (#11): drop it from every encoding rather than
|
||||
// scaling — there's no budget to distribute.
|
||||
for (const enc of params.encodings) enc.maxBitrate = undefined;
|
||||
} else {
|
||||
applyProportionalMaxBitrate(params.encodings, maxBitrate);
|
||||
}
|
||||
}
|
||||
await sender.setParameters(params);
|
||||
// Remember only the caps that are still active (defined) after this
|
||||
// write, so a later clear knows exactly which keys to unset.
|
||||
|
||||
Reference in New Issue
Block a user