diff --git a/src/Slider.tsx b/src/Slider.tsx index c6520e422..c2465874b 100644 --- a/src/Slider.tsx +++ b/src/Slider.tsx @@ -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 = ({ max, step, disabled, + tooltipFormatter, }) => { const onValueChange = useCallback( ([v]: number[]) => onValueChangeProp(v), @@ -71,7 +77,7 @@ export const Slider: FC = ({ {/* Note: This is expected not to be visible on mobile.*/} - + diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 30ac36185..6ccafec1e 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -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 = ({ ); }; + 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 ( + <> +

{t("settings.screen_share_header", "Screen sharing")}

+ + setAdvancedEnabled(e.target.checked)} + /> + + {advancedEnabled && ( + <> + + setResolution(e.target.value)} + > + + + + + + + +
+ + `${v} fps`} + /> +
+
+ + + `${(v / 1_000_000).toFixed(1)} Mbps` + } + /> +
+ + setCodec(e.target.value as VideoCodec)} + > + + + + + + + + )} + + ); + }; + const devices = useMediaDevices(); useEffect(() => { if (open) devices.requestDeviceNames(); @@ -183,6 +293,8 @@ export const SettingsModal: FC = ({ + + ), }; diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 917c79f16..141d4449b 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -150,3 +150,30 @@ export const customLivekitUrl = new Setting( "custom-livekit-url", null, ); + +export type VideoCodec = "vp8" | "vp9" | "h264" | "av1"; + +export const advancedScreenShare = new Setting( + "advanced-screen-share", + false, +); + +export const screenShareResolution = new Setting( + "screen-share-resolution", + "1920x1080", +); + +export const screenShareFramerate = new Setting( + "screen-share-framerate", + 30, +); + +export const screenShareBitrate = new Setting( + "screen-share-bitrate", + 5_000_000, +); + +export const screenShareCodec = new Setting( + "screen-share-codec", + "vp9", +); diff --git a/src/state/CallViewModel/localMember/LocalMember.ts b/src/state/CallViewModel/localMember/LocalMember.ts index 5cbe855a9..7a954ae73 100644 --- a/src/state/CallViewModel/localMember/LocalMember.ts +++ b/src/state/CallViewModel/localMember/LocalMember.ts @@ -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); }; }