lotus(#7): fix screenshare cap per review

- Apply the encoding patch to ALL simulcast layers, not just encodings[0]:
  screenshare publishes simulcast (VP8), so the full-res layer (the real
  bandwidth hog) was left uncapped (CRITICAL).
- Re-apply on TrackUnmuted/restart + a 500ms settle, since LiveKit's
  refreshSenderEncodings() overwrites our caps on replaceTrack (device/
  source switch, processor toggle) without firing LocalTrackPublished (HIGH).
- Clamp values to sane ranges so a typo can't brick the encoder (MED).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Lotus CI
2026-06-29 23:38:45 -04:00
co-authored by Claude Opus 4.8
parent 22be058108
commit 7d792c33bf
+32 -17
View File
@@ -89,17 +89,24 @@ export function startLotusQuality(vm: CallViewModel): () => void {
// Add listeners for new rooms + apply current settings to them.
for (const room of next) {
if (!roomListeners.has(room)) {
const onPublished = (): void => applyToRoom(room);
room.localParticipant.on(
// Re-apply on (re)publish AND unmute/track-restart: LiveKit's
// refreshSenderEncodings() overwrites maxBitrate/maxFramerate from the
// publish presets on replaceTrack (device/source switch, processor
// toggle, restart-on-unmute), and those paths don't emit
// LocalTrackPublished. The settle re-apply lands after LiveKit's async
// recompute so our cap wins.
const reapply = (): void => {
applyToRoom(room);
setTimeout(() => applyToRoom(room), 500);
};
const events = [
ParticipantEvent.LocalTrackPublished,
onPublished,
);
roomListeners.set(room, () =>
room.localParticipant.off(
ParticipantEvent.LocalTrackPublished,
onPublished,
),
);
ParticipantEvent.TrackUnmuted,
] as const;
for (const e of events) room.localParticipant.on(e, reapply);
roomListeners.set(room, () => {
for (const e of events) room.localParticipant.off(e, reapply);
});
applyToRoom(room);
}
}
@@ -109,14 +116,19 @@ export function startLotusQuality(vm: CallViewModel): () => void {
void w.api.transport.reply(ev.detail, {});
const data = ev.detail.data as Record<string, unknown> | undefined;
if (!data) return;
for (const key of [
"audioMaxBitrate",
"screenshareMaxBitrate",
"screenshareMaxFramerate",
] as const) {
// Clamp to sane ranges so a typo can't brick the encoder (e.g. a 1 bps mic).
const ranges: Record<keyof QualitySettings, [number, number]> = {
audioMaxBitrate: [6_000, 510_000],
screenshareMaxBitrate: [50_000, 20_000_000],
screenshareMaxFramerate: [1, 60],
};
for (const key of Object.keys(ranges) as (keyof QualitySettings)[]) {
const v = data[key];
if (v === null) settings[key] = undefined;
else if (typeof v === "number" && v > 0) settings[key] = v;
else if (typeof v === "number" && Number.isFinite(v)) {
const [lo, hi] = ranges[key];
settings[key] = Math.min(hi, Math.max(lo, v));
}
}
applyToAll();
};
@@ -139,7 +151,10 @@ async function patchSender(
const params = sender.getParameters();
if (!params.encodings || params.encodings.length === 0)
params.encodings = [{}];
Object.assign(params.encodings[0], patch);
// 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);
await sender.setParameters(params);
} catch (e) {
logger.warn("[lotus] set_quality: setParameters failed", e);