2026-07-28 22:46:19 +02:00
|
|
|
/*
|
|
|
|
|
Copyright 2022-2024 New Vector Ltd.
|
|
|
|
|
|
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
|
|
|
Please see LICENSE in the repository root for full details.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { type FC, type ReactNode, useState } from "react";
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
|
|
|
|
|
import { FieldRow, InputField } from "../input/Input";
|
|
|
|
|
import { type Setting, useSetting, type VideoCodec } from "./settings";
|
|
|
|
|
import settingsStyles from "./SettingsModal.module.css";
|
|
|
|
|
import { Slider } from "../Slider";
|
|
|
|
|
|
|
|
|
|
export const MediaQualitySettings: 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 { t } = useTranslation();
|
|
|
|
|
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>
|
2026-07-28 23:08:11 +02:00
|
|
|
{/* Only list codecs the SFU actually accepts right now (confirmed
|
|
|
|
|
live via matrix-rtc-sfu negotiation logs, 2026-07-28 -
|
|
|
|
|
enabledPublishCodecs was [VP8, H264, H265]). VP9/AV1 are not
|
|
|
|
|
currently negotiated by the SFU - selecting them silently
|
|
|
|
|
falls back to VP8 via the client's backupCodec, so leaving
|
|
|
|
|
them out avoids offering a choice that has no real effect. */}
|
2026-07-28 22:46:19 +02:00
|
|
|
<select
|
|
|
|
|
id={`${id}Codec`}
|
|
|
|
|
value={codec}
|
|
|
|
|
onChange={(e): void => setCodec(e.target.value as VideoCodec)}
|
|
|
|
|
>
|
|
|
|
|
<option value="vp8">VP8</option>
|
|
|
|
|
<option value="h264">H.264</option>
|
2026-07-28 23:08:11 +02:00
|
|
|
<option value="h265">H.265</option>
|
2026-07-28 22:46:19 +02:00
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|