feat: add user-configurable screen share quality settings UI

Adds a "Screen sharing" section to Settings > Video with controls for:
- Resolution (576p to 4K)
- Framerate (5-60 fps slider)
- Bitrate (0.5-15 Mbps slider)
- Codec (VP8/VP9/H.264/AV1)

Gated behind an "Advanced screen share settings" toggle. When enabled,
settings are passed to LiveKit's setScreenShareEnabled as both capture
constraints and publish options. When disabled, falls back to
config.json media_quality defaults.

Settings are persisted in localStorage via the existing Setting<T>
system. The Slider component is extended with a tooltipFormatter prop
for custom tooltip display.

Inspired by pirosuki's advanced-screen-share-settings branch, but
reimplemented cleanly: settings are read directly in LocalMember.ts
(no signature changes), the existing Slider is extended (no component
duplication), and proper form components are used throughout.

Signed-off-by: Ryan Emmick <ryanemmick4@gmail.com>
This commit is contained in:
Ryan Emmick
2026-02-11 02:03:08 -06:00
parent 390dc22c96
commit 342b61f633
4 changed files with 195 additions and 11 deletions

View File

@@ -31,6 +31,11 @@ interface Props {
max: number;
step: number;
disabled?: boolean;
/**
* Custom formatter for the tooltip label. If not provided, the value is
* displayed as a percentage.
*/
tooltipFormatter?: (value: number) => string;
}
/**
@@ -46,6 +51,7 @@ export const Slider: FC<Props> = ({
max,
step,
disabled,
tooltipFormatter,
}) => {
const onValueChange = useCallback(
([v]: number[]) => onValueChangeProp(v),
@@ -71,7 +77,7 @@ export const Slider: FC<Props> = ({
<Range className={styles.highlight} />
</Track>
{/* Note: This is expected not to be visible on mobile.*/}
<Tooltip placement="top" label={Math.round(value * 100).toString() + "%"}>
<Tooltip placement="top" label={tooltipFormatter ? tooltipFormatter(value) : Math.round(value * 100).toString() + "%"}>
<Thumb className={styles.handle} aria-label={label} />
</Tooltip>
</Root>

View File

