mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-29 21:15:19 +00:00
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:
@@ -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>
|
||||||
|
|||||||
@@ -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 />
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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",
|
||||||
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user