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
@@ -7,7 +7,11 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import { buildPatch, patchSender } from "./lotusQuality";
|
||||
import {
|
||||
applyProportionalMaxBitrate,
|
||||
buildPatch,
|
||||
patchSender,
|
||||
} from "./lotusQuality";
|
||||
|
||||
function makeSender(
|
||||
initialEncodings: RTCRtpEncodingParameters[] = [{}],
|
||||
@@ -91,3 +95,65 @@ describe("lotusQuality set_quality -> clear (#11)", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("lotusQuality screenshare simulcast cap (#12)", () => {
|
||||
test("caps the aggregate across a 3-layer simulcast publication, not per-layer", async () => {
|
||||
// A typical VP8 simulcast screenshare publication: low/medium/high
|
||||
// layers with existing presets, ordered low-to-high like LiveKit
|
||||
// publishes them.
|
||||
const sender = makeSender([
|
||||
{ maxBitrate: 150_000 },
|
||||
{ maxBitrate: 500_000 },
|
||||
{ maxBitrate: 1_500_000 },
|
||||
]);
|
||||
const writtenKeys = new WeakMap<
|
||||
RTCRtpSender,
|
||||
Set<keyof RTCRtpEncodingParameters>
|
||||
>();
|
||||
|
||||
const cap = 750_000;
|
||||
const patch = buildPatch(sender, { maxBitrate: cap }, writtenKeys);
|
||||
await patchSender(sender, patch, writtenKeys);
|
||||
|
||||
const encodings = sender.getParameters().encodings;
|
||||
expect(encodings).toHaveLength(3);
|
||||
// Each layer keeps its old proportion of the total, but the aggregate
|
||||
// across all layers must not exceed the requested cap — that's the bug:
|
||||
// writing `cap` into every layer let the aggregate reach ~3x the cap.
|
||||
const oldTotal = 150_000 + 500_000 + 1_500_000;
|
||||
expect(encodings[0].maxBitrate).toBe(
|
||||
Math.floor((150_000 / oldTotal) * cap),
|
||||
);
|
||||
expect(encodings[1].maxBitrate).toBe(
|
||||
Math.floor((500_000 / oldTotal) * cap),
|
||||
);
|
||||
expect(encodings[2].maxBitrate).toBe(
|
||||
Math.floor((1_500_000 / oldTotal) * cap),
|
||||
);
|
||||
// The highest layer still ends up with the largest share.
|
||||
expect(encodings[2].maxBitrate).toBeGreaterThan(encodings[1].maxBitrate!);
|
||||
expect(encodings[1].maxBitrate).toBeGreaterThan(encodings[0].maxBitrate!);
|
||||
|
||||
const aggregate = encodings.reduce(
|
||||
(sum, enc) => sum + (enc.maxBitrate ?? 0),
|
||||
0,
|
||||
);
|
||||
expect(aggregate).toBeLessThanOrEqual(cap);
|
||||
});
|
||||
|
||||
test("a single encoding keeps the previous (non-scaled) behaviour", () => {
|
||||
const encodings: RTCRtpEncodingParameters[] = [{ maxBitrate: 2_000_000 }];
|
||||
applyProportionalMaxBitrate(encodings, 500_000);
|
||||
expect(encodings).toEqual([{ maxBitrate: 500_000 }]);
|
||||
});
|
||||
|
||||
test("falls back to capping only the highest layer when there are no prior ratios", () => {
|
||||
const encodings: RTCRtpEncodingParameters[] = [
|
||||
{}, // no prior cap on any layer to derive a ratio from
|
||||
{},
|
||||
];
|
||||
applyProportionalMaxBitrate(encodings, 500_000);
|
||||
expect(encodings[0].maxBitrate).toBeUndefined();
|
||||
expect(encodings[1].maxBitrate).toBe(500_000);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user