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; max: number;
step: number; step: number;
disabled?: boolean; 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, max,
step, step,
disabled, disabled,
tooltipFormatter,
}) => { }) => {
const onValueChange = useCallback( const onValueChange = useCallback(
([v]: number[]) => onValueChangeProp(v), ([v]: number[]) => onValueChangeProp(v),
@@ -71,7 +77,7 @@ export const Slider: FC<Props> = ({
<Range className={styles.highlight} /> <Range className={styles.highlight} />
</Track> </Track>
{/* Note: This is expected not to be visible on mobile.*/} {/* 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} /> <Thumb className={styles.handle} aria-label={label} />
</Tooltip> </Tooltip>
</Root> </Root>

View File

@@ -24,6 +24,12 @@ import {
soundEffectVolume as soundEffectVolumeSetting, soundEffectVolume as soundEffectVolumeSetting,
backgroundBlur as backgroundBlurSetting, backgroundBlur as backgroundBlurSetting,
developerMode, developerMode,
advancedScreenShare as advancedScreenShareSetting,
screenShareResolution as screenShareResolutionSetting,
screenShareFramerate as screenShareFramerateSetting,
screenShareBitrate as screenShareBitrateSetting,
screenShareCodec as screenShareCodecSetting,
type VideoCodec,
} from "./settings"; } from "./settings";
import { PreferencesSettingsTab } from "./PreferencesSettingsTab"; import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
import { Slider } from "../Slider"; 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(); const devices = useMediaDevices();
useEffect(() => { useEffect(() => {
if (open) devices.requestDeviceNames(); if (open) devices.requestDeviceNames();
@@ -183,6 +293,8 @@ export const SettingsModal: FC<Props> = ({
</Form> </Form>
<Separator /> <Separator />
<BlurCheckbox /> <BlurCheckbox />
<Separator />
<ScreenShareSettings />
</> </>
), ),
}; };

View File

@@ -150,3 +150,30 @@ export const customLivekitUrl = new Setting<string | null>(
"custom-livekit-url", "custom-livekit-url",
null, 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, ParticipantEvent,
type LocalParticipant, type LocalParticipant,
type ScreenShareCaptureOptions, type ScreenShareCaptureOptions,
type TrackPublishOptions,
RoomEvent, RoomEvent,
MediaDeviceFailure, MediaDeviceFailure,
type ScreenSharePreset,
VideoPreset as VideoPresetClass,
} from "livekit-client"; } from "livekit-client";
import { observeParticipantEvents } from "@livekit/components-core"; import { observeParticipantEvents } from "@livekit/components-core";
import { import {
@@ -55,7 +54,14 @@ import {
import { ElementWidgetActions, widget } from "../../../widget.ts"; import { ElementWidgetActions, widget } from "../../../widget.ts";
import { getUrlParams } from "../../../UrlParams.ts"; import { getUrlParams } from "../../../UrlParams.ts";
import { PosthogAnalytics } from "../../../analytics/PosthogAnalytics.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 { Config } from "../../../config/Config.ts";
import { import {
ConnectionState, ConnectionState,
@@ -663,7 +669,6 @@ export const createLocalMembership$ = ({
!getUrlParams().hideScreensharing !getUrlParams().hideScreensharing
) { ) {
toggleScreenSharing = (): void => { toggleScreenSharing = (): void => {
const screenConf = Config.get().media_quality?.screen_share;
const screenshareSettings: ScreenShareCaptureOptions = { const screenshareSettings: ScreenShareCaptureOptions = {
// Screen share audio shouldn't have any filtering. // Screen share audio shouldn't have any filtering.
// "echoCancellation" is purposely excluded, as setting it to // "echoCancellation" is purposely excluded, as setting it to
@@ -677,14 +682,44 @@ export const createLocalMembership$ = ({
selfBrowserSurface: "include", selfBrowserSurface: "include",
surfaceSwitching: "include", surfaceSwitching: "include",
systemAudio: "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), width: Math.round((screenConf.max_resolution * 16) / 9),
height: screenConf.max_resolution, height: screenConf.max_resolution,
frameRate: screenConf.max_framerate ?? 30, frameRate: screenConf.max_framerate ?? 30,
}, };
}), }
}; }
const targetScreenshareState = !sharingScreen$.value; const targetScreenshareState = !sharingScreen$.value;
logger.info( logger.info(
`toggleScreenSharing called. Switching ${ `toggleScreenSharing called. Switching ${
@@ -700,7 +735,11 @@ export const createLocalMembership$ = ({
// is still initializing or publishing tracks, because there's no // is still initializing or publishing tracks, because there's no
// technical reason to disallow this. LiveKit will publish if it can. // technical reason to disallow this. LiveKit will publish if it can.
participant$.value participant$.value
?.setScreenShareEnabled(targetScreenshareState, screenshareSettings) ?.setScreenShareEnabled(
targetScreenshareState,
screenshareSettings,
publishOptions,
)
.catch(logger.error); .catch(logger.error);
}; };
} }