@@ -24,6 +24,12 @@ import {
soundEffectVolume as soundEffectVolumeSetting,
backgroundBlur as backgroundBlurSetting,
developerMode,
advancedScreenShare as advancedScreenShareSetting,
screenShareResolution as screenShareResolutionSetting,
screenShareFramerate as screenShareFramerateSetting,
screenShareBitrate as screenShareBitrateSetting,
screenShareCodec as screenShareCodecSetting,
type VideoCodec,
} from "./settings";
import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
import { Slider } from "../Slider";
@@ -98,6 +104,110 @@ export const SettingsModal: FC<Props> = ({
);
};
const ScreenShareSettings: React.FC = (): ReactNode => {
const [advancedEnabled, setAdvancedEnabled] = useSetting(
advancedScreenShareSetting,
);
const [resolution, setResolution] = useSetting(
screenShareResolutionSetting,
);
const [framerate, setFramerate] = useSetting(screenShareFramerateSetting);
const [framerateRaw, setFramerateRaw] = useState(framerate);
const [bitrate, setBitrate] = useSetting(screenShareBitrateSetting);
const [bitrateRaw, setBitrateRaw] = useState(bitrate);
const [codec, setCodec] = useSetting(screenShareCodecSetting);
return (
<>
<h4>{t("settings.screen_share_header", "Screen sharing")}</h4>
<FieldRow>
<InputField
id="advancedScreenShare"
label={t(
"settings.advanced_screen_share_label",
"Advanced screen share settings",
)}
description={t(
"settings.advanced_screen_share_description",
"Configure resolution, framerate, bitrate, and codec for screen sharing",
)}
type="checkbox"
checked={advancedEnabled}
onChange={(e): void => setAdvancedEnabled(e.target.checked)}
/>
</FieldRow>
{advancedEnabled && (
<>
<FieldRow>
<InputField
id="screenShareResolution"
label={t(
"settings.screen_share_resolution_label",
"Resolution",
)}
type="select"
value={resolution}
onChange={(e): void => setResolution(e.target.value)}
>
<option value="1024x576">576p</option>
<option value="1280x720">720p</option>
<option value="1920x1080">1080p</option>
<option value="2560x1440">1440p</option>
<option value="3840x2160">4K</option>
</InputField>
</FieldRow>
<div className={styles.volumeSlider}>
<label>
{t("settings.screen_share_framerate_label", "Framerate")}
</label>
<Slider
label={t("settings.screen_share_framerate_label", "Framerate")}
value={framerateRaw}
onValueChange={setFramerateRaw}
onValueCommit={setFramerate}
min={5}
max={60}
step={5}
tooltipFormatter={(v): string => `${v} fps`}
/>
</div>
<div className={styles.volumeSlider}>
<label>
{t("settings.screen_share_bitrate_label", "Bitrate")}
</label>
<Slider
label={t("settings.screen_share_bitrate_label", "Bitrate")}
value={bitrateRaw}
onValueChange={setBitrateRaw}
onValueCommit={setBitrate}
min={500_000}
max={15_000_000}
step={500_000}
tooltipFormatter={(v): string =>
`${(v / 1_000_000).toFixed(1)} Mbps`
}
/>
</div>
<FieldRow>
<InputField
id="screenShareCodec"
label={t("settings.screen_share_codec_label", "Codec")}
type="select"
value={codec}
onChange={(e): void => setCodec(e.target.value as VideoCodec)}
>
<option value="vp8">VP8</option>
<option value="vp9">VP9</option>
<option value="h264">H.264</option>
<option value="av1">AV1</option>
</InputField>
</FieldRow>
</>
)}
</>
);
};
const devices = useMediaDevices();
useEffect(() => {
if (open) devices.requestDeviceNames();
@@ -183,6 +293,8 @@ export const SettingsModal: FC<Props> = ({
</Form>
<Separator />
<BlurCheckbox />
<Separator />
<ScreenShareSettings />
</>
),
};

View File

@@ -150,3 +150,30 @@ export const customLivekitUrl = new Setting<string | null>(
"custom-livekit-url",
null,
);
export type VideoCodec = "vp8" | "vp9" | "h264" | "av1";
export const advancedScreenShare = new Setting<boolean>(
"advanced-screen-share",
false,
);
export const screenShareResolution = new Setting<string>(
"screen-share-resolution",
"1920x1080",
);
export const screenShareFramerate = new Setting<number>(
"screen-share-framerate",
30,
);
export const screenShareBitrate = new Setting<number>(
"screen-share-bitrate",
5_000_000,
);
export const screenShareCodec = new Setting<VideoCodec>(
"screen-share-codec",
"vp9",
);

View File

@@ -10,10 +10,9 @@ import {
ParticipantEvent,
type LocalParticipant,
type ScreenShareCaptureOptions,
type TrackPublishOptions,
RoomEvent,
MediaDeviceFailure,
type ScreenSharePreset,
VideoPreset as VideoPresetClass,
} from "livekit-client";
import { observeParticipantEvents } from "@livekit/components-core";
import {
@@ -55,7 +54,14 @@ import {
import { ElementWidgetActions, widget } from "../../../widget.ts";
import { getUrlParams } from "../../../UrlParams.ts";
import { PosthogAnalytics } from "../../../analytics/PosthogAnalytics.ts";
import { MatrixRTCMode } from "../../../settings/settings.ts";
import {
MatrixRTCMode,
advancedScreenShare,
screenShareResolution,
screenShareFramerate,
screenShareBitrate,
screenShareCodec,
} from "../../../settings/settings.ts";
import { Config } from "../../../config/Config.ts";
import {
ConnectionState,
@@ -663,7 +669,6 @@ export const createLocalMembership$ = ({
!getUrlParams().hideScreensharing
) {
toggleScreenSharing = (): void => {
const screenConf = Config.get().media_quality?.screen_share;
const screenshareSettings: ScreenShareCaptureOptions = {
// Screen share audio shouldn't have any filtering.
// "echoCancellation" is purposely excluded, as setting it to
@@ -677,14 +682,44 @@ export const createLocalMembership$ = ({
selfBrowserSurface: "include",
surfaceSwitching: "include",
systemAudio: "include",
...(screenConf?.max_resolution && {
resolution: {
};
let publishOptions: TrackPublishOptions | undefined;
if (advancedScreenShare.getValue()) {
// User has advanced screen share settings enabled
const resParts = screenShareResolution.getValue().split("x");
const width = Number(resParts[0]);
const height = Number(resParts[1]);
const fps = screenShareFramerate.getValue();
const bps = screenShareBitrate.getValue();
const codec = screenShareCodec.getValue();
screenshareSettings.resolution = {
width,
height,
frameRate: fps,
};
publishOptions = {
screenShareEncoding: {
maxBitrate: bps,
maxFramerate: fps,
},
videoCodec: codec,
};
} else {
// Fall back to config.json settings if available
const screenConf = Config.get().media_quality?.screen_share;
if (screenConf?.max_resolution) {
screenshareSettings.resolution = {
width: Math.round((screenConf.max_resolution * 16) / 9),
height: screenConf.max_resolution,
frameRate: screenConf.max_framerate ?? 30,
},
}),
};
};
}
}
const targetScreenshareState = !sharingScreen$.value;
logger.info(
`toggleScreenSharing called. Switching ${
@@ -700,7 +735,11 @@ export const createLocalMembership$ = ({
// is still initializing or publishing tracks, because there's no
// technical reason to disallow this. LiveKit will publish if it can.
participant$.value
?.setScreenShareEnabled(targetScreenshareState, screenshareSettings)
?.setScreenShareEnabled(
targetScreenshareState,
screenshareSettings,
publishOptions,
)
.catch(logger.error);
};
}