From dedf4e158e23baed15c941446ef3442244483e7f Mon Sep 17 00:00:00 2001 From: Ryan Emmick Date: Thu, 5 Mar 2026 13:38:15 -0600 Subject: [PATCH] refactor: move media settings to developer tab, centralize config defaults --- src/Slider.tsx | 9 +- src/config/ConfigOptions.ts | 31 +++ src/livekit/options.test.ts | 10 +- src/livekit/options.ts | 35 +-- src/settings/DeveloperSettingsTab.tsx | 252 ++++++++++++++++++ src/settings/SettingsModal.tsx | 242 ----------------- src/settings/settings.ts | 38 +-- .../CallViewModel/localMember/LocalMember.ts | 4 +- 8 files changed, 332 insertions(+), 289 deletions(-) diff --git a/src/Slider.tsx b/src/Slider.tsx index c2465874b..29f9ef426 100644 --- a/src/Slider.tsx +++ b/src/Slider.tsx @@ -77,7 +77,14 @@ export const Slider: FC = ({ {/* Note: This is expected not to be visible on mobile.*/} - + diff --git a/src/config/ConfigOptions.ts b/src/config/ConfigOptions.ts index 914efbd2b..cc625d483 100644 --- a/src/config/ConfigOptions.ts +++ b/src/config/ConfigOptions.ts @@ -211,6 +211,24 @@ export interface ResolvedConfigOptions extends ConfigOptions { }; }; ssla: string; + media_quality: Required< + Pick, "video_codec"> + > & { + video: Required< + Pick< + NonNullable["video"]>, + "max_resolution" | "max_bitrate" | "max_framerate" + > + >; + screen_share: Required< + Pick< + NonNullable< + NonNullable["screen_share"] + >, + "max_resolution" | "max_bitrate" | "max_framerate" + > + >; + }; } export const DEFAULT_CONFIG: ResolvedConfigOptions = { @@ -224,4 +242,17 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = { feature_use_device_session_member_events: true, }, ssla: "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf", + media_quality: { + video_codec: "vp8", + video: { + max_resolution: 720, + max_bitrate: 1_700_000, + max_framerate: 30, + }, + screen_share: { + max_resolution: 1080, + max_bitrate: 5_000_000, + max_framerate: 30, + }, + }, }; diff --git a/src/livekit/options.test.ts b/src/livekit/options.test.ts index 29b806086..e48bcad4f 100644 --- a/src/livekit/options.test.ts +++ b/src/livekit/options.test.ts @@ -75,14 +75,15 @@ describe("buildLiveKitOptions", () => { }); }); - it("uses 720p screen share preset for low resolution", () => { + it("uses DEFAULT_CONFIG defaults when only resolution is set", () => { const opts = buildLiveKitOptions({ screen_share: { max_resolution: 720, }, }); + // Bitrate and framerate fall back to DEFAULT_CONFIG, not the preset expect(opts.publishDefaults?.screenShareEncoding).toEqual({ - maxBitrate: 2_000_000, + maxBitrate: 5_000_000, maxFramerate: 30, }); }); @@ -155,11 +156,10 @@ describe("getLiveKitOptions", () => { expect(opts.publishDefaults?.videoCodec).toBe("h264"); }); - it("falls back to defaults when Config throws", () => { + it("throws when Config is not initialized", () => { vi.mocked(Config.get).mockImplementation(() => { throw new Error("Config not initialized"); }); - const opts = getLiveKitOptions(); - expect(opts.publishDefaults?.videoCodec).toBe("vp8"); + expect(() => getLiveKitOptions()).toThrow("Config not initialized"); }); }); diff --git a/src/livekit/options.ts b/src/livekit/options.ts index aac7ce583..a4972f002 100644 --- a/src/livekit/options.ts +++ b/src/livekit/options.ts @@ -9,7 +9,6 @@ import { AudioPresets, DefaultReconnectPolicy, type RoomOptions, - ScreenSharePresets, type TrackPublishDefaults, type VideoPreset, VideoPresets, @@ -17,7 +16,7 @@ import { } from "livekit-client"; import { Config } from "../config/Config"; -import type { ConfigOptions } from "../config/ConfigOptions"; +import { DEFAULT_CONFIG, type ConfigOptions } from "../config/ConfigOptions"; /** * Find the closest matching VideoPreset for a given height. @@ -38,16 +37,15 @@ function videoPresetForHeight(height: number): VideoPreset { function buildPublishOptions( mediaQuality: ConfigOptions["media_quality"], ): TrackPublishDefaults { + const defaults = DEFAULT_CONFIG.media_quality; const videoConf = mediaQuality?.video; const screenConf = mediaQuality?.screen_share; - const codec = mediaQuality?.video_codec ?? "vp8"; + const codec = mediaQuality?.video_codec ?? defaults.video_codec; // Camera video encoding - const videoHeight = videoConf?.max_resolution ?? 720; - const basePreset = videoPresetForHeight(videoHeight); const videoEncoding = { - maxBitrate: videoConf?.max_bitrate ?? basePreset.encoding.maxBitrate, - maxFramerate: videoConf?.max_framerate ?? basePreset.encoding.maxFramerate, + maxBitrate: videoConf?.max_bitrate ?? defaults.video.max_bitrate, + maxFramerate: videoConf?.max_framerate ?? defaults.video.max_framerate, }; // Camera simulcast layers @@ -59,7 +57,7 @@ function buildPublishOptions( Math.round((layer.height * 16) / 9), layer.height, layer.bitrate, - videoConf?.max_framerate ?? 30, + videoConf?.max_framerate ?? defaults.video.max_framerate, ), ); } else { @@ -67,15 +65,10 @@ function buildPublishOptions( } // Screen share encoding - const screenHeight = screenConf?.max_resolution ?? 1080; - const screenBasePreset = - screenHeight <= 720 - ? ScreenSharePresets.h720fps30 - : ScreenSharePresets.h1080fps30; const screenShareEncoding = { - maxBitrate: screenConf?.max_bitrate ?? screenBasePreset.encoding.maxBitrate, + maxBitrate: screenConf?.max_bitrate ?? defaults.screen_share.max_bitrate, maxFramerate: - screenConf?.max_framerate ?? screenBasePreset.encoding.maxFramerate, + screenConf?.max_framerate ?? defaults.screen_share.max_framerate, }; // Screen share simulcast layers @@ -122,7 +115,9 @@ function buildPublishOptions( export function buildLiveKitOptions( mediaQuality?: ConfigOptions["media_quality"], ): RoomOptions { - const videoHeight = mediaQuality?.video?.max_resolution ?? 720; + const videoHeight = + mediaQuality?.video?.max_resolution ?? + DEFAULT_CONFIG.media_quality.video.max_resolution; const basePreset = videoPresetForHeight(videoHeight); return { @@ -150,12 +145,8 @@ export function buildLiveKitOptions( /** * Get LiveKit options, reading from the loaded Config singleton. - * Falls back to defaults if Config is not yet initialized. + * Requires Config.init() to have resolved first. */ export function getLiveKitOptions(): RoomOptions { - try { - return buildLiveKitOptions(Config.get().media_quality); - } catch { - return buildLiveKitOptions(); - } + return buildLiveKitOptions(Config.get().media_quality); } diff --git a/src/settings/DeveloperSettingsTab.tsx b/src/settings/DeveloperSettingsTab.tsx index 9df6181f9..8e5a8594f 100644 --- a/src/settings/DeveloperSettingsTab.tsx +++ b/src/settings/DeveloperSettingsTab.tsx @@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details. import { type ChangeEvent, type FC, + type ReactNode, useCallback, useEffect, useMemo, @@ -29,11 +30,13 @@ import { InlineField, Label, RadioControl, + Separator, } from "@vector-im/compound-web"; import { type Room as LivekitRoom } from "livekit-client"; import { FieldRow, InputField } from "../input/Input"; import { + type Setting, useSetting, duplicateTiles as duplicateTilesSetting, debugTileLayout as debugTileLayoutSetting, @@ -43,8 +46,24 @@ import { matrixRTCMode as matrixRTCModeSetting, customLivekitUrl as customLivekitUrlSetting, MatrixRTCMode, + advancedScreenShare as advancedScreenShareSetting, + screenShareResolution as screenShareResolutionSetting, + screenShareFramerate as screenShareFramerateSetting, + screenShareBitrate as screenShareBitrateSetting, + screenShareCodec as screenShareCodecSetting, + advancedCamera as advancedCameraSetting, + cameraResolution as cameraResolutionSetting, + cameraFramerate as cameraFramerateSetting, + cameraBitrate as cameraBitrateSetting, + cameraCodec as cameraCodecSetting, + echoCancellationSetting, + noiseSuppressionSetting, + autoGainControlSetting, + type VideoCodec, } from "./settings"; import styles from "./DeveloperSettingsTab.module.css"; +import settingsStyles from "./SettingsModal.module.css"; +import { Slider } from "../Slider"; import { useUrlParams } from "../UrlParams"; import { getSFUConfigWithOpenID } from "../livekit/openIDSFU"; @@ -127,6 +146,185 @@ export const DeveloperSettingsTab: FC = ({ return null; }, [livekitRooms]); + const MediaQualitySettings: React.FC<{ + id: string; + header: string; + toggleLabel: string; + description: string; + toggleSetting: Setting; + resolutionSetting: Setting; + framerateSetting: Setting; + bitrateSetting: Setting; + codecSetting: Setting; + resolutionOptions: { value: string; label: string }[]; + bitrateRange: { min: number; max: number; step: number }; + }> = ({ + id, + header, + toggleLabel, + description, + toggleSetting, + resolutionSetting, + framerateSetting, + bitrateSetting, + codecSetting, + resolutionOptions, + bitrateRange, + }): ReactNode => { + const [advancedEnabled, setAdvancedEnabled] = useSetting(toggleSetting); + const [resolution, setResolution] = useSetting(resolutionSetting); + const [framerate, setFramerate] = useSetting(framerateSetting); + const [framerateRaw, setFramerateRaw] = useState(framerate); + const [bitrate, setBitrate] = useSetting(bitrateSetting); + const [bitrateRaw, setBitrateRaw] = useState(bitrate); + const [codec, setCodec] = useSetting(codecSetting); + + return ( + <> +

{header}

+ + setAdvancedEnabled(e.target.checked)} + /> + + {advancedEnabled && ( + <> +
+ + +
+
+ + `${v} fps`} + /> +
+
+ + + `${(v / 1_000_000).toFixed(1)} Mbps` + } + /> +
+
+ + +
+ + )} + + ); + }; + + const AudioProcessingSettings: React.FC = (): ReactNode => { + const [echoCancellation, setEchoCancellation] = useSetting( + echoCancellationSetting, + ); + const [noiseSuppression, setNoiseSuppression] = useSetting( + noiseSuppressionSetting, + ); + const [autoGainControl, setAutoGainControl] = useSetting( + autoGainControlSetting, + ); + + return ( + <> +

{t("settings.audio_processing_header", "Audio processing")}

+

+ {t( + "settings.audio_processing_description", + "Changes apply on next call join.", + )} +

+ + setEchoCancellation(e.target.checked)} + /> + + + setNoiseSuppression(e.target.checked)} + /> + + + setAutoGainControl(e.target.checked)} + /> + + + ); + }; + return ( <>

@@ -379,6 +577,60 @@ export const DeveloperSettingsTab: FC = ({

{JSON.stringify(env, null, 2)}

{t("developer_mode.url_params")}

{JSON.stringify(urlParams, null, 2)}
+ + + + + + ); }; diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 8acc400d1..0e97931e6 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -20,25 +20,10 @@ import { iosDeviceMenu$ } from "../state/MediaDevices"; import { useMediaDevices } from "../MediaDevicesContext"; import { widget } from "../widget"; import { - type Setting, useSetting, soundEffectVolume as soundEffectVolumeSetting, backgroundBlur as backgroundBlurSetting, developerMode, - advancedScreenShare as advancedScreenShareSetting, - screenShareResolution as screenShareResolutionSetting, - screenShareFramerate as screenShareFramerateSetting, - screenShareBitrate as screenShareBitrateSetting, - screenShareCodec as screenShareCodecSetting, - advancedCamera as advancedCameraSetting, - cameraResolution as cameraResolutionSetting, - cameraFramerate as cameraFramerateSetting, - cameraBitrate as cameraBitrateSetting, - cameraCodec as cameraCodecSetting, - echoCancellationSetting, - noiseSuppressionSetting, - autoGainControlSetting, - type VideoCodec, } from "./settings"; import { PreferencesSettingsTab } from "./PreferencesSettingsTab"; import { Slider } from "../Slider"; @@ -113,191 +98,6 @@ export const SettingsModal: FC = ({ ); }; - const MediaQualitySettings: React.FC<{ - id: string; - header: string; - toggleLabel: string; - description: string; - toggleSetting: Setting; - resolutionSetting: Setting; - framerateSetting: Setting; - bitrateSetting: Setting; - codecSetting: Setting; - resolutionOptions: { value: string; label: string }[]; - bitrateRange: { min: number; max: number; step: number }; - }> = ({ - id, - header, - toggleLabel, - description, - toggleSetting, - resolutionSetting, - framerateSetting, - bitrateSetting, - codecSetting, - resolutionOptions, - bitrateRange, - }): ReactNode => { - const [advancedEnabled, setAdvancedEnabled] = useSetting(toggleSetting); - const [resolution, setResolution] = useSetting(resolutionSetting); - const [framerate, setFramerate] = useSetting(framerateSetting); - const [framerateRaw, setFramerateRaw] = useState(framerate); - const [bitrate, setBitrate] = useSetting(bitrateSetting); - const [bitrateRaw, setBitrateRaw] = useState(bitrate); - const [codec, setCodec] = useSetting(codecSetting); - - return ( - <> -

{header}

- - setAdvancedEnabled(e.target.checked)} - /> - - {advancedEnabled && ( - <> -
- - -
-
- - `${v} fps`} - /> -
-
- - - `${(v / 1_000_000).toFixed(1)} Mbps` - } - /> -
-
- - -
- - )} - - ); - }; - - const AudioProcessingSettings: React.FC = (): ReactNode => { - const [echoCancellation, setEchoCancellation] = useSetting( - echoCancellationSetting, - ); - const [noiseSuppression, setNoiseSuppression] = useSetting( - noiseSuppressionSetting, - ); - const [autoGainControl, setAutoGainControl] = useSetting( - autoGainControlSetting, - ); - - return ( - <> -

{t("settings.audio_processing_header", "Audio processing")}

-

- {t( - "settings.audio_processing_description", - "Changes apply on next call join.", - )} -

- - setEchoCancellation(e.target.checked)} - /> - - - setNoiseSuppression(e.target.checked)} - /> - - - setAutoGainControl(e.target.checked)} - /> - - - ); - }; - const devices = useMediaDevices(); useEffect(() => { if (open) devices.requestDeviceNames(); @@ -371,8 +171,6 @@ export const SettingsModal: FC = ({ /> - - ), }; @@ -391,46 +189,6 @@ export const SettingsModal: FC = ({ - - - - ), }; diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 48f5ee55c..40d706133 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -169,7 +169,10 @@ export type VideoCodec = "vp8" | "vp9" | "h264" | "av1"; /** * Parse a "WIDTHxHEIGHT" resolution string into numeric width and height. */ -export function parseResolution(res: string): { width: number; height: number } { +export function parseResolution(res: string): { + width: number; + height: number; +} { const [w, h] = res.split("x").map(Number); return { width: w, height: h }; } @@ -209,10 +212,7 @@ export const cameraResolution = new Setting( export const cameraFramerate = new Setting("camera-framerate", 30); -export const cameraBitrate = new Setting( - "camera-bitrate", - 1_700_000, -); +export const cameraBitrate = new Setting("camera-bitrate", 1_700_000); export const cameraCodec = new Setting("camera-codec", "vp8"); @@ -238,19 +238,21 @@ export const autoGainControlSetting = new Setting( * Only updates settings that the user hasn't explicitly set in localStorage. */ export function seedSettingsFromConfig( - mediaQuality: { - video_codec?: VideoCodec; - video?: { - max_resolution?: number; - max_bitrate?: number; - max_framerate?: number; - }; - screen_share?: { - max_resolution?: number; - max_bitrate?: number; - max_framerate?: number; - }; - } | undefined, + mediaQuality: + | { + video_codec?: VideoCodec; + video?: { + max_resolution?: number; + max_bitrate?: number; + max_framerate?: number; + }; + screen_share?: { + max_resolution?: number; + max_bitrate?: number; + max_framerate?: number; + }; + } + | undefined, ): void { if (!mediaQuality) return; diff --git a/src/state/CallViewModel/localMember/LocalMember.ts b/src/state/CallViewModel/localMember/LocalMember.ts index 002dba6b8..981793f1f 100644 --- a/src/state/CallViewModel/localMember/LocalMember.ts +++ b/src/state/CallViewModel/localMember/LocalMember.ts @@ -689,7 +689,9 @@ export const createLocalMembership$ = ({ if (advancedScreenShare.getValue()) { // User has advanced screen share settings enabled - const { width, height } = parseResolution(screenShareResolution.getValue()); + const { width, height } = parseResolution( + screenShareResolution.getValue(), + ); const fps = screenShareFramerate.getValue(); const bps = screenShareBitrate.getValue(); const codec = screenShareCodec.getValue();