mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-29 21:15:19 +00:00
refactor: move media settings to developer tab, centralize config defaults
This commit is contained in:
@@ -77,7 +77,14 @@ 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={tooltipFormatter ? tooltipFormatter(value) : 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>
|
||||||
|
|||||||
@@ -211,6 +211,24 @@ export interface ResolvedConfigOptions extends ConfigOptions {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
ssla: string;
|
ssla: string;
|
||||||
|
media_quality: Required<
|
||||||
|
Pick<NonNullable<ConfigOptions["media_quality"]>, "video_codec">
|
||||||
|
> & {
|
||||||
|
video: Required<
|
||||||
|
Pick<
|
||||||
|
NonNullable<NonNullable<ConfigOptions["media_quality"]>["video"]>,
|
||||||
|
"max_resolution" | "max_bitrate" | "max_framerate"
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
screen_share: Required<
|
||||||
|
Pick<
|
||||||
|
NonNullable<
|
||||||
|
NonNullable<ConfigOptions["media_quality"]>["screen_share"]
|
||||||
|
>,
|
||||||
|
"max_resolution" | "max_bitrate" | "max_framerate"
|
||||||
|
>
|
||||||
|
>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_CONFIG: ResolvedConfigOptions = {
|
export const DEFAULT_CONFIG: ResolvedConfigOptions = {
|
||||||
@@ -224,4 +242,17 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = {
|
|||||||
feature_use_device_session_member_events: true,
|
feature_use_device_session_member_events: true,
|
||||||
},
|
},
|
||||||
ssla: "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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({
|
const opts = buildLiveKitOptions({
|
||||||
screen_share: {
|
screen_share: {
|
||||||
max_resolution: 720,
|
max_resolution: 720,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// Bitrate and framerate fall back to DEFAULT_CONFIG, not the preset
|
||||||
expect(opts.publishDefaults?.screenShareEncoding).toEqual({
|
expect(opts.publishDefaults?.screenShareEncoding).toEqual({
|
||||||
maxBitrate: 2_000_000,
|
maxBitrate: 5_000_000,
|
||||||
maxFramerate: 30,
|
maxFramerate: 30,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -155,11 +156,10 @@ describe("getLiveKitOptions", () => {
|
|||||||
expect(opts.publishDefaults?.videoCodec).toBe("h264");
|
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(() => {
|
vi.mocked(Config.get).mockImplementation(() => {
|
||||||
throw new Error("Config not initialized");
|
throw new Error("Config not initialized");
|
||||||
});
|
});
|
||||||
const opts = getLiveKitOptions();
|
expect(() => getLiveKitOptions()).toThrow("Config not initialized");
|
||||||
expect(opts.publishDefaults?.videoCodec).toBe("vp8");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
AudioPresets,
|
AudioPresets,
|
||||||
DefaultReconnectPolicy,
|
DefaultReconnectPolicy,
|
||||||
type RoomOptions,
|
type RoomOptions,
|
||||||
ScreenSharePresets,
|
|
||||||
type TrackPublishDefaults,
|
type TrackPublishDefaults,
|
||||||
type VideoPreset,
|
type VideoPreset,
|
||||||
VideoPresets,
|
VideoPresets,
|
||||||
@@ -17,7 +16,7 @@ import {
|
|||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
|
|
||||||
import { Config } from "../config/Config";
|
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.
|
* Find the closest matching VideoPreset for a given height.
|
||||||
@@ -38,16 +37,15 @@ function videoPresetForHeight(height: number): VideoPreset {
|
|||||||
function buildPublishOptions(
|
function buildPublishOptions(
|
||||||
mediaQuality: ConfigOptions["media_quality"],
|
mediaQuality: ConfigOptions["media_quality"],
|
||||||
): TrackPublishDefaults {
|
): TrackPublishDefaults {
|
||||||
|
const defaults = DEFAULT_CONFIG.media_quality;
|
||||||
const videoConf = mediaQuality?.video;
|
const videoConf = mediaQuality?.video;
|
||||||
const screenConf = mediaQuality?.screen_share;
|
const screenConf = mediaQuality?.screen_share;
|
||||||
const codec = mediaQuality?.video_codec ?? "vp8";
|
const codec = mediaQuality?.video_codec ?? defaults.video_codec;
|
||||||
|
|
||||||
// Camera video encoding
|
// Camera video encoding
|
||||||
const videoHeight = videoConf?.max_resolution ?? 720;
|
|
||||||
const basePreset = videoPresetForHeight(videoHeight);
|
|
||||||
const videoEncoding = {
|
const videoEncoding = {
|
||||||
maxBitrate: videoConf?.max_bitrate ?? basePreset.encoding.maxBitrate,
|
maxBitrate: videoConf?.max_bitrate ?? defaults.video.max_bitrate,
|
||||||
maxFramerate: videoConf?.max_framerate ?? basePreset.encoding.maxFramerate,
|
maxFramerate: videoConf?.max_framerate ?? defaults.video.max_framerate,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Camera simulcast layers
|
// Camera simulcast layers
|
||||||
@@ -59,7 +57,7 @@ function buildPublishOptions(
|
|||||||
Math.round((layer.height * 16) / 9),
|
Math.round((layer.height * 16) / 9),
|
||||||
layer.height,
|
layer.height,
|
||||||
layer.bitrate,
|
layer.bitrate,
|
||||||
videoConf?.max_framerate ?? 30,
|
videoConf?.max_framerate ?? defaults.video.max_framerate,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -67,15 +65,10 @@ function buildPublishOptions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Screen share encoding
|
// Screen share encoding
|
||||||
const screenHeight = screenConf?.max_resolution ?? 1080;
|
|
||||||
const screenBasePreset =
|
|
||||||
screenHeight <= 720
|
|
||||||
? ScreenSharePresets.h720fps30
|
|
||||||
: ScreenSharePresets.h1080fps30;
|
|
||||||
const screenShareEncoding = {
|
const screenShareEncoding = {
|
||||||
maxBitrate: screenConf?.max_bitrate ?? screenBasePreset.encoding.maxBitrate,
|
maxBitrate: screenConf?.max_bitrate ?? defaults.screen_share.max_bitrate,
|
||||||
maxFramerate:
|
maxFramerate:
|
||||||
screenConf?.max_framerate ?? screenBasePreset.encoding.maxFramerate,
|
screenConf?.max_framerate ?? defaults.screen_share.max_framerate,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Screen share simulcast layers
|
// Screen share simulcast layers
|
||||||
@@ -122,7 +115,9 @@ function buildPublishOptions(
|
|||||||
export function buildLiveKitOptions(
|
export function buildLiveKitOptions(
|
||||||
mediaQuality?: ConfigOptions["media_quality"],
|
mediaQuality?: ConfigOptions["media_quality"],
|
||||||
): RoomOptions {
|
): 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);
|
const basePreset = videoPresetForHeight(videoHeight);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -150,12 +145,8 @@ export function buildLiveKitOptions(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get LiveKit options, reading from the loaded Config singleton.
|
* 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 {
|
export function getLiveKitOptions(): RoomOptions {
|
||||||
try {
|
return buildLiveKitOptions(Config.get().media_quality);
|
||||||
return buildLiveKitOptions(Config.get().media_quality);
|
|
||||||
} catch {
|
|
||||||
return buildLiveKitOptions();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
import {
|
import {
|
||||||
type ChangeEvent,
|
type ChangeEvent,
|
||||||
type FC,
|
type FC,
|
||||||
|
type ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
@@ -29,11 +30,13 @@ import {
|
|||||||
InlineField,
|
InlineField,
|
||||||
Label,
|
Label,
|
||||||
RadioControl,
|
RadioControl,
|
||||||
|
Separator,
|
||||||
} from "@vector-im/compound-web";
|
} from "@vector-im/compound-web";
|
||||||
import { type Room as LivekitRoom } from "livekit-client";
|
import { type Room as LivekitRoom } from "livekit-client";
|
||||||
|
|
||||||
import { FieldRow, InputField } from "../input/Input";
|
import { FieldRow, InputField } from "../input/Input";
|
||||||
import {
|
import {
|
||||||
|
type Setting,
|
||||||
useSetting,
|
useSetting,
|
||||||
duplicateTiles as duplicateTilesSetting,
|
duplicateTiles as duplicateTilesSetting,
|
||||||
debugTileLayout as debugTileLayoutSetting,
|
debugTileLayout as debugTileLayoutSetting,
|
||||||
@@ -43,8 +46,24 @@ import {
|
|||||||
matrixRTCMode as matrixRTCModeSetting,
|
matrixRTCMode as matrixRTCModeSetting,
|
||||||
customLivekitUrl as customLivekitUrlSetting,
|
customLivekitUrl as customLivekitUrlSetting,
|
||||||
MatrixRTCMode,
|
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";
|
} from "./settings";
|
||||||
import styles from "./DeveloperSettingsTab.module.css";
|
import styles from "./DeveloperSettingsTab.module.css";
|
||||||
|
import settingsStyles from "./SettingsModal.module.css";
|
||||||
|
import { Slider } from "../Slider";
|
||||||
import { useUrlParams } from "../UrlParams";
|
import { useUrlParams } from "../UrlParams";
|
||||||
import { getSFUConfigWithOpenID } from "../livekit/openIDSFU";
|
import { getSFUConfigWithOpenID } from "../livekit/openIDSFU";
|
||||||
|
|
||||||
@@ -127,6 +146,185 @@ export const DeveloperSettingsTab: FC<Props> = ({
|
|||||||
return null;
|
return null;
|
||||||
}, [livekitRooms]);
|
}, [livekitRooms]);
|
||||||
|
|
||||||
|
const MediaQualitySettings: React.FC<{
|
||||||
|
id: string;
|
||||||
|
header: string;
|
||||||
|
toggleLabel: string;
|
||||||
|
description: string;
|
||||||
|
toggleSetting: Setting<boolean>;
|
||||||
|
resolutionSetting: Setting<string>;
|
||||||
|
framerateSetting: Setting<number>;
|
||||||
|
bitrateSetting: Setting<number>;
|
||||||
|
codecSetting: Setting<VideoCodec>;
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<h4>{header}</h4>
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
id={`${id}Toggle`}
|
||||||
|
label={toggleLabel}
|
||||||
|
description={description}
|
||||||
|
type="checkbox"
|
||||||
|
checked={advancedEnabled}
|
||||||
|
onChange={(e): void => setAdvancedEnabled(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
|
{advancedEnabled && (
|
||||||
|
<>
|
||||||
|
<div className={settingsStyles.volumeSlider}>
|
||||||
|
<label htmlFor={`${id}Resolution`}>
|
||||||
|
{t("settings.resolution_label", "Resolution")}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id={`${id}Resolution`}
|
||||||
|
value={resolution}
|
||||||
|
onChange={(e): void => setResolution(e.target.value)}
|
||||||
|
>
|
||||||
|
{resolutionOptions.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className={settingsStyles.volumeSlider}>
|
||||||
|
<label>
|
||||||
|
{t("settings.framerate_label", "Framerate")}
|
||||||
|
{": "}
|
||||||
|
<span className={settingsStyles.settingValue}>
|
||||||
|
{framerateRaw} fps
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<Slider
|
||||||
|
label={t("settings.framerate_label", "Framerate")}
|
||||||
|
value={framerateRaw}
|
||||||
|
onValueChange={setFramerateRaw}
|
||||||
|
onValueCommit={setFramerate}
|
||||||
|
min={5}
|
||||||
|
max={60}
|
||||||
|
step={5}
|
||||||
|
tooltipFormatter={(v): string => `${v} fps`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={settingsStyles.volumeSlider}>
|
||||||
|
<label>
|
||||||
|
{t("settings.bitrate_label", "Bitrate")}
|
||||||
|
{": "}
|
||||||
|
<span className={settingsStyles.settingValue}>
|
||||||
|
{(bitrateRaw / 1_000_000).toFixed(1)} Mbps
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<Slider
|
||||||
|
label={t("settings.bitrate_label", "Bitrate")}
|
||||||
|
value={bitrateRaw}
|
||||||
|
onValueChange={setBitrateRaw}
|
||||||
|
onValueCommit={setBitrate}
|
||||||
|
min={bitrateRange.min}
|
||||||
|
max={bitrateRange.max}
|
||||||
|
step={bitrateRange.step}
|
||||||
|
tooltipFormatter={(v): string =>
|
||||||
|
`${(v / 1_000_000).toFixed(1)} Mbps`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={settingsStyles.volumeSlider}>
|
||||||
|
<label htmlFor={`${id}Codec`}>
|
||||||
|
{t("settings.codec_label", "Codec")}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id={`${id}Codec`}
|
||||||
|
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>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AudioProcessingSettings: React.FC = (): ReactNode => {
|
||||||
|
const [echoCancellation, setEchoCancellation] = useSetting(
|
||||||
|
echoCancellationSetting,
|
||||||
|
);
|
||||||
|
const [noiseSuppression, setNoiseSuppression] = useSetting(
|
||||||
|
noiseSuppressionSetting,
|
||||||
|
);
|
||||||
|
const [autoGainControl, setAutoGainControl] = useSetting(
|
||||||
|
autoGainControlSetting,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4>{t("settings.audio_processing_header", "Audio processing")}</h4>
|
||||||
|
<p>
|
||||||
|
{t(
|
||||||
|
"settings.audio_processing_description",
|
||||||
|
"Changes apply on next call join.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
id="echoCancellation"
|
||||||
|
label={t("settings.echo_cancellation_label", "Echo cancellation")}
|
||||||
|
type="checkbox"
|
||||||
|
checked={echoCancellation}
|
||||||
|
onChange={(e): void => setEchoCancellation(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
id="noiseSuppression"
|
||||||
|
label={t("settings.noise_suppression_label", "Noise suppression")}
|
||||||
|
type="checkbox"
|
||||||
|
checked={noiseSuppression}
|
||||||
|
onChange={(e): void => setNoiseSuppression(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
|
<FieldRow>
|
||||||
|
<InputField
|
||||||
|
id="autoGainControl"
|
||||||
|
label={t(
|
||||||
|
"settings.auto_gain_control_label",
|
||||||
|
"Automatic gain control",
|
||||||
|
)}
|
||||||
|
type="checkbox"
|
||||||
|
checked={autoGainControl}
|
||||||
|
onChange={(e): void => setAutoGainControl(e.target.checked)}
|
||||||
|
/>
|
||||||
|
</FieldRow>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p>
|
<p>
|
||||||
@@ -379,6 +577,60 @@ export const DeveloperSettingsTab: FC<Props> = ({
|
|||||||
<pre>{JSON.stringify(env, null, 2)}</pre>
|
<pre>{JSON.stringify(env, null, 2)}</pre>
|
||||||
<p>{t("developer_mode.url_params")}</p>
|
<p>{t("developer_mode.url_params")}</p>
|
||||||
<pre>{JSON.stringify(urlParams, null, 2)}</pre>
|
<pre>{JSON.stringify(urlParams, null, 2)}</pre>
|
||||||
|
<Separator />
|
||||||
|
<MediaQualitySettings
|
||||||
|
id="camera"
|
||||||
|
header={t("settings.camera_header", "Camera quality")}
|
||||||
|
toggleLabel={t(
|
||||||
|
"settings.advanced_camera_label",
|
||||||
|
"Advanced camera settings",
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
"settings.advanced_camera_description",
|
||||||
|
"Configure resolution, framerate, bitrate, and codec for camera video. Changes apply on next call join.",
|
||||||
|
)}
|
||||||
|
toggleSetting={advancedCameraSetting}
|
||||||
|
resolutionSetting={cameraResolutionSetting}
|
||||||
|
framerateSetting={cameraFramerateSetting}
|
||||||
|
bitrateSetting={cameraBitrateSetting}
|
||||||
|
codecSetting={cameraCodecSetting}
|
||||||
|
resolutionOptions={[
|
||||||
|
{ value: "640x360", label: "360p" },
|
||||||
|
{ value: "960x540", label: "540p" },
|
||||||
|
{ value: "1280x720", label: "720p" },
|
||||||
|
{ value: "1920x1080", label: "1080p" },
|
||||||
|
{ value: "2560x1440", label: "1440p" },
|
||||||
|
]}
|
||||||
|
bitrateRange={{ min: 200_000, max: 8_000_000, step: 100_000 }}
|
||||||
|
/>
|
||||||
|
<Separator />
|
||||||
|
<MediaQualitySettings
|
||||||
|
id="screenShare"
|
||||||
|
header={t("settings.screen_share_header", "Screen sharing")}
|
||||||
|
toggleLabel={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",
|
||||||
|
)}
|
||||||
|
toggleSetting={advancedScreenShareSetting}
|
||||||
|
resolutionSetting={screenShareResolutionSetting}
|
||||||
|
framerateSetting={screenShareFramerateSetting}
|
||||||
|
bitrateSetting={screenShareBitrateSetting}
|
||||||
|
codecSetting={screenShareCodecSetting}
|
||||||
|
resolutionOptions={[
|
||||||
|
{ value: "1024x576", label: "576p" },
|
||||||
|
{ value: "1280x720", label: "720p" },
|
||||||
|
{ value: "1920x1080", label: "1080p" },
|
||||||
|
{ value: "2560x1440", label: "1440p" },
|
||||||
|
{ value: "3840x2160", label: "4K" },
|
||||||
|
]}
|
||||||
|
bitrateRange={{ min: 500_000, max: 15_000_000, step: 500_000 }}
|
||||||
|
/>
|
||||||
|
<Separator />
|
||||||
|
<AudioProcessingSettings />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,25 +20,10 @@ import { iosDeviceMenu$ } from "../state/MediaDevices";
|
|||||||
import { useMediaDevices } from "../MediaDevicesContext";
|
import { useMediaDevices } from "../MediaDevicesContext";
|
||||||
import { widget } from "../widget";
|
import { widget } from "../widget";
|
||||||
import {
|
import {
|
||||||
type Setting,
|
|
||||||
useSetting,
|
useSetting,
|
||||||
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,
|
|
||||||
advancedCamera as advancedCameraSetting,
|
|
||||||
cameraResolution as cameraResolutionSetting,
|
|
||||||
cameraFramerate as cameraFramerateSetting,
|
|
||||||
cameraBitrate as cameraBitrateSetting,
|
|
||||||
cameraCodec as cameraCodecSetting,
|
|
||||||
echoCancellationSetting,
|
|
||||||
noiseSuppressionSetting,
|
|
||||||
autoGainControlSetting,
|
|
||||||
type VideoCodec,
|
|
||||||
} from "./settings";
|
} from "./settings";
|
||||||
import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
|
import { PreferencesSettingsTab } from "./PreferencesSettingsTab";
|
||||||
import { Slider } from "../Slider";
|
import { Slider } from "../Slider";
|
||||||
@@ -113,191 +98,6 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const MediaQualitySettings: React.FC<{
|
|
||||||
id: string;
|
|
||||||
header: string;
|
|
||||||
toggleLabel: string;
|
|
||||||
description: string;
|
|
||||||
toggleSetting: Setting<boolean>;
|
|
||||||
resolutionSetting: Setting<string>;
|
|
||||||
framerateSetting: Setting<number>;
|
|
||||||
bitrateSetting: Setting<number>;
|
|
||||||
codecSetting: Setting<VideoCodec>;
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<h4>{header}</h4>
|
|
||||||
<FieldRow>
|
|
||||||
<InputField
|
|
||||||
id={`${id}Toggle`}
|
|
||||||
label={toggleLabel}
|
|
||||||
description={description}
|
|
||||||
type="checkbox"
|
|
||||||
checked={advancedEnabled}
|
|
||||||
onChange={(e): void => setAdvancedEnabled(e.target.checked)}
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
{advancedEnabled && (
|
|
||||||
<>
|
|
||||||
<div className={styles.volumeSlider}>
|
|
||||||
<label htmlFor={`${id}Resolution`}>
|
|
||||||
{t("settings.resolution_label", "Resolution")}
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id={`${id}Resolution`}
|
|
||||||
value={resolution}
|
|
||||||
onChange={(e): void => setResolution(e.target.value)}
|
|
||||||
>
|
|
||||||
{resolutionOptions.map((opt) => (
|
|
||||||
<option key={opt.value} value={opt.value}>
|
|
||||||
{opt.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className={styles.volumeSlider}>
|
|
||||||
<label>
|
|
||||||
{t("settings.framerate_label", "Framerate")}
|
|
||||||
{": "}
|
|
||||||
<span className={styles.settingValue}>{framerateRaw} fps</span>
|
|
||||||
</label>
|
|
||||||
<Slider
|
|
||||||
label={t("settings.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.bitrate_label", "Bitrate")}
|
|
||||||
{": "}
|
|
||||||
<span className={styles.settingValue}>
|
|
||||||
{(bitrateRaw / 1_000_000).toFixed(1)} Mbps
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Slider
|
|
||||||
label={t("settings.bitrate_label", "Bitrate")}
|
|
||||||
value={bitrateRaw}
|
|
||||||
onValueChange={setBitrateRaw}
|
|
||||||
onValueCommit={setBitrate}
|
|
||||||
min={bitrateRange.min}
|
|
||||||
max={bitrateRange.max}
|
|
||||||
step={bitrateRange.step}
|
|
||||||
tooltipFormatter={(v): string =>
|
|
||||||
`${(v / 1_000_000).toFixed(1)} Mbps`
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={styles.volumeSlider}>
|
|
||||||
<label htmlFor={`${id}Codec`}>
|
|
||||||
{t("settings.codec_label", "Codec")}
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id={`${id}Codec`}
|
|
||||||
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>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AudioProcessingSettings: React.FC = (): ReactNode => {
|
|
||||||
const [echoCancellation, setEchoCancellation] = useSetting(
|
|
||||||
echoCancellationSetting,
|
|
||||||
);
|
|
||||||
const [noiseSuppression, setNoiseSuppression] = useSetting(
|
|
||||||
noiseSuppressionSetting,
|
|
||||||
);
|
|
||||||
const [autoGainControl, setAutoGainControl] = useSetting(
|
|
||||||
autoGainControlSetting,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h4>{t("settings.audio_processing_header", "Audio processing")}</h4>
|
|
||||||
<p>
|
|
||||||
{t(
|
|
||||||
"settings.audio_processing_description",
|
|
||||||
"Changes apply on next call join.",
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<FieldRow>
|
|
||||||
<InputField
|
|
||||||
id="echoCancellation"
|
|
||||||
label={t(
|
|
||||||
"settings.echo_cancellation_label",
|
|
||||||
"Echo cancellation",
|
|
||||||
)}
|
|
||||||
type="checkbox"
|
|
||||||
checked={echoCancellation}
|
|
||||||
onChange={(e): void => setEchoCancellation(e.target.checked)}
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
<FieldRow>
|
|
||||||
<InputField
|
|
||||||
id="noiseSuppression"
|
|
||||||
label={t(
|
|
||||||
"settings.noise_suppression_label",
|
|
||||||
"Noise suppression",
|
|
||||||
)}
|
|
||||||
type="checkbox"
|
|
||||||
checked={noiseSuppression}
|
|
||||||
onChange={(e): void => setNoiseSuppression(e.target.checked)}
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
<FieldRow>
|
|
||||||
<InputField
|
|
||||||
id="autoGainControl"
|
|
||||||
label={t(
|
|
||||||
"settings.auto_gain_control_label",
|
|
||||||
"Automatic gain control",
|
|
||||||
)}
|
|
||||||
type="checkbox"
|
|
||||||
checked={autoGainControl}
|
|
||||||
onChange={(e): void => setAutoGainControl(e.target.checked)}
|
|
||||||
/>
|
|
||||||
</FieldRow>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const devices = useMediaDevices();
|
const devices = useMediaDevices();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) devices.requestDeviceNames();
|
if (open) devices.requestDeviceNames();
|
||||||
@@ -371,8 +171,6 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
<Separator />
|
|
||||||
<AudioProcessingSettings />
|
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@@ -391,46 +189,6 @@ export const SettingsModal: FC<Props> = ({
|
|||||||
</Form>
|
</Form>
|
||||||
<Separator />
|
<Separator />
|
||||||
<BlurCheckbox />
|
<BlurCheckbox />
|
||||||
<Separator />
|
|
||||||
<MediaQualitySettings
|
|
||||||
id="camera"
|
|
||||||
header={t("settings.camera_header", "Camera quality")}
|
|
||||||
toggleLabel={t("settings.advanced_camera_label", "Advanced camera settings")}
|
|
||||||
description={t("settings.advanced_camera_description", "Configure resolution, framerate, bitrate, and codec for camera video. Changes apply on next call join.")}
|
|
||||||
toggleSetting={advancedCameraSetting}
|
|
||||||
resolutionSetting={cameraResolutionSetting}
|
|
||||||
framerateSetting={cameraFramerateSetting}
|
|
||||||
bitrateSetting={cameraBitrateSetting}
|
|
||||||
codecSetting={cameraCodecSetting}
|
|
||||||
resolutionOptions={[
|
|
||||||
{ value: "640x360", label: "360p" },
|
|
||||||
{ value: "960x540", label: "540p" },
|
|
||||||
{ value: "1280x720", label: "720p" },
|
|
||||||
{ value: "1920x1080", label: "1080p" },
|
|
||||||
{ value: "2560x1440", label: "1440p" },
|
|
||||||
]}
|
|
||||||
bitrateRange={{ min: 200_000, max: 8_000_000, step: 100_000 }}
|
|
||||||
/>
|
|
||||||
<Separator />
|
|
||||||
<MediaQualitySettings
|
|
||||||
id="screenShare"
|
|
||||||
header={t("settings.screen_share_header", "Screen sharing")}
|
|
||||||
toggleLabel={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")}
|
|
||||||
toggleSetting={advancedScreenShareSetting}
|
|
||||||
resolutionSetting={screenShareResolutionSetting}
|
|
||||||
framerateSetting={screenShareFramerateSetting}
|
|
||||||
bitrateSetting={screenShareBitrateSetting}
|
|
||||||
codecSetting={screenShareCodecSetting}
|
|
||||||
resolutionOptions={[
|
|
||||||
{ value: "1024x576", label: "576p" },
|
|
||||||
{ value: "1280x720", label: "720p" },
|
|
||||||
{ value: "1920x1080", label: "1080p" },
|
|
||||||
{ value: "2560x1440", label: "1440p" },
|
|
||||||
{ value: "3840x2160", label: "4K" },
|
|
||||||
]}
|
|
||||||
bitrateRange={{ min: 500_000, max: 15_000_000, step: 500_000 }}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -169,7 +169,10 @@ export type VideoCodec = "vp8" | "vp9" | "h264" | "av1";
|
|||||||
/**
|
/**
|
||||||
* Parse a "WIDTHxHEIGHT" resolution string into numeric width and height.
|
* 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);
|
const [w, h] = res.split("x").map(Number);
|
||||||
return { width: w, height: h };
|
return { width: w, height: h };
|
||||||
}
|
}
|
||||||
@@ -209,10 +212,7 @@ export const cameraResolution = new Setting<string>(
|
|||||||
|
|
||||||
export const cameraFramerate = new Setting<number>("camera-framerate", 30);
|
export const cameraFramerate = new Setting<number>("camera-framerate", 30);
|
||||||
|
|
||||||
export const cameraBitrate = new Setting<number>(
|
export const cameraBitrate = new Setting<number>("camera-bitrate", 1_700_000);
|
||||||
"camera-bitrate",
|
|
||||||
1_700_000,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const cameraCodec = new Setting<VideoCodec>("camera-codec", "vp8");
|
export const cameraCodec = new Setting<VideoCodec>("camera-codec", "vp8");
|
||||||
|
|
||||||
@@ -238,19 +238,21 @@ export const autoGainControlSetting = new Setting<boolean>(
|
|||||||
* Only updates settings that the user hasn't explicitly set in localStorage.
|
* Only updates settings that the user hasn't explicitly set in localStorage.
|
||||||
*/
|
*/
|
||||||
export function seedSettingsFromConfig(
|
export function seedSettingsFromConfig(
|
||||||
mediaQuality: {
|
mediaQuality:
|
||||||
video_codec?: VideoCodec;
|
| {
|
||||||
video?: {
|
video_codec?: VideoCodec;
|
||||||
max_resolution?: number;
|
video?: {
|
||||||
max_bitrate?: number;
|
max_resolution?: number;
|
||||||
max_framerate?: number;
|
max_bitrate?: number;
|
||||||
};
|
max_framerate?: number;
|
||||||
screen_share?: {
|
};
|
||||||
max_resolution?: number;
|
screen_share?: {
|
||||||
max_bitrate?: number;
|
max_resolution?: number;
|
||||||
max_framerate?: number;
|
max_bitrate?: number;
|
||||||
};
|
max_framerate?: number;
|
||||||
} | undefined,
|
};
|
||||||
|
}
|
||||||
|
| undefined,
|
||||||
): void {
|
): void {
|
||||||
if (!mediaQuality) return;
|
if (!mediaQuality) return;
|
||||||
|
|
||||||
|
|||||||
@@ -689,7 +689,9 @@ export const createLocalMembership$ = ({
|
|||||||
|
|
||||||
if (advancedScreenShare.getValue()) {
|
if (advancedScreenShare.getValue()) {
|
||||||
// User has advanced screen share settings enabled
|
// User has advanced screen share settings enabled
|
||||||
const { width, height } = parseResolution(screenShareResolution.getValue());
|
const { width, height } = parseResolution(
|
||||||
|
screenShareResolution.getValue(),
|
||||||
|
);
|
||||||
const fps = screenShareFramerate.getValue();
|
const fps = screenShareFramerate.getValue();
|
||||||
const bps = screenShareBitrate.getValue();
|
const bps = screenShareBitrate.getValue();
|
||||||
const codec = screenShareCodec.getValue();
|
const codec = screenShareCodec.getValue();
|
||||||
|
|||||||
Reference in New Issue
Block a user